From 6863493ddbec8c542715f4e83d4b4d062de8e1a6 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 17:39:39 +0800 Subject: [PATCH 001/678] Migrate pagx library from tgfx repository to libpag with SVG test resources and automated tests. --- CMakeLists.txt | 12 +- pagx/CMakeLists.txt | 39 + pagx/docs/pagx_spec_v1.0.md | 1780 ++++++++++++++++++++ pagx/include/pagx/layers/LayerBuilder.h | 72 + pagx/include/pagx/layers/TextLayouter.h | 53 + pagx/include/pagx/svg/SVGImporter.h | 63 + pagx/src/layers/LayerBuilder.cpp | 62 + pagx/src/layers/PAGXAttributes.cpp | 336 ++++ pagx/src/layers/PAGXAttributes.h | 84 + pagx/src/layers/PAGXParser.cpp | 796 +++++++++ pagx/src/layers/PAGXParser.h | 137 ++ pagx/src/layers/PAGXUtils.cpp | 80 + pagx/src/layers/PAGXUtils.h | 29 + pagx/src/layers/TextLayouter.cpp | 170 ++ pagx/src/svg/SVGImporter.cpp | 68 + pagx/src/svg/SVGToPAGXConverter.cpp | 1897 ++++++++++++++++++++++ pagx/src/svg/SVGToPAGXConverter.h | 137 ++ pagx/viewer/.gitignore | 9 + pagx/viewer/CMakeLists.txt | 69 + pagx/viewer/index.css | 167 ++ pagx/viewer/index.html | 45 + pagx/viewer/index.ts | 428 +++++ pagx/viewer/package.json | 31 + pagx/viewer/script/cmake.js | 47 + pagx/viewer/script/rollup.js | 62 + pagx/viewer/server.js | 54 + pagx/viewer/src/GridBackground.cpp | 65 + pagx/viewer/src/GridBackground.h | 42 + pagx/viewer/src/PAGXView.cpp | 192 +++ pagx/viewer/src/PAGXView.h | 66 + pagx/viewer/src/binding.cpp | 40 + pagx/viewer/tsconfig.json | 17 + pagx/viewer/types.ts | 62 + resources/apitest/SVG/4w_node.svg | 1 + resources/apitest/SVG/Baseline.svg | 1174 +++++++++++++ resources/apitest/SVG/ColorPicker.svg | 563 +++++++ resources/apitest/SVG/Guidelines.svg | 540 ++++++ resources/apitest/SVG/Overview.svg | 1328 +++++++++++++++ resources/apitest/SVG/Switch.svg | 111 ++ resources/apitest/SVG/UIkit.svg | 898 ++++++++++ resources/apitest/SVG/blur.svg | 12 + resources/apitest/SVG/complex1.svg | 8 + resources/apitest/SVG/complex2.svg | 1 + resources/apitest/SVG/complex3.svg | 67 + resources/apitest/SVG/complex4.svg | 1 + resources/apitest/SVG/complex5.svg | 1502 +++++++++++++++++ resources/apitest/SVG/complex6.svg | 163 ++ resources/apitest/SVG/complex7.svg | 264 +++ resources/apitest/SVG/displayp3.svg | 8 + resources/apitest/SVG/drawImageRect.svg | 15 + resources/apitest/SVG/emoji.svg | 8 + resources/apitest/SVG/jpg.svg | 9 + resources/apitest/SVG/mask.svg | 18 + resources/apitest/SVG/path.svg | 153 ++ resources/apitest/SVG/png.svg | 9 + resources/apitest/SVG/radialGradient.svg | 10 + resources/apitest/SVG/refStyle.svg | 16 + resources/apitest/SVG/text.svg | 8 + resources/apitest/SVG/textFont.svg | 9 + resources/apitest/SVG/widegamut.svg | 13 + test/src/PAGXTest.cpp | 157 ++ 61 files changed, 14275 insertions(+), 2 deletions(-) create mode 100644 pagx/CMakeLists.txt create mode 100644 pagx/docs/pagx_spec_v1.0.md create mode 100644 pagx/include/pagx/layers/LayerBuilder.h create mode 100644 pagx/include/pagx/layers/TextLayouter.h create mode 100644 pagx/include/pagx/svg/SVGImporter.h create mode 100644 pagx/src/layers/LayerBuilder.cpp create mode 100644 pagx/src/layers/PAGXAttributes.cpp create mode 100644 pagx/src/layers/PAGXAttributes.h create mode 100644 pagx/src/layers/PAGXParser.cpp create mode 100644 pagx/src/layers/PAGXParser.h create mode 100644 pagx/src/layers/PAGXUtils.cpp create mode 100644 pagx/src/layers/PAGXUtils.h create mode 100644 pagx/src/layers/TextLayouter.cpp create mode 100644 pagx/src/svg/SVGImporter.cpp create mode 100644 pagx/src/svg/SVGToPAGXConverter.cpp create mode 100644 pagx/src/svg/SVGToPAGXConverter.h create mode 100644 pagx/viewer/.gitignore create mode 100644 pagx/viewer/CMakeLists.txt create mode 100644 pagx/viewer/index.css create mode 100644 pagx/viewer/index.html create mode 100644 pagx/viewer/index.ts create mode 100644 pagx/viewer/package.json create mode 100644 pagx/viewer/script/cmake.js create mode 100644 pagx/viewer/script/rollup.js create mode 100644 pagx/viewer/server.js create mode 100644 pagx/viewer/src/GridBackground.cpp create mode 100644 pagx/viewer/src/GridBackground.h create mode 100644 pagx/viewer/src/PAGXView.cpp create mode 100644 pagx/viewer/src/PAGXView.h create mode 100644 pagx/viewer/src/binding.cpp create mode 100644 pagx/viewer/tsconfig.json create mode 100644 pagx/viewer/types.ts create mode 100644 resources/apitest/SVG/4w_node.svg create mode 100644 resources/apitest/SVG/Baseline.svg create mode 100644 resources/apitest/SVG/ColorPicker.svg create mode 100644 resources/apitest/SVG/Guidelines.svg create mode 100644 resources/apitest/SVG/Overview.svg create mode 100644 resources/apitest/SVG/Switch.svg create mode 100644 resources/apitest/SVG/UIkit.svg create mode 100644 resources/apitest/SVG/blur.svg create mode 100644 resources/apitest/SVG/complex1.svg create mode 100644 resources/apitest/SVG/complex2.svg create mode 100644 resources/apitest/SVG/complex3.svg create mode 100644 resources/apitest/SVG/complex4.svg create mode 100644 resources/apitest/SVG/complex5.svg create mode 100644 resources/apitest/SVG/complex6.svg create mode 100644 resources/apitest/SVG/complex7.svg create mode 100644 resources/apitest/SVG/displayp3.svg create mode 100644 resources/apitest/SVG/drawImageRect.svg create mode 100644 resources/apitest/SVG/emoji.svg create mode 100644 resources/apitest/SVG/jpg.svg create mode 100644 resources/apitest/SVG/mask.svg create mode 100644 resources/apitest/SVG/path.svg create mode 100644 resources/apitest/SVG/png.svg create mode 100644 resources/apitest/SVG/radialGradient.svg create mode 100644 resources/apitest/SVG/refStyle.svg create mode 100644 resources/apitest/SVG/text.svg create mode 100644 resources/apitest/SVG/textFont.svg create mode 100644 resources/apitest/SVG/widegamut.svg create mode 100644 test/src/PAGXTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d8b05fe977..0935dccd7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,8 @@ if (PAG_BUILD_TESTS) set(PAG_USE_HARFBUZZ ON) set(PAG_USE_SYSTEM_LZ4 OFF) set(PAG_BUILD_SHARED OFF) + set(PAG_BUILD_SVG ON) + set(PAG_BUILD_LAYERS ON) endif () message("PAG_USE_LIBAVC: ${PAG_USE_LIBAVC}") @@ -504,6 +506,8 @@ else () set(TGFX_ENABLE_PROFILING ${PAG_ENABLE_PROFILING}) set(TGFX_USE_THREADS ${PAG_USE_THREADS}) set(TGFX_USE_TEXT_GAMMA_CORRECTION ON) + set(TGFX_BUILD_SVG ${PAG_BUILD_SVG}) + set(TGFX_BUILD_LAYERS ${PAG_BUILD_LAYERS}) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) add_subdirectory(${TGFX_DIR} tgfx EXCLUDE_FROM_ALL) list(APPEND PAG_STATIC_LIBS $) @@ -633,13 +637,17 @@ if (PAG_BUILD_TESTS) endif () endif () + # Add pagx library for testing + add_subdirectory(pagx) + file(GLOB_RECURSE SRC_FILES test/src/*.*) list(APPEND PAG_TEST_FILES ${SRC_FILES}) - list(APPEND PAG_TEST_LIBS pag ${PAG_SHARED_LIBS}) + list(APPEND PAG_TEST_LIBS pagx pag ${PAG_SHARED_LIBS}) set(GOOGLE_TEST_DIR ${TGFX_DIR}/third_party/googletest/googletest) list(APPEND PAG_TEST_INCLUDES ${PAG_INCLUDES} test/src ${GOOGLE_TEST_DIR} ${GOOGLE_TEST_DIR}/include - ${TGFX_DIR}/third_party/json/include) + ${TGFX_DIR}/third_party/json/include + pagx/include pagx/src/layers pagx/src/svg) add_vendor_target(test-vendor STATIC_VENDORS googletest CONFIG_DIR ${TGFX_DIR}) find_vendor_libraries(test-vendor STATIC TEST_VENDOR_STATIC_LIBRARIES) diff --git a/pagx/CMakeLists.txt b/pagx/CMakeLists.txt new file mode 100644 index 0000000000..8e72c3239f --- /dev/null +++ b/pagx/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.13) +project(PAGX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +option(PAGX_BUILD_SVG "Build PAGX SVG converter module" ON) + +# tgfx must be available as a target (added by parent project or find_package) +if (NOT TARGET tgfx) + message(FATAL_ERROR "tgfx target not found. Please add pagx as a subdirectory of a project that includes tgfx.") +endif() + +# PAGX library sources +file(GLOB PAGX_LAYERS_SOURCES src/layers/*.cpp) +list(APPEND PAGX_SOURCES ${PAGX_LAYERS_SOURCES}) + +if (PAGX_BUILD_SVG) + file(GLOB PAGX_SVG_SOURCES src/svg/*.cpp) + list(APPEND PAGX_SOURCES ${PAGX_SVG_SOURCES}) +endif() + +add_library(pagx STATIC ${PAGX_SOURCES}) + +target_include_directories(pagx PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_include_directories(pagx PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/layers +) + +if (PAGX_BUILD_SVG) + target_include_directories(pagx PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/svg + ) +endif() + +target_link_libraries(pagx PUBLIC tgfx) diff --git a/pagx/docs/pagx_spec_v1.0.md b/pagx/docs/pagx_spec_v1.0.md new file mode 100644 index 0000000000..591d032ee6 --- /dev/null +++ b/pagx/docs/pagx_spec_v1.0.md @@ -0,0 +1,1780 @@ +# PAGX 格式规范 v1.0 + +## 1. Introduction(介绍) + +**PAGX**(Portable Animated Graphics XML)是一种基于 XML 的矢量动画标记语言。它提供了统一且强大的矢量图形与动画描述能力,旨在成为跨所有主要工具与运行时的矢量动画交换标准。 + +**PAGX** (Portable Animated Graphics XML) is an XML-based markup language for describing animated vector graphics. It provides a unified and powerful representation of vector graphics and animations, designed to be the vector animation interchange standard across all major tools and runtimes. + +### 1.1 设计目标 + +- **开放可读**:纯文本 XML 格式,易于阅读和编辑,天然支持版本控制与差异对比,便于调试及 AI 理解与生成。 + +- **特性完备**:完整覆盖矢量图形、图片、富文本、滤镜效果、混合模式、遮罩等能力,满足复杂动效的描述需求。 + +- **精简高效**:提供简洁且强大的统一结构,兼顾静态矢量与动画的优化描述,同时预留未来交互和脚本的扩展能力。 + +- **生态兼容**:可作为 After Effects、Figma、腾讯设计等设计工具的通用交换格式,实现设计资产无缝流转。 + +- **高效部署**:设计资产可一键导出并部署到研发环境,转换为二进制 PAG 格式后获得极高压缩比和运行时性能。 + +### 1.2 文件结构 + +PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也支持 base64 数据 URI 内嵌图片。发布时可转换为内嵌所有资源的二进制 PAG 格式以优化加载性能。 + +### 1.3 文档组织 + +本规范按以下顺序组织: + +1. **基础数据类型**:定义文档中使用的基本数据格式 +2. **文档结构**:描述 PAGX 文档的整体组织方式 +3. **图层系统**:定义图层及其相关特性(样式、滤镜、遮罩) +4. **矢量元素系统**:定义图层内容的矢量元素及其处理模型 + +--- + +## 2. Basic Data Types(基础数据类型) + +本节定义 PAGX 文档中使用的基础数据类型和命名规范。 + +### 2.1 命名规范 + +| 类别 | 规范 | 示例 | +|------|------|------| +| 元素名 | PascalCase,不缩写 | `Group`、`Rectangle`、`Fill` | +| 属性名 | camelCase,尽量简短 | `antiAlias`、`blendMode`、`fontSize` | +| 属性节点 | camelCase | ``、``、`` | +| 默认单位 | 像素(无需标注) | `width="100"` | +| 角度单位 | 度 | `rotation="45"` | + +### 2.2 基本数值类型 + +| 类型 | 说明 | 示例 | +|------|------|------| +| `float` | 浮点数 | `1.5`、`-0.5`、`100` | +| `int` | 整数 | `400`、`0`、`-1` | +| `bool` | 布尔值 | `true`、`false` | +| `string` | 字符串 | `"Arial"`、`"myLayer"` | +| `enum` | 枚举值 | `normal`、`multiply` | +| `idref` | ID 引用 | `#gradientId`、`#maskLayer` | + +### 2.3 点(Point) + +点使用逗号分隔的两个浮点数表示: + +``` +"x,y" +``` + +**示例**:`"100,200"`、`"0.5,0.5"`、`"-50,100"` + +### 2.4 矩形(Rect) + +矩形使用逗号分隔的四个浮点数表示: + +``` +"x,y,width,height" +``` + +**示例**:`"0,0,100,100"`、`"10,20,200,150"` + +### 2.5 变换矩阵(Matrix) + +#### 2D 变换矩阵 + +2D 变换使用 6 个逗号分隔的浮点数表示,对应标准 2D 仿射变换矩阵: + +``` +"a,b,c,d,tx,ty" +``` + +矩阵形式: +``` +| a c tx | +| b d ty | +| 0 0 1 | +``` + +**单位矩阵**:`"1,0,0,1,0,0"` + +#### 3D 变换矩阵 + +3D 变换使用 16 个逗号分隔的浮点数表示,列优先顺序: + +``` +"m00,m10,m20,m30,m01,m11,m21,m31,m02,m12,m22,m32,m03,m13,m23,m33" +``` + +### 2.6 颜色(Color) + +PAGX 支持多种颜色格式: + +| 格式 | 示例 | 说明 | +|------|------|------| +| HEX | `#RGB`、`#RRGGBB`、`#RRGGBBAA` | 十六进制 | +| RGB | `rgb(255,0,0)`、`rgba(255,0,0,0.5)` | RGB 带可选透明度 | +| HSL | `hsl(0,100%,50%)`、`hsla(0,100%,50%,0.5)` | HSL 带可选透明度 | +| 色域 | `color(display-p3 1 0 0)` | 广色域颜色 | +| 引用 | `#resourceId` | 引用 Resources 中定义的颜色源 | + +### 2.7 路径数据(Path Data) + +路径数据使用 SVG 路径语法,支持以下命令: + +| 命令 | 参数 | 说明 | +|------|------|------| +| M/m | x y | 移动到(绝对/相对) | +| L/l | x y | 直线到 | +| H/h | x | 水平线到 | +| V/v | y | 垂直线到 | +| C/c | x1 y1 x2 y2 x y | 三次贝塞尔曲线 | +| S/s | x2 y2 x y | 平滑三次贝塞尔 | +| Q/q | x1 y1 x y | 二次贝塞尔曲线 | +| T/t | x y | 平滑二次贝塞尔 | +| A/a | rx ry rotation large-arc sweep x y | 椭圆弧 | +| Z/z | - | 闭合路径 | + +**示例**:`"M 0 0 L 100 0 L 100 100 Z"` + +### 2.8 图片(Image) + +图片资源定义可在文档中引用的位图数据。 + +```xml + + + + + +``` + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `id` | string | 是 | 唯一标识 | +| `source` | string | 是 | 文件路径或数据 URI | + +**支持格式**:PNG、JPEG、WebP、GIF + +### 2.9 颜色源(Color Source) + +颜色源定义可用于渲染的颜色,支持两种定义方式: + +1. **共享定义**:在 `` 中预定义,通过 `#id` 引用。适用于**被多处引用**的颜色源。 +2. **内联定义**:直接嵌套在 `` 或 `` 元素内部。适用于**仅使用一次**的颜色源,更简洁。 + +**最佳实践**: +- 只使用一次的颜色源应内联定义,避免在 Resources 中增加不必要的条目 +- 被多处引用的颜色源应定义在 Resources 中以便复用 +- ImagePattern 使用 objectBoundingBox 时(tile 尺寸依赖形状尺寸),通常需要内联定义,因为不同形状需要不同的 matrix + +#### 2.9.1 SolidColor(纯色) + +```xml + +``` + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `id` | string | 是(Resources 中) | 唯一标识 | +| `color` | color | 是 | 颜色值 | + +#### 2.9.2 LinearGradient(线性渐变) + +线性渐变沿起点到终点的方向插值。 + +```xml + + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | - | 唯一标识 | +| `startX` | float | 0 | 起点 X | +| `startY` | float | 0 | 起点 Y | +| `endX` | float | - | 终点 X | +| `endY` | float | - | 终点 Y | +| `matrix` | string | 单位矩阵 | 变换矩阵 | + +**计算**:对于点 P,其颜色由 P 在起点-终点连线上的投影位置决定。 + +#### 2.9.3 RadialGradient(径向渐变) + +径向渐变从中心向外辐射。 + +```xml + + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | - | 唯一标识 | +| `centerX` | float | 0 | 中心点 X | +| `centerY` | float | 0 | 中心点 Y | +| `radius` | float | - | 渐变半径 | +| `matrix` | string | 单位矩阵 | 变换矩阵 | + +**计算**:对于点 P,其颜色由 `distance(P, center) / radius` 决定。 + +#### 2.9.4 ConicGradient(锥形渐变) + +锥形渐变(也称扫描渐变)沿圆周方向插值。 + +```xml + + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | - | 唯一标识 | +| `centerX` | float | 0 | 中心点 X | +| `centerY` | float | 0 | 中心点 Y | +| `startAngle` | float | 0 | 起始角度 | +| `endAngle` | float | 360 | 结束角度 | +| `matrix` | string | 单位矩阵 | 变换矩阵 | + +**计算**:对于点 P,其颜色由 `atan2(P.y - center.y, P.x - center.x)` 在 `[startAngle, endAngle]` 范围内的比例决定。 + +#### 2.9.5 DiamondGradient(菱形渐变) + +菱形渐变从中心向四角辐射。 + +```xml + + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | - | 唯一标识 | +| `centerX` | float | 0 | 中心点 X | +| `centerY` | float | 0 | 中心点 Y | +| `halfDiagonal` | float | - | 半对角线长度 | +| `matrix` | string | 单位矩阵 | 变换矩阵 | + +**计算**:对于点 P,其颜色由曼哈顿距离 `(|P.x - center.x| + |P.y - center.y|) / halfDiagonal` 决定。 + +#### 2.9.6 ColorStop(渐变色标) + +```xml + +``` + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `offset` | float | 是 | 位置 0.0~1.0 | +| `color` | color | 是 | 色标颜色 | + +**渐变通用规则**: + +- **色标插值**:相邻色标之间使用线性插值 +- **色标边界**: + - `offset < 0` 的色标被视为 `offset = 0` + - `offset > 1` 的色标被视为 `offset = 1` + - 如果没有 `offset = 0` 的色标,使用第一个色标的颜色填充 + - 如果没有 `offset = 1` 的色标,使用最后一个色标的颜色填充 +- **渐变变换**:`matrix` 属性对渐变坐标系应用变换 + +#### 2.9.8 颜色源坐标系统 + +所有颜色源(渐变、图案)的坐标系是**相对于几何元素的局部坐标系原点**。 + +**变换行为**: + +1. **外部变换会同时作用于几何和颜色源**:Group 的变换、Layer 的矩阵等外部变换会整体作用于几何元素及其颜色源,两者一起缩放、旋转、平移。 + +2. **修改几何属性不影响颜色源**:直接修改几何元素的属性(如 Rectangle 的 width/height、Path 的路径数据)只改变几何内容本身,不会影响颜色源的坐标系。 + +**示例**:在 100×100 的区域内绘制一个从左到右的线性渐变: + +```xml + + + + + + + + + + + +``` + +- 对该图层应用 `scale(2, 2)` 变换:矩形变为 200×200,渐变也随之放大,视觉效果保持一致 +- 直接将 Rectangle 的 width/height 改为 200:矩形变为 200×200,但渐变坐标不变,只覆盖矩形的左半部分 + +#### 2.9.7 ImagePattern(图片图案) + +图片图案使用图片作为颜色源。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | - | 唯一标识 | +| `image` | idref | - | 图片引用 "#id" | +| `tileModeX` | TileMode | clamp | X 方向平铺模式(见下方) | +| `tileModeY` | TileMode | clamp | Y 方向平铺模式(见下方) | +| `sampling` | SamplingMode | linear | 采样模式(见下方) | +| `matrix` | string | 单位矩阵 | 变换矩阵 | + +**TileMode(平铺模式)**: + +| 值 | 说明 | +|------|------| +| `clamp` | 钳制:超出边界使用边缘像素颜色 | +| `repeat` | 重复:平铺图片 | +| `mirror` | 镜像:交替翻转平铺 | +| `decal` | 贴花:超出边界为透明 | + +**SamplingMode(采样模式)**: + +| 值 | 说明 | +|------|------| +| `nearest` | 最近邻:取最近像素,锐利但有锯齿 | +| `linear` | 双线性:平滑插值 | +| `mipmap` | 多级渐远:根据缩放级别选择合适的 mip 层 | + +**图案变换**: + +`matrix` 属性对图案应用变换,效果与对普通图形元素的变换一致: + +- `matrix="2,0,0,2,0,0"`(缩放 2 倍):图案视觉上**放大** 2 倍 +- `matrix="0.5,0,0,0.5,0,0"`(缩放 0.5 倍):图案视觉上**缩小**到原来的 1/2 + +**示例**:假设有一个 12×12 像素的棋盘格图片,希望每个 tile 显示为 24×24 像素: + +```xml + + +``` + +--- + +## 3. Document Structure(文档结构) + +本节定义 PAGX 文档的整体结构。 + +### 3.1 坐标系统 + +PAGX 使用标准的 2D 笛卡尔坐标系: + +- **原点**:位于画布左上角 +- **X 轴**:向右为正方向 +- **Y 轴**:向下为正方向 +- **角度**:顺时针方向为正(0° 指向 X 轴正方向) +- **单位**:所有长度值默认为像素,角度值默认为度 + +### 3.2 根元素(pagx) + +`` 是 PAGX 文档的根元素,定义画布尺寸并直接包含图层列表。 + +```xml + + + ... + ... + ... + +``` + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `version` | string | 是 | 格式版本 | +| `width` | float | 是 | 画布宽度 | +| `height` | float | 是 | 画布高度 | + +**图层渲染顺序**:图层按文档顺序依次渲染,文档中靠前的图层先渲染(位于下方),靠后的图层后渲染(位于上方)。 + +### 3.3 Resources(资源区) + +`` 定义可复用的资源,包括图片、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `#id` 形式引用。 + +```xml + + + + + + + + + ... + +``` + +#### 3.3.1 Image(图片) + +图片资源定义见 2.8 节。 + +#### 3.3.2 颜色源 + +Resources 中可定义以下颜色源类型(详见 2.9 节): + +- `SolidColor`:纯色 +- `LinearGradient`:线性渐变 +- `RadialGradient`:径向渐变 +- `ConicGradient`:锥形渐变 +- `DiamondGradient`:菱形渐变 +- `ImagePattern`:图片图案 + +#### 3.3.3 Composition(合成) + +合成用于内容复用(类似 After Effects 的 Pre-comp)。 + +```xml + + + + + + + + +``` + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `id` | string | 是 | 唯一标识 | +| `width` | float | 是 | 合成宽度 | +| `height` | float | 是 | 合成高度 | + +### 3.4 文档层级结构 + +PAGX 文档采用层级结构组织内容: + +``` + ← 根元素(定义画布尺寸) +├── ← 资源区(可选,定义可复用资源) +│ ├── ← 图片资源 +│ ├── ← 纯色定义 +│ ├── ← 渐变定义 +│ ├── ← 图片图案定义 +│ └── ← 合成定义 +│ └── ← 合成内的图层 +│ +└── ← 图层(可多个) + ├── ← 矢量内容(VectorElement 系统) + │ ├── 几何元素 ← Rectangle、Ellipse、Path、TextSpan 等 + │ ├── 修改器 ← TrimPath、RoundCorner、TextModifier 等 + │ ├── 绘制器 ← Fill、Stroke + │ └── ← 矢量元素容器(可嵌套) + │ + ├── ← 图层样式 + │ └── ← 投影、内阴影等 + │ + ├── ← 滤镜 + │ └── ← 模糊、颜色矩阵等 + │ + └── ← 子图层(递归结构) + └── ... +``` + +--- + +## 4. Layer System(图层系统) + +图层(Layer)是 PAGX 内容组织的基本单元,提供了丰富的视觉效果控制能力。 + +### 4.1 Layer(图层) + +`` 是内容和子图层的基本容器。 + +```xml + + + + + + + + + + + + ... + + +``` + +#### contents 子节点 + +`` 是图层的矢量内容容器,本身相当于一个不带变换属性的 Group,可直接包含几何元素、修改器、绘制器等 VectorElement。 + +#### 图层属性 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | - | 唯一标识,用于引用 | +| `name` | string | "" | 显示名称 | +| `visible` | bool | true | 是否可见 | +| `alpha` | float | 1 | 透明度 0~1 | +| `blendMode` | BlendMode | normal | 混合模式(见下方) | +| `x` | float | 0 | X 位置 | +| `y` | float | 0 | Y 位置 | +| `matrix` | string | 单位矩阵 | 2D 变换 "a,b,c,d,tx,ty" | +| `matrix3D` | string | - | 3D 变换(16 个值,列优先) | +| `preserve3D` | bool | false | 保持 3D 变换 | +| `antiAlias` | bool | true | 边缘抗锯齿 | +| `groupOpacity` | bool | false | 组透明度 | +| `passThroughBackground` | bool | true | 是否允许背景透传给子图层 | +| `excludeChildEffectsInLayerStyle` | bool | false | 图层样式是否排除子图层效果 | +| `scrollRect` | string | - | 滚动裁剪区域 "x,y,w,h" | +| `mask` | idref | - | 遮罩图层引用 "#id" | +| `maskType` | MaskType | alpha | 遮罩类型(见 4.4.2) | +| `composition` | idref | - | 合成引用 "#id" | + +**BlendMode(混合模式)**: + +混合模式定义源颜色(S)如何与目标颜色(D)组合。 + +| 值 | 公式 | 说明 | +|------|------|------| +| `normal` | S | 正常(覆盖) | +| `multiply` | S × D | 正片叠底 | +| `screen` | 1 - (1-S)(1-D) | 滤色 | +| `overlay` | multiply/screen 组合 | 叠加 | +| `darken` | min(S, D) | 变暗 | +| `lighten` | max(S, D) | 变亮 | +| `colorDodge` | D / (1-S) | 颜色减淡 | +| `colorBurn` | 1 - (1-D)/S | 颜色加深 | +| `hardLight` | multiply/screen 反向组合 | 强光 | +| `softLight` | 柔和版叠加 | 柔光 | +| `difference` | \|S - D\| | 差值 | +| `exclusion` | S + D - 2SD | 排除 | +| `hue` | D 的饱和度和亮度 + S 的色相 | 色相 | +| `saturation` | D 的色相和亮度 + S 的饱和度 | 饱和度 | +| `color` | D 的亮度 + S 的色相和饱和度 | 颜色 | +| `luminosity` | S 的亮度 + D 的色相和饱和度 | 亮度 | +| `add` | S + D | 相加 | + +#### 图层渲染流程 + +图层内容(填充、描边等)通过 `placement` 属性分为背景内容和前景内容,默认为背景内容。单个图层渲染时按以下顺序处理: + +1. **图层样式(下方)**:渲染位于内容下方的图层样式(如投影阴影) +2. **图层背景内容**:渲染填充和描边 +3. **子图层**:按文档顺序递归渲染所有子图层 +4. **图层样式(上方)**:渲染位于内容上方的图层样式(如内阴影) +5. **图层前景内容**:渲染填充和描边 +6. **滤镜**:应用滤镜链 + +**图层样式的参考内容**:图层样式计算时使用的参考内容包含背景内容和前景内容的完整形状。例如,当填充为背景、描边为前景时,描边会绘制在子图层之上,但投影阴影仍然基于包含填充和描边的完整形状计算。 + +### 4.2 Layer Styles(图层样式) + +图层样式在图层内容渲染完成后应用。 + +```xml + + + + + +``` + +**所有 LayerStyle 共有属性**: + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `blendMode` | BlendMode | normal | 混合模式(见 4.1) | + +#### 4.2.1 DropShadowStyle(投影阴影) + +在图层外部绘制投影阴影。 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `offsetX` | float | 0 | X 偏移 | +| `offsetY` | float | 0 | Y 偏移 | +| `blurrinessX` | float | 0 | X 模糊半径 | +| `blurrinessY` | float | 0 | Y 模糊半径 | +| `color` | color | #000000 | 阴影颜色 | +| `showBehindLayer` | bool | true | 图层后面是否显示阴影 | + +**渲染步骤**: +1. 将图层内容偏移 `(offsetX, offsetY)` +2. 应用高斯模糊 `(blurrinessX, blurrinessY)` +3. 将图层 alpha 通道替换为 `color` 的颜色 + +**showBehindLayer**: +- `true`:阴影绘制在图层内容下方 +- `false`:阴影绘制在图层内容上方 + +#### 4.2.2 InnerShadowStyle(内阴影) + +在图层内部绘制内阴影。 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `offsetX` | float | 0 | X 偏移 | +| `offsetY` | float | 0 | Y 偏移 | +| `blurrinessX` | float | 0 | X 模糊半径 | +| `blurrinessY` | float | 0 | Y 模糊半径 | +| `color` | color | #000000 | 阴影颜色 | + +**渲染步骤**: +1. 创建图层轮廓的反向遮罩 +2. 偏移并模糊 +3. 与图层内容求交集 + +#### 4.2.3 BackgroundBlurStyle(背景模糊) + +对图层下方的背景应用模糊效果。 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `blurrinessX` | float | 0 | X 模糊半径 | +| `blurrinessY` | float | 0 | Y 模糊半径 | +| `tileMode` | TileMode | mirror | 平铺模式(见下方) | + +**TileMode(平铺模式)**: + +| 值 | 说明 | +|------|------| +| `clamp` | 钳制:超出边界使用边缘像素颜色 | +| `repeat` | 重复:平铺图片 | +| `mirror` | 镜像:交替翻转平铺 | +| `decal` | 贴花:超出边界为透明 | + +**渲染步骤**: +1. 获取图层边界下方的背景内容 +2. 应用高斯模糊 `(blurrinessX, blurrinessY)` +3. 使用图层轮廓作为遮罩裁剪模糊结果 + +### 4.3 Filter Effects(滤镜效果) + +滤镜按文档顺序链式应用,每个滤镜的输出作为下一个滤镜的输入。 + +```xml + + + + + +``` + +#### 4.3.1 BlurFilter(模糊滤镜) + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `blurrinessX` | float | - | X 模糊半径 | +| `blurrinessY` | float | - | Y 模糊半径 | +| `tileMode` | TileMode | decal | 平铺模式(见 4.2.3) | + +#### 4.3.2 DropShadowFilter(投影阴影滤镜) + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `offsetX` | float | 0 | X 偏移 | +| `offsetY` | float | 0 | Y 偏移 | +| `blurrinessX` | float | 0 | X 模糊半径 | +| `blurrinessY` | float | 0 | Y 模糊半径 | +| `color` | color | #000000 | 阴影颜色 | +| `shadowOnly` | bool | false | 仅显示阴影 | + +#### 4.3.3 InnerShadowFilter(内阴影滤镜) + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `offsetX` | float | 0 | X 偏移 | +| `offsetY` | float | 0 | Y 偏移 | +| `blurrinessX` | float | 0 | X 模糊半径 | +| `blurrinessY` | float | 0 | Y 模糊半径 | +| `color` | color | #000000 | 阴影颜色 | +| `shadowOnly` | bool | false | 仅显示阴影 | + +#### 4.3.4 BlendFilter(混合滤镜) + +将指定颜色以指定混合模式叠加到图层上。 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `color` | color | - | 混合颜色 | +| `blendMode` | BlendMode | normal | 混合模式(见 4.1) | + +#### 4.3.5 ColorMatrixFilter(颜色矩阵滤镜) + +使用 4×5 颜色矩阵变换颜色。 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `matrix` | string | - | 4x5 颜色矩阵(20 个逗号分隔的浮点数) | + +**矩阵格式**(20 个值,行优先): +``` +| R' | | m[0] m[1] m[2] m[3] m[4] | | R | +| G' | | m[5] m[6] m[7] m[8] m[9] | | G | +| B' | = | m[10] m[11] m[12] m[13] m[14] | × | B | +| A' | | m[15] m[16] m[17] m[18] m[19] | | A | + | 1 | +``` + +### 4.4 Clipping, Masking and Compositing(裁剪、遮罩与合成) + +#### 4.4.1 scrollRect(滚动裁剪) + +`scrollRect` 属性定义图层的可视区域,超出该区域的内容会被裁剪。 + +```xml + + ... + +``` + +#### 4.4.2 遮罩(Masking) + +通过 `mask` 属性引用另一个图层作为遮罩。 + +```xml + + + + + + +... +``` + +**遮罩类型**: + +**MaskType(遮罩类型)**: + +| 值 | 说明 | +|------|------| +| `alpha` | Alpha 遮罩:使用遮罩的 alpha 通道 | +| `luminance` | 亮度遮罩:使用遮罩的亮度值 | +| `contour` | 轮廓遮罩:使用遮罩的轮廓进行裁剪 | + +**遮罩规则**: +- 遮罩图层自身不渲染(`visible` 属性被忽略) +- 遮罩图层的变换不影响被遮罩图层 + +#### 4.4.3 混合模式(Blend Modes) + +混合模式详见 4.1 BlendMode。 + +--- + +## 5. VectorElement System(矢量元素系统) + +矢量元素系统定义了图层 `` 内的矢量内容如何被处理和渲染。 + +### 5.1 Processing Model(处理模型) + +VectorElement 系统采用**累积-渲染**的处理模型:几何元素在渲染上下文中累积,修改器对累积的几何进行变换,绘制器触发最终渲染。 + +#### 5.1.1 术语定义 + +| 术语 | 包含元素 | 说明 | +|------|----------|------| +| **几何元素** | Rectangle、Ellipse、Polystar、Path、TextSpan | 提供几何形状的元素,在上下文中累积为几何列表 | +| **修改器** | TrimPath、RoundCorner、MergePath、TextModifier、TextPath、TextLayout、Repeater | 对累积的几何进行变换 | +| **绘制器** | Fill、Stroke | 对累积的几何进行填充或描边渲染 | +| **容器** | Group | 创建独立作用域并应用矩阵变换,处理完成后合并 | + +#### 5.1.2 几何元素的内部结构 + +几何元素在上下文中累积时,内部结构有所不同: + +| 元素类型 | 内部结构 | 说明 | +|----------|----------|------| +| 形状元素(Rectangle、Ellipse、Polystar、Path) | 单个 Path | 每个形状元素产生一个路径 | +| 文本元素(TextSpan) | 字形列表 | 一个 TextSpan 经过塑形后产生多个字形 | + +#### 5.1.3 处理与渲染顺序 + +VectorElement 按**文档顺序**依次处理,文档中靠前的元素先处理。默认情况下,先处理的绘制器先渲染(位于下方)。 + +由于 Fill 和 Stroke 可通过 `placement` 属性指定渲染到图层的背景或前景,因此最终渲染顺序可能与文档顺序不完全一致。但在默认情况下(所有内容均为背景),渲染顺序与文档顺序一致。 + +#### 5.1.4 统一处理流程 + +``` +几何元素 修改器 绘制器 +┌──────────┐ ┌──────────┐ ┌──────────┐ +│Rectangle │ │ TrimPath │ │ Fill │ +│ Ellipse │ │RoundCorn │ │ Stroke │ +│ Polystar │ │MergePath │ └────┬─────┘ +│ Path │ │TextModif │ │ +│ TextSpan │ │ TextPath │ │ +└────┬─────┘ │TextLayout│ │ + │ │ Repeater │ │ + │ └────┬─────┘ │ + │ │ │ + │ 累积几何 │ 变换几何 │ 渲染 + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────┐ +│ 几何列表 [Path1, Path2, 字形列表1, 字形列表2...] │ +└─────────────────────────────────────────────────────────┘ +``` + +**渲染上下文**累积的是一个几何列表,其中: +- 每个形状元素贡献一个 Path +- 每个 TextSpan 贡献一个字形列表(包含多个字形) + +#### 5.1.5 修改器的作用范围 + +不同修改器对几何列表中的元素有不同的作用范围: + +| 修改器类型 | 作用对象 | 说明 | +|------------|----------|------| +| 形状修改器(TrimPath、RoundCorner、MergePath) | 仅 Path | 对文本触发强制转换 | +| 文本修改器(TextModifier、TextPath、TextLayout) | 仅字形列表 | 对 Path 无效 | +| 复制器(Repeater) | Path + 字形列表 | 同时作用于所有几何 | + +### 5.2 Geometry Elements(几何元素) + +几何元素提供可渲染的形状。 + +#### 5.2.1 Rectangle(矩形) + +矩形从中心点定义,支持统一圆角。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `centerX` | float | 0 | 中心 X | +| `centerY` | float | 0 | 中心 Y | +| `width` | float | 0 | 宽度 | +| `height` | float | 0 | 高度 | +| `roundness` | float | 0 | 圆角半径 | +| `reversed` | bool | false | 反转路径方向 | + +**计算规则**: +``` +rect.left = centerX - width / 2 +rect.top = centerY - height / 2 +rect.right = centerX + width / 2 +rect.bottom = centerY + height / 2 +``` + +**圆角处理**: +- `roundness` 值自动限制为 `min(roundness, width/2, height/2)` +- 当 `roundness >= min(width, height) / 2` 时,短边方向呈半圆形 + +**路径起点**:矩形路径从**右上角**开始,顺时针方向绘制(`reversed="false"` 时)。 + +#### 5.2.2 Ellipse(椭圆) + +椭圆从中心点定义。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `centerX` | float | 0 | 中心 X | +| `centerY` | float | 0 | 中心 Y | +| `width` | float | 0 | 宽度 | +| `height` | float | 0 | 高度 | +| `reversed` | bool | false | 反转路径方向 | + +**计算规则**: +``` +boundingRect.left = centerX - width / 2 +boundingRect.top = centerY - height / 2 +boundingRect.right = centerX + width / 2 +boundingRect.bottom = centerY + height / 2 +``` + +**路径起点**:椭圆路径从**右侧中点**(3 点钟方向)开始。 + +#### 5.2.3 Polystar(多边形/星形) + +支持正多边形和星形两种模式。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `centerX` | float | 0 | 中心 X | +| `centerY` | float | 0 | 中心 Y | +| `type` | PolystarType | star | 类型(见下方) | +| `points` | float | 5 | 顶点数(支持小数) | +| `outerRadius` | float | 100 | 外半径 | +| `innerRadius` | float | 50 | 内半径(仅星形) | +| `rotation` | float | 0 | 旋转角度 | +| `outerRoundness` | float | 0 | 外角圆度 | +| `innerRoundness` | float | 0 | 内角圆度 | +| `reversed` | bool | false | 反转路径方向 | + +**PolystarType(类型)**: + +| 值 | 说明 | +|------|------| +| `polygon` | 正多边形:只使用外半径 | +| `star` | 星形:使用外半径和内半径交替 | + +**多边形模式** (`type="polygon"`): +- 只使用 `outerRadius` 和 `outerRoundness` +- `innerRadius` 和 `innerRoundness` 被忽略 + +**星形模式** (`type="star"`): +- 外顶点位于 `outerRadius` 处 +- 内顶点位于 `innerRadius` 处 +- 顶点交替连接形成星形 + +**顶点计算**(第 i 个外顶点): +``` +angle = rotation + (i / points) * 360° +x = centerX + outerRadius * cos(angle) +y = centerY + outerRadius * sin(angle) +``` + +**小数点数**: +- `points` 支持小数值(如 `5.5`) +- 小数部分表示最后一个顶点的"完成度",产生不完整的最后一个角 +- `points <= 0` 时不生成任何路径 + +**圆度处理**: +- `outerRoundness` 和 `innerRoundness` 取值范围 0~1 +- 0 表示尖角,1 表示完全圆滑 +- 圆度通过在顶点处添加贝塞尔控制点实现 + +#### 5.2.4 Path(路径) + +使用 SVG 路径语法定义任意形状。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `d` | string | "" | SVG 路径数据(语法见 2.7 节) | +| `reversed` | bool | false | 反转路径方向 | + +#### 5.2.5 TextSpan(文本片段) + +文本片段提供文本内容的几何形状。一个 TextSpan 经过塑形后会产生**字形列表**(多个字形),而非单一 Path。 + +```xml + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `x` | float | 0 | X 位置 | +| `y` | float | 0 | Y 位置 | +| `font` | string | - | 字体族 | +| `fontSize` | float | 12 | 字号 | +| `fontWeight` | int | 400 | 字重(100-900) | +| `fontStyle` | enum | normal | normal 或 italic | +| `tracking` | float | 0 | 字距 | +| `baselineShift` | float | 0 | 基线偏移 | + +**处理流程**: +1. 根据 `font`、`fontSize`、`fontWeight`、`fontStyle` 查找字体 +2. 应用 `tracking`(字距调整) +3. 将文本塑形(shaping)为字形列表 +4. 按 `x`、`y` 位置放置 + +**字体回退**:当指定字体不可用时,按平台默认字体回退链选择替代字体。 + +### 5.3 Painters(绘制器) + +绘制器(Fill、Stroke)对**当前时刻**累积的所有几何(Path 和字形列表)进行渲染。 + +#### 5.3.1 Fill(填充) + +填充使用指定的颜色源绘制几何的内部区域。 + +```xml + + + + + + + + + + + + + + + + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `color` | color/idref | - | 颜色值或颜色源引用 | +| `alpha` | float | 1 | 透明度 0~1 | +| `blendMode` | BlendMode | normal | 混合模式(见 4.1) | +| `fillRule` | FillRule | winding | 填充规则(见下方) | +| `placement` | Placement | background | 绘制位置(见 5.3.3) | + +**FillRule(填充规则)**: + +| 值 | 说明 | +|------|------| +| `winding` | 非零环绕规则:根据路径方向计数,非零则填充 | +| `evenOdd` | 奇偶规则:根据交叉次数,奇数则填充 | + +**文本填充**: +- 文本以字形(glyph)为单位填充 +- 支持通过 TextModifier 对单个字形应用颜色覆盖 +- 颜色覆盖采用 alpha 混合:`finalColor = lerp(originalColor, overrideColor, overrideAlpha)` + +#### 5.3.2 Stroke(描边) + +描边沿几何边界绘制线条。 + +```xml + + + + + + + + + + + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `color` | color/idref | - | 颜色值或颜色源引用 | +| `width` | float | 1 | 描边宽度 | +| `alpha` | float | 1 | 透明度 0~1 | +| `blendMode` | BlendMode | normal | 混合模式(见 4.1) | +| `cap` | LineCap | butt | 线帽样式(见下方) | +| `join` | LineJoin | miter | 线连接样式(见下方) | +| `miterLimit` | float | 4 | 斜接限制 | +| `dashes` | string | - | 虚线模式 "d1,d2,..." | +| `dashOffset` | float | 0 | 虚线偏移 | +| `align` | StrokeAlign | center | 描边对齐(见下方) | +| `placement` | Placement | background | 绘制位置(见 5.3.3) | + +**LineCap(线帽样式)**: + +| 值 | 说明 | +|------|------| +| `butt` | 平头:线条不超出端点 | +| `round` | 圆头:以半圆形扩展端点 | +| `square` | 方头:以方形扩展端点 | + +**LineJoin(线连接样式)**: + +| 值 | 说明 | +|------|------| +| `miter` | 斜接:延伸外边缘形成尖角 | +| `round` | 圆角:以圆弧连接 | +| `bevel` | 斜角:以三角形填充连接处 | + +**StrokeAlign(描边对齐)**: + +| 值 | 说明 | +|------|------| +| `center` | 描边居中于路径(默认) | +| `inside` | 描边在闭合路径内侧 | +| `outside` | 描边在闭合路径外侧 | + +内侧/外侧描边通过以下方式实现: +1. 以双倍宽度描边 +2. 与原始形状进行布尔运算(内侧用交集,外侧用差集) + +**虚线模式**: +- `dashes`:定义虚线段长度序列,如 `"5,3"` 表示 5px 实线 + 3px 空白 +- `dashOffset`:虚线起始偏移量 + +#### 5.3.3 Placement(绘制位置) + +Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: + +| 值 | 说明 | +|------|------| +| `background` | 在子图层**下方**绘制(默认) | +| `foreground` | 在子图层**上方**绘制 | + +### 5.4 Shape Modifiers(形状修改器) + +形状修改器对累积的 Path 进行**原地变换**,对字形列表则触发强制转换为 Path。 + +#### 5.4.1 TrimPath(路径裁剪) + +裁剪路径到指定的起止范围。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `start` | float | 0 | 起始位置 0~1 | +| `end` | float | 1 | 结束位置 0~1 | +| `offset` | float | 0 | 偏移量(度) | +| `type` | TrimType | separate | 裁剪类型(见下方) | + +**TrimType(裁剪类型)**: + +| 值 | 说明 | +|------|------| +| `separate` | 独立模式:每个形状独立裁剪,使用相同的 start/end 参数 | +| `continuous` | 连续模式:所有形状视为一条连续路径,按总长度比例裁剪 | + +**边界情况**: +- `start > end`:反向裁剪,路径方向反转 +- 支持环绕:当裁剪范围超出 [0,1] 时,自动环绕到路径另一端 +- 路径总长度为 0 时,不执行任何操作 + +**连续模式示例**: +```xml + + + +``` + +#### 5.4.2 RoundCorner(圆角) + +将路径的尖角转换为圆角。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `radius` | float | 0 | 圆角半径 | + +**处理规则**: +- 只影响尖角(非平滑连接的顶点) +- 圆角半径自动限制为不超过相邻边长度的一半 +- `radius <= 0` 时不执行任何操作 + +#### 5.4.3 MergePath(路径合并) + +将所有形状合并为单个形状。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `op` | PathOp | append | 合并操作(见下方) | + +**PathOp(路径合并操作)**: + +| 值 | 说明 | +|------|------| +| `append` | 追加:简单合并所有路径,不进行布尔运算(默认) | +| `union` | 并集:合并所有形状的覆盖区域 | +| `intersect` | 交集:只保留所有形状的重叠区域 | +| `xor` | 异或:保留非重叠区域 | +| `difference` | 差集:从第一个形状中减去后续形状 | + +**重要行为**: +- MergePath 会**清空之前渲染的所有样式** +- 合并时应用各形状的当前变换矩阵 +- 合并后的形状变换矩阵重置为单位矩阵 + +**示例**: +```xml + + + + + +``` + +### 5.5 Text Modifiers(文本修改器) + +文本修改器对文本中的独立字形进行变换。 + +#### 5.5.1 文本修改器处理 + +遇到文本修改器时,上下文中累积的**所有字形列表**会汇总为一个统一的字形列表进行操作: + +```xml + + + + + + +``` + +#### 5.5.2 文本转形状 + +当文本遇到形状修改器时,会强制转换为形状路径: + +``` +文本元素 形状修改器 后续修改器 +┌──────────┐ ┌──────────┐ +│ TextSpan │ │ TrimPath │ +└────┬─────┘ │RoundCorn │ + │ │MergePath │ + │ 累积字形列表 └────┬─────┘ + ▼ │ +┌──────────────┐ │ 触发转换 +│ 字形列表 │───────────┼──────────────────────┐ +│ [H,e,l,l,o] │ │ │ +└──────────────┘ ▼ ▼ + ┌──────────────┐ ┌──────────────────┐ + │ 合并为单个 │ │ Emoji 被丢弃 │ + │ Path │ │ (无法转为路径) │ + └──────────────┘ └──────────────────┘ + │ + │ 后续文本修改器不再生效 + ▼ + ┌──────────────┐ + │ TextModifier │ → 跳过(已是 Path) + └──────────────┘ +``` + +**转换规则**: + +1. **触发条件**:文本遇到 TrimPath、RoundCorner、MergePath 时触发转换 +2. **合并为单个 Path**:一个 TextSpan 的所有字形合并为**一个** Path,而非每个字形产生一个独立 Path +3. **Emoji 丢失**:Emoji 无法转换为路径轮廓,转换时被丢弃 +4. **不可逆转换**:转换后成为纯 Path,后续的文本修改器对其无效 + +**示例**: +```xml + + + + + + +``` + +#### 5.5.3 TextModifier(文本变换器) + +对选定范围内的字形应用变换和样式覆盖。 + +```xml + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `anchor` | point | 0.5,0.5 | 锚点(归一化) | +| `position` | point | 0,0 | 位置偏移 | +| `rotation` | float | 0 | 旋转 | +| `scale` | point | 1,1 | 缩放 | +| `skew` | float | 0 | 倾斜 | +| `skewAxis` | float | 0 | 倾斜轴 | +| `alpha` | float | 1 | 透明度 | +| `fillColor` | color | - | 填充颜色覆盖 | +| `strokeColor` | color | - | 描边颜色覆盖 | +| `strokeWidth` | float | - | 描边宽度覆盖 | + +**选择器计算**: +1. 根据 RangeSelector 的 `start`、`end`、`offset` 计算选择范围(支持任意小数值,超出 [0,1] 范围时自动环绕) +2. 根据 `shape` 计算每个字形的影响因子(0~1) +3. 多个选择器按 `mode` 组合 + +**变换应用**: + +选择器计算出的 `factor` 范围为 [-1, 1],控制变换属性的应用程度: + +``` +factor = clamp(selectorFactor × weight, -1, 1) + +// 位置和旋转:线性应用 factor +transform = translate(-anchor × factor) + × scale(1 + (scale - 1) × factor) // 缩放从 1 插值到目标值 + × skew(skew × factor, skewAxis) + × rotate(rotation × factor) + × translate(anchor × factor) + × translate(position × factor) + +// 透明度:使用 factor 的绝对值 +alphaFactor = 1 + (alpha - 1) × |factor| +finalAlpha = originalAlpha × max(0, alphaFactor) +``` + +**颜色覆盖**: + +颜色覆盖使用 `factor` 的绝对值进行 alpha 混合: + +``` +blendFactor = overrideColor.alpha × |factor| +finalColor = blend(originalColor, overrideColor, blendFactor) +``` + +#### 5.5.4 RangeSelector(范围选择器) + +范围选择器定义 TextModifier 影响的字形范围和影响程度。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `start` | float | 0 | 选择起始 | +| `end` | float | 1 | 选择结束 | +| `offset` | float | 0 | 选择偏移 | +| `unit` | SelectorUnit | percentage | 单位(见下方) | +| `shape` | SelectorShape | square | 形状(见下方) | +| `easeIn` | float | 0 | 缓入量 | +| `easeOut` | float | 0 | 缓出量 | +| `mode` | SelectorMode | add | 组合模式(见下方) | +| `weight` | float | 1 | 选择器权重 | +| `randomize` | bool | false | 随机顺序 | +| `seed` | int | 0 | 随机种子 | + +**SelectorUnit(单位)**: + +| 值 | 说明 | +|------|------| +| `index` | 索引:按字形索引计算范围 | +| `percentage` | 百分比:按字形总数的百分比计算范围 | + +**SelectorShape(形状)**: + +| 值 | 说明 | +|------|------| +| `square` | 矩形:范围内为 1,范围外为 0 | +| `rampUp` | 上升斜坡:从 0 线性增加到 1 | +| `rampDown` | 下降斜坡:从 1 线性减少到 0 | +| `triangle` | 三角形:中心为 1,两端为 0 | +| `round` | 圆形:正弦曲线过渡 | +| `smooth` | 平滑:更平滑的过渡曲线 | + +**SelectorMode(组合模式)**: + +| 值 | 说明 | +|------|------| +| `add` | 相加:累加选择器权重 | +| `subtract` | 相减:减去选择器权重 | +| `intersect` | 交集:取最小权重 | +| `min` | 最小:取最小值 | +| `max` | 最大:取最大值 | +| `difference` | 差值:取绝对差值 | + +#### 5.5.5 TextPath(文本路径) + +将文本沿指定路径排列。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `path` | idref | - | 路径引用 "#id" | +| `align` | TextPathAlign | start | 对齐模式(见下方) | +| `firstMargin` | float | 0 | 起始边距 | +| `lastMargin` | float | 0 | 结束边距 | +| `perpendicularToPath` | bool | true | 垂直于路径 | +| `reversed` | bool | false | 反转方向 | +| `forceAlignment` | bool | false | 强制对齐 | + +**TextPathAlign(对齐模式)**: + +| 值 | 说明 | +|------|------| +| `start` | 从路径起点开始排列 | +| `center` | 文本居中于路径 | +| `end` | 文本结束于路径终点 | + +**边距**: +- `firstMargin`:起点边距(从路径起点向内偏移) +- `lastMargin`:终点边距(从路径终点向内偏移) + +**字形定位**: +1. 计算字形中心在路径上的位置 +2. 获取该位置的路径切线方向 +3. 如果 `perpendicularToPath="true"`,旋转字形使其垂直于路径 + +**闭合路径**:对于闭合路径,超出范围的字形会环绕到路径另一端。 + +**强制对齐**:`forceAlignment="true"` 时,自动调整字间距以填满可用路径长度(减去边距)。 + +#### 5.5.6 TextLayout(文本排版) + +文本排版修改器对累积的文本元素应用段落排版,是 PAGX 格式特有的元素。与 TextPath 类似,TextLayout 作用于累积的字形列表,为其应用自动换行和对齐。 + +渲染时会由附加的文字排版模块预先排版,重新计算每个字形的位置。转换为 PAG 二进制格式时,TextLayout 会被预排版展开,字形位置直接写入 TextSpan。 + +```xml + + 第一段内容... + 粗体 + 普通文本。 + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `width` | float | - | 文本框宽度 | +| `height` | float | - | 文本框高度 | +| `align` | TextAlign | left | 水平对齐(见下方) | +| `verticalAlign` | VerticalAlign | top | 垂直对齐(见下方) | +| `lineHeight` | float | 1.2 | 行高倍数 | +| `indent` | float | 0 | 首行缩进 | +| `overflow` | Overflow | clip | 溢出处理(见下方) | + +**TextAlign(水平对齐)**: + +| 值 | 说明 | +|------|------| +| `left` | 左对齐 | +| `center` | 居中对齐 | +| `right` | 右对齐 | +| `justify` | 两端对齐 | + +**VerticalAlign(垂直对齐)**: + +| 值 | 说明 | +|------|------| +| `top` | 顶部对齐 | +| `center` | 垂直居中 | +| `bottom` | 底部对齐 | + +**Overflow(溢出处理)**: + +| 值 | 说明 | +|------|------| +| `clip` | 裁剪:超出部分不显示 | +| `visible` | 可见:超出部分仍然显示 | +| `ellipsis` | 省略:超出部分显示省略号 | + +#### 5.5.7 富文本 + +富文本通过 Group 内的多个 TextSpan 元素组合,共享 Fill/Stroke 样式。 + +```xml + + Hello + World + + +``` + +### 5.6 Repeater(复制器) + +复制累积的内容和已渲染的样式,对每个副本应用渐进变换。Repeater 对 Path 和字形列表同时生效,且不会触发文本转形状。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `copies` | float | 3 | 副本数 | +| `offset` | float | 0 | 起始偏移 | +| `order` | RepeaterOrder | belowOriginal | 堆叠顺序(见下方) | +| `anchor` | point | 0,0 | 锚点 | +| `position` | point | 100,100 | 每个副本的位置偏移 | +| `rotation` | float | 0 | 每个副本的旋转 | +| `scale` | point | 1,1 | 每个副本的缩放 | +| `startAlpha` | float | 1 | 首个副本透明度 | +| `endAlpha` | float | 1 | 末个副本透明度 | + +**变换计算**(第 i 个副本,i 从 0 开始): +``` +progress = i + offset +matrix = translate(-anchor) + × scale(scale^progress) // 指数缩放 + × rotate(rotation × progress) // 线性旋转 + × translate(position × progress) // 线性位移 + × translate(anchor) +``` + +**透明度插值**: +``` +t = progress / copies +alpha = lerp(startAlpha, endAlpha, t) +``` + +**RepeaterOrder(堆叠顺序)**: + +| 值 | 说明 | +|------|------| +| `belowOriginal` | 副本在原件下方。索引 0 在最上 | +| `aboveOriginal` | 副本在原件上方。索引 N-1 在最上 | + +**小数副本数**: + +当 `copies` 为小数时(如 `3.5`),采用**叠加半透明**的方式实现部分副本效果: + +1. **几何复制**:形状和文本按 `ceil(copies)` 个复制(即 4 个),几何本身不做缩放或裁剪 +2. **透明度调整**:最后一个副本的透明度乘以小数部分(如 0.5),产生半透明效果 +3. **视觉效果**:通过透明度渐变模拟"部分存在"的副本 + +**示例**:`copies="2.3"` 时 +- 复制 3 个完整的几何副本 +- 第 1、2 个副本正常渲染 +- 第 3 个副本透明度 × 0.3,呈现半透明效果 + +**边界情况**: +- `copies < 0`:不执行任何操作 +- `copies = 0`:清空所有累积的内容和已渲染的样式 + +**Repeater 特性**: +- **同时作用**:复制所有累积的 Path 和字形列表 +- **保留文本属性**:字形列表复制后仍保留字形信息,后续文本修改器仍可作用 +- **复制已渲染样式**:同时复制已渲染的填充和描边 + +```xml + + + + + + +``` + +### 5.7 Group(容器) + +Group 是带变换属性的矢量元素容器。 + +```xml + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `name` | string | "" | 组名称 | +| `anchor` | point | 0,0 | 锚点 "x,y" | +| `position` | point | 0,0 | 位置 "x,y" | +| `rotation` | float | 0 | 旋转角度 | +| `scale` | point | 1,1 | 缩放 "sx,sy" | +| `skew` | float | 0 | 倾斜量 | +| `skewAxis` | float | 0 | 倾斜轴角度 | +| `alpha` | float | 1 | 透明度 0~1 | + +#### 变换顺序 + +变换按以下顺序应用(后应用的变换先计算): + +1. 平移到锚点的负方向(`translate(-anchor)`) +2. 缩放(`scale`) +3. 倾斜(`skew` 沿 `skewAxis` 方向) +4. 旋转(`rotation`) +5. 平移到位置(`translate(position)`) + +**变换矩阵**: +``` +M = translate(position) × rotate(rotation) × skew(skew, skewAxis) × scale(scale) × translate(-anchor) +``` + +**倾斜变换**: +``` +skewMatrix = rotate(skewAxis) × shearX(tan(skew)) × rotate(-skewAxis) +``` + +#### 作用域隔离 + +Group 创建独立的作用域,用于隔离几何累积和渲染: + +- 组内的几何元素只在组内累积 +- 组内的绘制器只渲染组内累积的几何 +- 组内的修改器只影响组内累积的几何 +- 组的变换矩阵应用到组内所有内容 +- 组的 `alpha` 属性应用到组内所有渲染内容 + +**几何累积规则**: + +- **绘制器不清空几何**:Fill 和 Stroke 渲染后,几何列表保持不变,后续绘制器仍可渲染相同的几何 +- **子 Group 几何向上累积**:子 Group 处理完成后,其几何会累积到父作用域,父级末尾的绘制器可以渲染所有子 Group 的几何 +- **同级 Group 互不影响**:每个 Group 创建独立的累积起点,不会看到后续兄弟 Group 的几何 +- **隔离渲染范围**:Group 内的绘制器只能渲染到当前位置已累积的几何,包括本组和已完成的子 Group + +**示例 1 - 基本隔离**: +```xml + + + + + + +``` + +**示例 2 - 子 Group 几何向上累积**: +```xml + + + + + + + + + + + +``` + +**示例 3 - 多个绘制器复用几何**: +```xml + + + +``` + +#### 多重填充与描边 + +由于绘制器不清空几何列表,同一几何可连续应用多个 Fill 和 Stroke。 + +**示例 4 - 多重填充**: +```xml + + + + + +``` + +**示例 5 - 多重描边**: +```xml + + + + +``` + +**示例 6 - 混合叠加**: +```xml + + + + + + + + + + + +``` + +**渲染顺序**:多个绘制器按文档顺序渲染,先出现的位于下方。 + +--- + +## 6. Conformance(一致性) + +### 6.1 解析规则 + +- 未知元素跳过不报错(向前兼容) +- 未知属性忽略 +- 缺失的可选属性使用默认值 +- 无效值尽可能回退到默认值 + +### 6.2 转换为 PAG + +将 PAGX 转换为 PAG 二进制格式时: + +1. 内嵌外部资源 +2. 预排版文本(转换为字形坐标) +3. 静态图层可合并优化 +4. 合成内联展开 +5. 排版相关文本属性预计算为位置偏移 + +--- + +## Appendix A. Complete Example(完整示例) + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` diff --git a/pagx/include/pagx/layers/LayerBuilder.h b/pagx/include/pagx/layers/LayerBuilder.h new file mode 100644 index 0000000000..5becaaa9fa --- /dev/null +++ b/pagx/include/pagx/layers/LayerBuilder.h @@ -0,0 +1,72 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "tgfx/core/Stream.h" +#include "tgfx/layers/Layer.h" + +namespace pagx { + +/** + * PAGXContent represents the result of parsing a PAGX file, containing the root layer + * and canvas dimensions. + */ +struct PAGXContent { + /** + * The root layer of the PAGX content. nullptr if parsing failed. + */ + std::shared_ptr root = nullptr; + /** + * The canvas width specified in the PAGX file. + */ + float width = 0.0f; + /** + * The canvas height specified in the PAGX file. + */ + float height = 0.0f; +}; + +/** + * LayerBuilder provides functionality to build tgfx vector layer trees from PAGX files. + * PAGX (Portable Animated Graphics XML) is an XML-based markup language for describing + * animated vector graphics. + */ +class LayerBuilder { + public: + /** + * Builds a Layer tree from a PAGX file. + * @param filePath The path to the PAGX file. The file's directory is used as the base path + * for resolving relative resource paths (e.g., images). + * @return PAGXContent containing the root layer and canvas dimensions. + */ + static PAGXContent FromFile(const std::string& filePath); + + /** + * Builds a Layer tree from a stream containing PAGX XML data. + * @param stream The stream containing PAGX XML data. + * @param basePath The base directory path for resolving relative resource paths. + * If empty, relative paths cannot be resolved. + * @return PAGXContent containing the root layer and canvas dimensions. + */ + static PAGXContent FromStream(tgfx::Stream& stream, const std::string& basePath = ""); +}; + +} // namespace pagx diff --git a/pagx/include/pagx/layers/TextLayouter.h b/pagx/include/pagx/layers/TextLayouter.h new file mode 100644 index 0000000000..80b9630a70 --- /dev/null +++ b/pagx/include/pagx/layers/TextLayouter.h @@ -0,0 +1,53 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "tgfx/core/Font.h" +#include "tgfx/core/TextBlob.h" +#include "tgfx/core/Typeface.h" + +namespace pagx { + +/** + * TextLayouter provides simple text layout functionality for PAGX text rendering. + * It handles font fallback and basic horizontal text layout. + */ +class TextLayouter { + public: + /** + * Sets the fallback typefaces for PAGX text rendering. + * When a character cannot be rendered by the primary typeface, the fallback typefaces + * are tried in order until one that supports the character is found. + */ + static void SetFallbackTypefaces(std::vector> typefaces); + + /** + * Creates a TextBlob from the given text with basic horizontal layout. + * Supports font fallback for characters not available in the primary typeface. + */ + static std::shared_ptr Layout(const std::string& text, const tgfx::Font& font); + + private: + static std::vector> GetFallbackTypefaces(); +}; + +} // namespace pagx diff --git a/pagx/include/pagx/svg/SVGImporter.h b/pagx/include/pagx/svg/SVGImporter.h new file mode 100644 index 0000000000..3537636c3b --- /dev/null +++ b/pagx/include/pagx/svg/SVGImporter.h @@ -0,0 +1,63 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "tgfx/core/Stream.h" +#include "tgfx/svg/SVGDOM.h" + +namespace pagx { + +/** + * SVGImporter provides functionality to import SVG files and convert them to PAGX format. + */ +class SVGImporter { + public: + /** + * Imports an SVG file and converts it to PAGX format. + * @param svgFilePath The path to the SVG file to import. + * @return The PAGX content as a string, or an empty string if import fails. + */ + static std::string ImportFromFile(const std::string& svgFilePath); + + /** + * Imports SVG content from a stream and converts it to PAGX format. + * @param svgStream The stream containing SVG content. + * @return The PAGX content as a string, or an empty string if import fails. + */ + static std::string ImportFromStream(tgfx::Stream& svgStream); + + /** + * Imports an SVGDOM object and converts it to PAGX format. + * @param svgDOM The SVGDOM object to import. + * @return The PAGX content as a string, or an empty string if import fails. + */ + static std::string ImportFromDOM(const std::shared_ptr& svgDOM); + + /** + * Saves PAGX content to a file. + * @param pagxContent The PAGX content to save. + * @param outputPath The path to save the PAGX file. + * @return True if the file was saved successfully, false otherwise. + */ + static bool SaveToFile(const std::string& pagxContent, const std::string& outputPath); +}; + +} // namespace pagx diff --git a/pagx/src/layers/LayerBuilder.cpp b/pagx/src/layers/LayerBuilder.cpp new file mode 100644 index 0000000000..8eb7a03e6c --- /dev/null +++ b/pagx/src/layers/LayerBuilder.cpp @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/layers/LayerBuilder.h" +#include "PAGXParser.h" +#include "tgfx/core/Data.h" +#include "tgfx/svg/xml/XMLDOM.h" + +namespace pagx { + +PAGXContent LayerBuilder::FromFile(const std::string& filePath) { + auto data = tgfx::Data::MakeFromFile(filePath); + if (!data) { + return {}; + } + auto stream = tgfx::Stream::MakeFromData(data); + if (!stream) { + return {}; + } + + std::string basePath; + auto lastSlash = filePath.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + basePath = filePath.substr(0, lastSlash); + } + + return FromStream(*stream, basePath); +} + +PAGXContent LayerBuilder::FromStream(tgfx::Stream& stream, const std::string& basePath) { + auto dom = tgfx::DOM::Make(stream); + if (!dom) { + return {}; + } + auto rootNode = dom->getRootNode(); + if (!rootNode || rootNode->name != "pagx") { + return {}; + } + + PAGXParser parser(rootNode, basePath); + if (!parser.parse()) { + return {}; + } + return {parser.getRoot(), parser.width(), parser.height()}; +} + +} // namespace pagx diff --git a/pagx/src/layers/PAGXAttributes.cpp b/pagx/src/layers/PAGXAttributes.cpp new file mode 100644 index 0000000000..900b2e74ae --- /dev/null +++ b/pagx/src/layers/PAGXAttributes.cpp @@ -0,0 +1,336 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PAGXAttributes.h" +#include +#include +#include +#include + +namespace pagx { + +float PAGXAttributes::ParseFloat(const std::shared_ptr& node, const std::string& name, + float defaultValue) { + auto [found, value] = node->findAttribute(name); + if (!found || value.empty()) { + return defaultValue; + } + return std::strtof(value.c_str(), nullptr); +} + +bool PAGXAttributes::ParseBool(const std::shared_ptr& node, const std::string& name, + bool defaultValue) { + auto [found, value] = node->findAttribute(name); + if (!found || value.empty()) { + return defaultValue; + } + if (value == "true" || value == "1") { + return true; + } + if (value == "false" || value == "0") { + return false; + } + return defaultValue; +} + +std::string PAGXAttributes::ParseString(const std::shared_ptr& node, + const std::string& name, const std::string& defaultValue) { + auto [found, value] = node->findAttribute(name); + if (!found) { + return defaultValue; + } + return value; +} + +static int HexCharToInt(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return 0; +} + +static Color ParseHexColor(const std::string& hex) { + size_t start = (hex[0] == '#') ? 1 : 0; + std::string h = hex.substr(start); + float r = 0.0f; + float g = 0.0f; + float b = 0.0f; + float a = 1.0f; + + if (h.length() == 3) { + // #RGB -> #RRGGBB + r = static_cast(HexCharToInt(h[0]) * 17) / 255.0f; + g = static_cast(HexCharToInt(h[1]) * 17) / 255.0f; + b = static_cast(HexCharToInt(h[2]) * 17) / 255.0f; + } else if (h.length() == 6) { + // #RRGGBB + r = static_cast(HexCharToInt(h[0]) * 16 + HexCharToInt(h[1])) / 255.0f; + g = static_cast(HexCharToInt(h[2]) * 16 + HexCharToInt(h[3])) / 255.0f; + b = static_cast(HexCharToInt(h[4]) * 16 + HexCharToInt(h[5])) / 255.0f; + } else if (h.length() == 8) { + // #RRGGBBAA + r = static_cast(HexCharToInt(h[0]) * 16 + HexCharToInt(h[1])) / 255.0f; + g = static_cast(HexCharToInt(h[2]) * 16 + HexCharToInt(h[3])) / 255.0f; + b = static_cast(HexCharToInt(h[4]) * 16 + HexCharToInt(h[5])) / 255.0f; + a = static_cast(HexCharToInt(h[6]) * 16 + HexCharToInt(h[7])) / 255.0f; + } + return {r, g, b, a}; +} + +static Color ParseRGBColor(const std::string& value) { + // rgb(255,0,0) or rgba(255,0,0,0.5) + bool hasAlpha = value.find("rgba") != std::string::npos; + size_t start = hasAlpha ? 5 : 4; + size_t end = value.find(')'); + std::string content = value.substr(start, end - start); + + std::vector values; + std::istringstream stream(content); + std::string token; + while (std::getline(stream, token, ',')) { + values.push_back(std::strtof(token.c_str(), nullptr)); + } + + float r = values.size() > 0 ? values[0] / 255.0f : 0.0f; + float g = values.size() > 1 ? values[1] / 255.0f : 0.0f; + float b = values.size() > 2 ? values[2] / 255.0f : 0.0f; + float a = values.size() > 3 ? values[3] : 1.0f; + return {r, g, b, a}; +} + +Color PAGXAttributes::ParseColor(const std::string& value) { + if (value.empty()) { + return Color::Black(); + } + if (value[0] == '#') { + return ParseHexColor(value); + } + if (value.find("rgb") == 0) { + return ParseRGBColor(value); + } + // TODO: Add HSL and color() support per spec + // Default fallback + return Color::Black(); +} + +Point PAGXAttributes::ParsePoint(const std::string& value, Point defaultValue) { + if (value.empty()) { + return defaultValue; + } + size_t commaPos = value.find(','); + if (commaPos == std::string::npos) { + return defaultValue; + } + float x = std::strtof(value.substr(0, commaPos).c_str(), nullptr); + float y = std::strtof(value.substr(commaPos + 1).c_str(), nullptr); + return {x, y}; +} + +Matrix PAGXAttributes::ParseMatrix(const std::string& value) { + if (value.empty()) { + return Matrix::I(); + } + std::vector values; + std::istringstream stream(value); + std::string token; + while (std::getline(stream, token, ',')) { + values.push_back(std::strtof(token.c_str(), nullptr)); + } + if (values.size() >= 6) { + // a,b,c,d,tx,ty + return Matrix::MakeAll(values[0], values[2], values[4], values[1], values[3], values[5]); + } + return Matrix::I(); +} + +BlendMode PAGXAttributes::ParseBlendMode(const std::string& value) { + if (value == "multiply") { + return BlendMode::Multiply; + } + if (value == "screen") { + return BlendMode::Screen; + } + if (value == "overlay") { + return BlendMode::Overlay; + } + if (value == "darken") { + return BlendMode::Darken; + } + if (value == "lighten") { + return BlendMode::Lighten; + } + if (value == "colorDodge") { + return BlendMode::ColorDodge; + } + if (value == "colorBurn") { + return BlendMode::ColorBurn; + } + if (value == "hardLight") { + return BlendMode::HardLight; + } + if (value == "softLight") { + return BlendMode::SoftLight; + } + if (value == "difference") { + return BlendMode::Difference; + } + if (value == "exclusion") { + return BlendMode::Exclusion; + } + if (value == "hue") { + return BlendMode::Hue; + } + if (value == "saturation") { + return BlendMode::Saturation; + } + if (value == "color") { + return BlendMode::Color; + } + if (value == "luminosity") { + return BlendMode::Luminosity; + } + if (value == "add") { + return BlendMode::PlusLighter; + } + return BlendMode::SrcOver; +} + +PathFillType PAGXAttributes::ParseFillRule(const std::string& value) { + if (value == "evenOdd") { + return PathFillType::EvenOdd; + } + return PathFillType::Winding; +} + +LineCap PAGXAttributes::ParseLineCap(const std::string& value) { + if (value == "round") { + return LineCap::Round; + } + if (value == "square") { + return LineCap::Square; + } + return LineCap::Butt; +} + +LineJoin PAGXAttributes::ParseLineJoin(const std::string& value) { + if (value == "round") { + return LineJoin::Round; + } + if (value == "bevel") { + return LineJoin::Bevel; + } + return LineJoin::Miter; +} + +TileMode PAGXAttributes::ParseTileMode(const std::string& value) { + if (value == "repeat") { + return TileMode::Repeat; + } + if (value == "mirror") { + return TileMode::Mirror; + } + if (value == "decal") { + return TileMode::Decal; + } + return TileMode::Clamp; +} + +LayerMaskType PAGXAttributes::ParseMaskType(const std::string& value) { + if (value == "luminance") { + return LayerMaskType::Luminance; + } + if (value == "contour") { + return LayerMaskType::Contour; + } + return LayerMaskType::Alpha; +} + +PolystarType PAGXAttributes::ParsePolystarType(const std::string& value) { + if (value == "polygon") { + return PolystarType::Polygon; + } + return PolystarType::Star; +} + +TrimPathType PAGXAttributes::ParseTrimPathType(const std::string& value) { + if (value == "continuous") { + return TrimPathType::Continuous; + } + return TrimPathType::Separate; +} + +MergePathOp PAGXAttributes::ParseMergePathOp(const std::string& value) { + if (value == "union") { + return MergePathOp::Union; + } + if (value == "intersect") { + return MergePathOp::Intersect; + } + if (value == "xor") { + return MergePathOp::XOR; + } + if (value == "difference") { + return MergePathOp::Difference; + } + return MergePathOp::Append; +} + +StrokeAlign PAGXAttributes::ParseStrokeAlign(const std::string& value) { + if (value == "inside") { + return StrokeAlign::Inside; + } + if (value == "outside") { + return StrokeAlign::Outside; + } + return StrokeAlign::Center; +} + +std::vector PAGXAttributes::ParseDashes(const std::string& value) { + if (value.empty()) { + return {}; + } + std::vector dashes; + std::istringstream stream(value); + std::string token; + while (std::getline(stream, token, ',')) { + dashes.push_back(std::strtof(token.c_str(), nullptr)); + } + return dashes; +} + +std::string PAGXAttributes::GetTextContent(const std::shared_ptr& node) { + if (!node) { + return ""; + } + auto child = node->firstChild; + while (child) { + if (child->type == DOMNodeType::Text) { + return child->name; + } + child = child->nextSibling; + } + return ""; +} + +} // namespace pagx diff --git a/pagx/src/layers/PAGXAttributes.h b/pagx/src/layers/PAGXAttributes.h new file mode 100644 index 0000000000..383fcf1637 --- /dev/null +++ b/pagx/src/layers/PAGXAttributes.h @@ -0,0 +1,84 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "tgfx/core/BlendMode.h" +#include "tgfx/core/Color.h" +#include "tgfx/core/Matrix.h" +#include "tgfx/core/Path.h" +#include "tgfx/core/Point.h" +#include "tgfx/core/Stroke.h" +#include "tgfx/core/TileMode.h" +#include "tgfx/layers/LayerMaskType.h" +#include "tgfx/layers/StrokeAlign.h" +#include "tgfx/layers/vectors/FillStyle.h" +#include "tgfx/layers/vectors/MergePath.h" +#include "tgfx/layers/vectors/Polystar.h" +#include "tgfx/layers/vectors/TrimPath.h" +#include "tgfx/svg/xml/XMLDOM.h" + +namespace pagx { + +using namespace tgfx; + +class PAGXAttributes { + public: + static float ParseFloat(const std::shared_ptr& node, const std::string& name, + float defaultValue = 0.0f); + + static bool ParseBool(const std::shared_ptr& node, const std::string& name, + bool defaultValue = true); + + static std::string ParseString(const std::shared_ptr& node, const std::string& name, + const std::string& defaultValue = ""); + + static Color ParseColor(const std::string& value); + + static Point ParsePoint(const std::string& value, Point defaultValue = Point::Zero()); + + static Matrix ParseMatrix(const std::string& value); + + static BlendMode ParseBlendMode(const std::string& value); + + static PathFillType ParseFillRule(const std::string& value); + + static LineCap ParseLineCap(const std::string& value); + + static LineJoin ParseLineJoin(const std::string& value); + + static TileMode ParseTileMode(const std::string& value); + + static LayerMaskType ParseMaskType(const std::string& value); + + static PolystarType ParsePolystarType(const std::string& value); + + static TrimPathType ParseTrimPathType(const std::string& value); + + static MergePathOp ParseMergePathOp(const std::string& value); + + static StrokeAlign ParseStrokeAlign(const std::string& value); + + static std::vector ParseDashes(const std::string& value); + + static std::string GetTextContent(const std::shared_ptr& node); +}; + +} // namespace pagx diff --git a/pagx/src/layers/PAGXParser.cpp b/pagx/src/layers/PAGXParser.cpp new file mode 100644 index 0000000000..b3a3738fbc --- /dev/null +++ b/pagx/src/layers/PAGXParser.cpp @@ -0,0 +1,796 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PAGXParser.h" +#include "PAGXAttributes.h" +#include "PAGXUtils.h" +#include "pagx/layers/TextLayouter.h" +#include "tgfx/core/Data.h" +#include "tgfx/core/Font.h" +#include "tgfx/core/Typeface.h" +#include "tgfx/layers/VectorLayer.h" +#include "tgfx/layers/filters/BlurFilter.h" +#include "tgfx/layers/filters/DropShadowFilter.h" +#include "tgfx/layers/layerstyles/DropShadowStyle.h" +#include "tgfx/layers/layerstyles/InnerShadowStyle.h" +#include "tgfx/layers/vectors/Ellipse.h" +#include "tgfx/layers/vectors/FillStyle.h" +#include "tgfx/layers/vectors/Gradient.h" +#include "tgfx/layers/vectors/ImagePattern.h" +#include "tgfx/layers/vectors/MergePath.h" +#include "tgfx/layers/vectors/Polystar.h" +#include "tgfx/layers/vectors/Rectangle.h" +#include "tgfx/layers/vectors/Repeater.h" +#include "tgfx/layers/vectors/RoundCorner.h" +#include "tgfx/layers/vectors/ShapePath.h" +#include "tgfx/layers/vectors/SolidColor.h" +#include "tgfx/layers/vectors/StrokeStyle.h" +#include "tgfx/layers/vectors/TextSpan.h" +#include "tgfx/layers/vectors/TrimPath.h" +#include "tgfx/layers/vectors/VectorGroup.h" +#include "tgfx/svg/SVGPathParser.h" + +namespace pagx { + +PAGXParser::PAGXParser(std::shared_ptr rootNode, const std::string& basePath) + : rootNode_(std::move(rootNode)), basePath_(basePath) { +} + +bool PAGXParser::parse() { + if (!rootNode_ || rootNode_->name != "pagx") { + return false; + } + width_ = PAGXAttributes::ParseFloat(rootNode_, "width", 0.0f); + height_ = PAGXAttributes::ParseFloat(rootNode_, "height", 0.0f); + if (width_ <= 0 || height_ <= 0) { + return false; + } + + rootLayer = Layer::Make(); + rootLayer->setName("root"); + + auto child = rootNode_->firstChild; + while (child) { + if (child->name == "Resources") { + parseResources(child); + } else if (child->name == "Layer") { + auto layer = parseLayer(child); + if (layer) { + rootLayer->addChild(layer); + } + } + child = child->nextSibling; + } + return true; +} + +void PAGXParser::parseResources(const std::shared_ptr& node) { + auto child = node->firstChild; + while (child) { + if (child->name == "Image") { + auto [found, id] = child->findAttribute("id"); + auto [srcFound, source] = child->findAttribute("source"); + if (found && srcFound && !id.empty()) { + auto image = resolveImageReference(source); + if (image) { + images[id] = image; + } + } + } else if (child->name == "SolidColor" || child->name == "LinearGradient" || + child->name == "RadialGradient" || child->name == "ConicGradient" || + child->name == "DiamondGradient" || child->name == "ImagePattern") { + auto [found, id] = child->findAttribute("id"); + if (found && !id.empty()) { + auto colorSource = parseColorSource(child); + if (colorSource) { + colorSources[id] = colorSource; + } + } + } + child = child->nextSibling; + } +} + +std::shared_ptr PAGXParser::parseLayer(const std::shared_ptr& node) { + auto [hasComposition, composition] = node->findAttribute("composition"); + if (hasComposition) { + // Composition reference is not supported in this version + return nullptr; + } + + auto layer = Layer::Make(); + + // Parse layer attributes + auto name = PAGXAttributes::ParseString(node, "name", ""); + layer->setName(name); + + auto visible = PAGXAttributes::ParseBool(node, "visible", true); + layer->setVisible(visible); + + auto alpha = PAGXAttributes::ParseFloat(node, "alpha", 1.0f); + layer->setAlpha(alpha); + + auto blendModeStr = PAGXAttributes::ParseString(node, "blendMode", "normal"); + layer->setBlendMode(PAGXAttributes::ParseBlendMode(blendModeStr)); + + auto x = PAGXAttributes::ParseFloat(node, "x", 0.0f); + auto y = PAGXAttributes::ParseFloat(node, "y", 0.0f); + layer->setPosition({x, y}); + + auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); + if (!matrixStr.empty()) { + layer->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); + } + + auto antiAlias = PAGXAttributes::ParseBool(node, "antiAlias", true); + layer->setAllowsEdgeAntialiasing(antiAlias); + + auto groupOpacity = PAGXAttributes::ParseBool(node, "groupOpacity", false); + layer->setAllowsGroupOpacity(groupOpacity); + + // Parse id for reference + auto [hasId, id] = node->findAttribute("id"); + if (hasId && !id.empty()) { + layerIdMap[id] = layer; + } + + // Parse children nodes + std::vector> contents; + std::vector> filters; + std::vector> styles; + std::string maskRef; + std::string maskTypeStr; + + auto child = node->firstChild; + while (child) { + if (child->name == "contents") { + contents = parseContents(child); + } else if (child->name == "filters") { + filters = parseFilters(child); + } else if (child->name == "styles") { + styles = parseStyles(child); + } else if (child->name == "Layer") { + auto childLayer = parseLayer(child); + if (childLayer) { + layer->addChild(childLayer); + } + } + child = child->nextSibling; + } + + // Apply contents to VectorLayer if present + if (!contents.empty()) { + auto vectorLayer = VectorLayer::Make(); + vectorLayer->setContents(std::move(contents)); + layer->addChildAt(vectorLayer, 0); + } + + // Apply filters and styles + if (!filters.empty()) { + layer->setFilters(filters); + } + if (!styles.empty()) { + layer->setLayerStyles(styles); + } + + // Handle mask reference + auto [hasMask, mask] = node->findAttribute("mask"); + if (hasMask && !mask.empty() && mask[0] == '#') { + maskRef = mask.substr(1); + auto [hasMaskType, maskType] = node->findAttribute("maskType"); + maskTypeStr = hasMaskType ? maskType : "alpha"; + } + + // Mask will be resolved later in a second pass + if (!maskRef.empty()) { + auto it = layerIdMap.find(maskRef); + if (it != layerIdMap.end()) { + layer->setMask(it->second); + layer->setMaskType(PAGXAttributes::ParseMaskType(maskTypeStr)); + } + } + + return layer; +} + +std::vector> PAGXParser::parseContents( + const std::shared_ptr& node) { + std::vector> elements; + auto child = node->firstChild; + while (child) { + auto element = parseVectorElement(child); + if (element) { + elements.push_back(element); + } + child = child->nextSibling; + } + return elements; +} + +std::shared_ptr PAGXParser::parseVectorElement( + const std::shared_ptr& node) { + const auto& name = node->name; + if (name == "Group") { + return parseGroup(node); + } + if (name == "Rectangle") { + return parseRectangle(node); + } + if (name == "Ellipse") { + return parseEllipse(node); + } + if (name == "Polystar") { + return parsePolystar(node); + } + if (name == "Path") { + return parsePath(node); + } + if (name == "TextSpan") { + return parseTextSpan(node); + } + if (name == "Fill") { + return parseFill(node); + } + if (name == "Stroke") { + return parseStroke(node); + } + if (name == "TrimPath") { + return parseTrimPath(node); + } + if (name == "RoundCorner") { + return parseRoundCorner(node); + } + if (name == "MergePath") { + return parseMergePath(node); + } + if (name == "Repeater") { + return parseRepeater(node); + } + return nullptr; +} + +std::shared_ptr PAGXParser::parseGroup(const std::shared_ptr& node) { + auto group = std::make_shared(); + + auto anchorStr = PAGXAttributes::ParseString(node, "anchor", "0,0"); + group->setAnchorPoint(PAGXAttributes::ParsePoint(anchorStr)); + + auto positionStr = PAGXAttributes::ParseString(node, "position", "0,0"); + group->setPosition(PAGXAttributes::ParsePoint(positionStr)); + + auto scaleStr = PAGXAttributes::ParseString(node, "scale", "1,1"); + group->setScale(PAGXAttributes::ParsePoint(scaleStr, {1.0f, 1.0f})); + + group->setRotation(PAGXAttributes::ParseFloat(node, "rotation", 0.0f)); + group->setAlpha(PAGXAttributes::ParseFloat(node, "alpha", 1.0f)); + group->setSkew(PAGXAttributes::ParseFloat(node, "skew", 0.0f)); + group->setSkewAxis(PAGXAttributes::ParseFloat(node, "skewAxis", 0.0f)); + + std::vector> elements; + auto child = node->firstChild; + while (child) { + auto element = parseVectorElement(child); + if (element) { + elements.push_back(element); + } + child = child->nextSibling; + } + group->setElements(std::move(elements)); + return group; +} + +std::shared_ptr PAGXParser::parseRectangle(const std::shared_ptr& node) { + auto rect = std::make_shared(); + auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); + auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); + rect->setCenter({centerX, centerY}); + auto width = PAGXAttributes::ParseFloat(node, "width", 100.0f); + auto height = PAGXAttributes::ParseFloat(node, "height", 100.0f); + rect->setSize({width, height}); + rect->setRoundness(PAGXAttributes::ParseFloat(node, "roundness", 0.0f)); + rect->setReversed(PAGXAttributes::ParseBool(node, "reversed", false)); + return rect; +} + +std::shared_ptr PAGXParser::parseEllipse(const std::shared_ptr& node) { + auto ellipse = std::make_shared(); + auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); + auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); + ellipse->setCenter({centerX, centerY}); + auto width = PAGXAttributes::ParseFloat(node, "width", 100.0f); + auto height = PAGXAttributes::ParseFloat(node, "height", 100.0f); + ellipse->setSize({width, height}); + ellipse->setReversed(PAGXAttributes::ParseBool(node, "reversed", false)); + return ellipse; +} + +std::shared_ptr PAGXParser::parsePolystar(const std::shared_ptr& node) { + auto polystar = std::make_shared(); + auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); + auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); + polystar->setCenter({centerX, centerY}); + auto typeStr = PAGXAttributes::ParseString(node, "type", "star"); + polystar->setPolystarType(PAGXAttributes::ParsePolystarType(typeStr)); + polystar->setPointCount(PAGXAttributes::ParseFloat(node, "points", 5.0f)); + polystar->setOuterRadius(PAGXAttributes::ParseFloat(node, "outerRadius", 100.0f)); + polystar->setInnerRadius(PAGXAttributes::ParseFloat(node, "innerRadius", 50.0f)); + polystar->setRotation(PAGXAttributes::ParseFloat(node, "rotation", 0.0f)); + polystar->setOuterRoundness(PAGXAttributes::ParseFloat(node, "outerRoundness", 0.0f)); + polystar->setInnerRoundness(PAGXAttributes::ParseFloat(node, "innerRoundness", 0.0f)); + polystar->setReversed(PAGXAttributes::ParseBool(node, "reversed", false)); + return polystar; +} + +std::shared_ptr PAGXParser::parsePath(const std::shared_ptr& node) { + auto shapePath = std::make_shared(); + auto d = PAGXAttributes::ParseString(node, "d", ""); + if (!d.empty()) { + auto path = SVGPathParser::FromSVGString(d); + if (path) { + shapePath->setPath(*path); + } + } + shapePath->setReversed(PAGXAttributes::ParseBool(node, "reversed", false)); + return shapePath; +} + +std::shared_ptr PAGXParser::parseTextSpan(const std::shared_ptr& node) { + auto textSpan = std::make_shared(); + auto x = PAGXAttributes::ParseFloat(node, "x", 0.0f); + auto y = PAGXAttributes::ParseFloat(node, "y", 0.0f); + + auto textContent = PAGXAttributes::GetTextContent(node); + auto fontFamily = PAGXAttributes::ParseString(node, "font", ""); + auto fontSize = PAGXAttributes::ParseFloat(node, "fontSize", 12.0f); + + // Try to create typeface from font name + auto typeface = fontFamily.empty() ? nullptr : Typeface::MakeFromName(fontFamily, ""); + + Font font(typeface, fontSize); + auto textBlob = TextLayouter::Layout(textContent, font); + textSpan->setTextBlob(textBlob); + + // Parse text anchor and adjust position accordingly + auto textAnchorStr = PAGXAttributes::ParseString(node, "textAnchor", "start"); + if (textBlob && textAnchorStr != "start") { + auto bounds = textBlob->getTightBounds(); + if (textAnchorStr == "middle") { + x -= bounds.width() * 0.5f; + } else if (textAnchorStr == "end") { + x -= bounds.width(); + } + } + + textSpan->setPosition({x, y}); + + return textSpan; +} + +std::shared_ptr PAGXParser::parseFill(const std::shared_ptr& node) { + auto fill = std::make_shared(); + + // Try to get color from attribute + auto colorStr = PAGXAttributes::ParseString(node, "color", ""); + if (!colorStr.empty()) { + if (colorStr[0] == '#' && colorStr.length() > 1 && !std::isxdigit(colorStr[1])) { + // Reference to color source: #gradientId + fill->setColorSource(resolveColorReference(colorStr)); + } else { + // Direct color value + fill->setColorSource(SolidColor::Make(PAGXAttributes::ParseColor(colorStr))); + } + } else { + // Try inline color source + auto inlineColor = parseInlineColorSource(node); + if (inlineColor) { + fill->setColorSource(inlineColor); + } + } + + fill->setAlpha(PAGXAttributes::ParseFloat(node, "alpha", 1.0f)); + auto blendModeStr = PAGXAttributes::ParseString(node, "blendMode", "normal"); + fill->setBlendMode(PAGXAttributes::ParseBlendMode(blendModeStr)); + auto fillRuleStr = PAGXAttributes::ParseString(node, "fillRule", "winding"); + fill->setFillRule(fillRuleStr == "evenOdd" ? FillRule::EvenOdd : FillRule::Winding); + + return fill; +} + +std::shared_ptr PAGXParser::parseStroke(const std::shared_ptr& node) { + auto stroke = std::make_shared(); + + auto colorStr = PAGXAttributes::ParseString(node, "color", ""); + if (!colorStr.empty()) { + if (colorStr[0] == '#' && colorStr.length() > 1 && !std::isxdigit(colorStr[1])) { + stroke->setColorSource(resolveColorReference(colorStr)); + } else { + stroke->setColorSource(SolidColor::Make(PAGXAttributes::ParseColor(colorStr))); + } + } else { + auto inlineColor = parseInlineColorSource(node); + if (inlineColor) { + stroke->setColorSource(inlineColor); + } + } + + stroke->setStrokeWidth(PAGXAttributes::ParseFloat(node, "width", 1.0f)); + stroke->setAlpha(PAGXAttributes::ParseFloat(node, "alpha", 1.0f)); + auto blendModeStr = PAGXAttributes::ParseString(node, "blendMode", "normal"); + stroke->setBlendMode(PAGXAttributes::ParseBlendMode(blendModeStr)); + + auto capStr = PAGXAttributes::ParseString(node, "cap", "butt"); + stroke->setLineCap(PAGXAttributes::ParseLineCap(capStr)); + + auto joinStr = PAGXAttributes::ParseString(node, "join", "miter"); + stroke->setLineJoin(PAGXAttributes::ParseLineJoin(joinStr)); + + stroke->setMiterLimit(PAGXAttributes::ParseFloat(node, "miterLimit", 4.0f)); + + auto dashesStr = PAGXAttributes::ParseString(node, "dashes", ""); + if (!dashesStr.empty()) { + stroke->setDashes(PAGXAttributes::ParseDashes(dashesStr)); + } + stroke->setDashOffset(PAGXAttributes::ParseFloat(node, "dashOffset", 0.0f)); + + auto alignStr = PAGXAttributes::ParseString(node, "align", "center"); + stroke->setStrokeAlign(PAGXAttributes::ParseStrokeAlign(alignStr)); + + return stroke; +} + +std::shared_ptr PAGXParser::parseTrimPath(const std::shared_ptr& node) { + auto trim = std::make_shared(); + trim->setStart(PAGXAttributes::ParseFloat(node, "start", 0.0f)); + trim->setEnd(PAGXAttributes::ParseFloat(node, "end", 1.0f)); + trim->setOffset(PAGXAttributes::ParseFloat(node, "offset", 0.0f)); + auto typeStr = PAGXAttributes::ParseString(node, "type", "separate"); + trim->setTrimType(PAGXAttributes::ParseTrimPathType(typeStr)); + return trim; +} + +std::shared_ptr PAGXParser::parseRoundCorner(const std::shared_ptr& node) { + auto round = std::make_shared(); + round->setRadius(PAGXAttributes::ParseFloat(node, "radius", 10.0f)); + return round; +} + +std::shared_ptr PAGXParser::parseMergePath(const std::shared_ptr& node) { + auto merge = std::make_shared(); + auto opStr = PAGXAttributes::ParseString(node, "op", "append"); + merge->setMode(PAGXAttributes::ParseMergePathOp(opStr)); + return merge; +} + +std::shared_ptr PAGXParser::parseRepeater(const std::shared_ptr& node) { + auto repeater = std::make_shared(); + repeater->setCopies(PAGXAttributes::ParseFloat(node, "copies", 3.0f)); + repeater->setOffset(PAGXAttributes::ParseFloat(node, "offset", 0.0f)); + + auto orderStr = PAGXAttributes::ParseString(node, "order", "belowOriginal"); + repeater->setOrder(orderStr == "aboveOriginal" ? RepeaterOrder::AboveOriginal + : RepeaterOrder::BelowOriginal); + + auto anchorStr = PAGXAttributes::ParseString(node, "anchor", "0,0"); + repeater->setAnchorPoint(PAGXAttributes::ParsePoint(anchorStr)); + + auto positionStr = PAGXAttributes::ParseString(node, "position", "100,100"); + repeater->setPosition(PAGXAttributes::ParsePoint(positionStr, {100.0f, 100.0f})); + + repeater->setRotation(PAGXAttributes::ParseFloat(node, "rotation", 0.0f)); + + auto scaleStr = PAGXAttributes::ParseString(node, "scale", "1,1"); + repeater->setScale(PAGXAttributes::ParsePoint(scaleStr, {1.0f, 1.0f})); + + repeater->setStartAlpha(PAGXAttributes::ParseFloat(node, "startAlpha", 1.0f)); + repeater->setEndAlpha(PAGXAttributes::ParseFloat(node, "endAlpha", 1.0f)); + return repeater; +} + +std::shared_ptr PAGXParser::parseColorSource(const std::shared_ptr& node) { + const auto& name = node->name; + if (name == "SolidColor") { + auto colorStr = PAGXAttributes::ParseString(node, "color", "#000000"); + return SolidColor::Make(PAGXAttributes::ParseColor(colorStr)); + } + if (name == "LinearGradient") { + return parseLinearGradient(node); + } + if (name == "RadialGradient") { + return parseRadialGradient(node); + } + if (name == "ConicGradient") { + return parseConicGradient(node); + } + if (name == "DiamondGradient") { + return parseDiamondGradient(node); + } + if (name == "ImagePattern") { + return parseImagePattern(node); + } + return nullptr; +} + +std::shared_ptr PAGXParser::parseInlineColorSource( + const std::shared_ptr& parentNode) { + auto child = parentNode->firstChild; + while (child) { + if (child->type == DOMNodeType::Element) { + auto source = parseColorSource(child); + if (source) { + return source; + } + } + child = child->nextSibling; + } + return nullptr; +} + +std::vector> PAGXParser::parseColorStops( + const std::shared_ptr& node) { + std::vector> stops; + auto child = node->firstChild; + while (child) { + if (child->name == "ColorStop") { + auto offset = PAGXAttributes::ParseFloat(child, "offset", 0.0f); + auto colorStr = PAGXAttributes::ParseString(child, "color", "#000000"); + stops.emplace_back(offset, PAGXAttributes::ParseColor(colorStr)); + } + child = child->nextSibling; + } + return stops; +} + +std::shared_ptr PAGXParser::parseLinearGradient( + const std::shared_ptr& node) { + auto startX = PAGXAttributes::ParseFloat(node, "startX", 0.0f); + auto startY = PAGXAttributes::ParseFloat(node, "startY", 0.0f); + auto endX = PAGXAttributes::ParseFloat(node, "endX", 0.0f); + auto endY = PAGXAttributes::ParseFloat(node, "endY", 0.0f); + + auto stops = parseColorStops(node); + std::vector colors; + std::vector positions; + colors.reserve(stops.size()); + positions.reserve(stops.size()); + for (const auto& [offset, color] : stops) { + positions.push_back(offset); + colors.push_back(color); + } + + auto gradient = Gradient::MakeLinear({startX, startY}, {endX, endY}, colors, positions); + auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); + if (!matrixStr.empty()) { + gradient->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); + } + return gradient; +} + +std::shared_ptr PAGXParser::parseRadialGradient( + const std::shared_ptr& node) { + auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); + auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); + auto radius = PAGXAttributes::ParseFloat(node, "radius", 0.0f); + + auto stops = parseColorStops(node); + std::vector colors; + std::vector positions; + colors.reserve(stops.size()); + positions.reserve(stops.size()); + for (const auto& [offset, color] : stops) { + positions.push_back(offset); + colors.push_back(color); + } + + auto gradient = Gradient::MakeRadial({centerX, centerY}, radius, colors, positions); + auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); + if (!matrixStr.empty()) { + gradient->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); + } + return gradient; +} + +std::shared_ptr PAGXParser::parseConicGradient( + const std::shared_ptr& node) { + auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); + auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); + auto startAngle = PAGXAttributes::ParseFloat(node, "startAngle", 0.0f); + auto endAngle = PAGXAttributes::ParseFloat(node, "endAngle", 360.0f); + + auto stops = parseColorStops(node); + std::vector colors; + std::vector positions; + colors.reserve(stops.size()); + positions.reserve(stops.size()); + for (const auto& [offset, color] : stops) { + positions.push_back(offset); + colors.push_back(color); + } + + auto gradient = Gradient::MakeConic({centerX, centerY}, startAngle, endAngle, colors, positions); + auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); + if (!matrixStr.empty()) { + gradient->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); + } + return gradient; +} + +std::shared_ptr PAGXParser::parseDiamondGradient( + const std::shared_ptr& node) { + auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); + auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); + auto halfDiagonal = PAGXAttributes::ParseFloat(node, "halfDiagonal", 0.0f); + + auto stops = parseColorStops(node); + std::vector colors; + std::vector positions; + colors.reserve(stops.size()); + positions.reserve(stops.size()); + for (const auto& [offset, color] : stops) { + positions.push_back(offset); + colors.push_back(color); + } + + auto gradient = Gradient::MakeDiamond({centerX, centerY}, halfDiagonal, colors, positions); + auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); + if (!matrixStr.empty()) { + gradient->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); + } + return gradient; +} + +std::shared_ptr PAGXParser::parseImagePattern(const std::shared_ptr& node) { + auto [hasImage, imageRef] = node->findAttribute("image"); + if (!hasImage || imageRef.empty()) { + return nullptr; + } + + std::shared_ptr image = nullptr; + if (imageRef[0] == '#') { + auto it = images.find(imageRef.substr(1)); + if (it != images.end()) { + image = it->second; + } + } + + if (!image) { + return nullptr; + } + + auto tileModeXStr = PAGXAttributes::ParseString(node, "tileModeX", "clamp"); + auto tileModeYStr = PAGXAttributes::ParseString(node, "tileModeY", "clamp"); + + auto pattern = + ImagePattern::Make(image, PAGXAttributes::ParseTileMode(tileModeXStr), + PAGXAttributes::ParseTileMode(tileModeYStr), SamplingOptions()); + auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); + if (!matrixStr.empty()) { + pattern->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); + } + return pattern; +} + +std::vector> PAGXParser::parseFilters( + const std::shared_ptr& node) { + std::vector> filters; + auto child = node->firstChild; + while (child) { + if (child->name == "BlurFilter") { + auto blurX = PAGXAttributes::ParseFloat(child, "blurrinessX", 0.0f); + auto blurY = PAGXAttributes::ParseFloat(child, "blurrinessY", 0.0f); + auto tileModeStr = PAGXAttributes::ParseString(child, "tileMode", "decal"); + auto filter = BlurFilter::Make(blurX, blurY, PAGXAttributes::ParseTileMode(tileModeStr)); + if (filter) { + filters.push_back(filter); + } + } else if (child->name == "DropShadowFilter") { + auto offsetX = PAGXAttributes::ParseFloat(child, "offsetX", 0.0f); + auto offsetY = PAGXAttributes::ParseFloat(child, "offsetY", 0.0f); + auto blurX = PAGXAttributes::ParseFloat(child, "blurrinessX", 0.0f); + auto blurY = PAGXAttributes::ParseFloat(child, "blurrinessY", 0.0f); + auto colorStr = PAGXAttributes::ParseString(child, "color", "#000000"); + auto shadowOnly = PAGXAttributes::ParseBool(child, "shadowOnly", false); + auto filter = DropShadowFilter::Make(offsetX, offsetY, blurX, blurY, + PAGXAttributes::ParseColor(colorStr), shadowOnly); + if (filter) { + filters.push_back(filter); + } + } + child = child->nextSibling; + } + return filters; +} + +std::vector> PAGXParser::parseStyles( + const std::shared_ptr& node) { + std::vector> styles; + auto child = node->firstChild; + while (child) { + if (child->name == "DropShadowStyle") { + auto offsetX = PAGXAttributes::ParseFloat(child, "offsetX", 0.0f); + auto offsetY = PAGXAttributes::ParseFloat(child, "offsetY", 0.0f); + auto blurX = PAGXAttributes::ParseFloat(child, "blurrinessX", 0.0f); + auto blurY = PAGXAttributes::ParseFloat(child, "blurrinessY", 0.0f); + auto colorStr = PAGXAttributes::ParseString(child, "color", "#000000"); + auto showBehind = PAGXAttributes::ParseBool(child, "showBehindLayer", true); + auto style = DropShadowStyle::Make(offsetX, offsetY, blurX, blurY, + PAGXAttributes::ParseColor(colorStr), showBehind); + if (style) { + styles.push_back(style); + } + } else if (child->name == "InnerShadowStyle") { + auto offsetX = PAGXAttributes::ParseFloat(child, "offsetX", 0.0f); + auto offsetY = PAGXAttributes::ParseFloat(child, "offsetY", 0.0f); + auto blurX = PAGXAttributes::ParseFloat(child, "blurrinessX", 0.0f); + auto blurY = PAGXAttributes::ParseFloat(child, "blurrinessY", 0.0f); + auto colorStr = PAGXAttributes::ParseString(child, "color", "#000000"); + auto style = InnerShadowStyle::Make(offsetX, offsetY, blurX, blurY, + PAGXAttributes::ParseColor(colorStr)); + if (style) { + styles.push_back(style); + } + } + child = child->nextSibling; + } + return styles; +} + +std::shared_ptr PAGXParser::resolveColorReference(const std::string& value) { + if (value.empty() || value[0] != '#') { + return nullptr; + } + auto id = value.substr(1); + auto it = colorSources.find(id); + if (it != colorSources.end()) { + return it->second; + } + return nullptr; +} + +std::shared_ptr PAGXParser::resolveImageReference(const std::string& ref) { + if (ref.empty()) { + return nullptr; + } + // Check for data URI + if (ref.find("data:") == 0) { + // Parse data URI format: data:[][;base64], + auto commaPos = ref.find(','); + if (commaPos == std::string::npos) { + return nullptr; + } + auto header = ref.substr(0, commaPos); + auto base64Data = ref.substr(commaPos + 1); + // Check if it's base64 encoded + if (header.find(";base64") == std::string::npos) { + return nullptr; + } + auto data = pagx::Base64Decode(base64Data); + if (data == nullptr) { + return nullptr; + } + return Image::MakeFromEncoded(data); + } + // Relative path - resolve against basePath + std::string fullPath = basePath_; + if (!fullPath.empty() && fullPath.back() != '/') { + fullPath += '/'; + } + fullPath += ref; + return Image::MakeFromFile(fullPath); +} + +} // namespace pagx diff --git a/pagx/src/layers/PAGXParser.h b/pagx/src/layers/PAGXParser.h new file mode 100644 index 0000000000..bb33e77f45 --- /dev/null +++ b/pagx/src/layers/PAGXParser.h @@ -0,0 +1,137 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include "tgfx/core/Image.h" +#include "tgfx/layers/Layer.h" +#include "tgfx/layers/VectorLayer.h" +#include "tgfx/layers/filters/LayerFilter.h" +#include "tgfx/layers/layerstyles/LayerStyle.h" +#include "tgfx/layers/vectors/ColorSource.h" +#include "tgfx/layers/vectors/Ellipse.h" +#include "tgfx/layers/vectors/FillStyle.h" +#include "tgfx/layers/vectors/Gradient.h" +#include "tgfx/layers/vectors/ImagePattern.h" +#include "tgfx/layers/vectors/MergePath.h" +#include "tgfx/layers/vectors/Polystar.h" +#include "tgfx/layers/vectors/Rectangle.h" +#include "tgfx/layers/vectors/Repeater.h" +#include "tgfx/layers/vectors/RoundCorner.h" +#include "tgfx/layers/vectors/ShapePath.h" +#include "tgfx/layers/vectors/StrokeStyle.h" +#include "tgfx/layers/vectors/TextSpan.h" +#include "tgfx/layers/vectors/TrimPath.h" +#include "tgfx/layers/vectors/VectorElement.h" +#include "tgfx/layers/vectors/VectorGroup.h" +#include "tgfx/svg/xml/XMLDOM.h" + +namespace pagx { + +using namespace tgfx; + +class PAGXParser { + public: + PAGXParser(std::shared_ptr rootNode, const std::string& basePath); + + bool parse(); + + float width() const { + return width_; + } + + float height() const { + return height_; + } + + std::shared_ptr getRoot() const { + return rootLayer; + } + + private: + void parseResources(const std::shared_ptr& node); + + std::shared_ptr parseLayer(const std::shared_ptr& node); + + std::vector> parseContents(const std::shared_ptr& node); + + std::shared_ptr parseVectorElement(const std::shared_ptr& node); + + std::shared_ptr parseGroup(const std::shared_ptr& node); + + std::shared_ptr parseRectangle(const std::shared_ptr& node); + + std::shared_ptr parseEllipse(const std::shared_ptr& node); + + std::shared_ptr parsePolystar(const std::shared_ptr& node); + + std::shared_ptr parsePath(const std::shared_ptr& node); + + std::shared_ptr parseTextSpan(const std::shared_ptr& node); + + std::shared_ptr parseFill(const std::shared_ptr& node); + + std::shared_ptr parseStroke(const std::shared_ptr& node); + + std::shared_ptr parseTrimPath(const std::shared_ptr& node); + + std::shared_ptr parseRoundCorner(const std::shared_ptr& node); + + std::shared_ptr parseMergePath(const std::shared_ptr& node); + + std::shared_ptr parseRepeater(const std::shared_ptr& node); + + std::shared_ptr parseColorSource(const std::shared_ptr& node); + + std::shared_ptr parseInlineColorSource(const std::shared_ptr& parentNode); + + std::shared_ptr parseLinearGradient(const std::shared_ptr& node); + + std::shared_ptr parseRadialGradient(const std::shared_ptr& node); + + std::shared_ptr parseConicGradient(const std::shared_ptr& node); + + std::shared_ptr parseDiamondGradient(const std::shared_ptr& node); + + std::shared_ptr parseImagePattern(const std::shared_ptr& node); + + std::vector> parseColorStops(const std::shared_ptr& node); + + std::vector> parseFilters(const std::shared_ptr& node); + + std::vector> parseStyles(const std::shared_ptr& node); + + std::shared_ptr resolveColorReference(const std::string& value); + + std::shared_ptr resolveImageReference(const std::string& ref); + + std::shared_ptr rootNode_ = nullptr; + std::string basePath_; + float width_ = 0.0f; + float height_ = 0.0f; + std::shared_ptr rootLayer = nullptr; + std::unordered_map> colorSources; + std::unordered_map> images; + std::unordered_map> layerIdMap; +}; + +} // namespace pagx diff --git a/pagx/src/layers/PAGXUtils.cpp b/pagx/src/layers/PAGXUtils.cpp new file mode 100644 index 0000000000..b421731a66 --- /dev/null +++ b/pagx/src/layers/PAGXUtils.cpp @@ -0,0 +1,80 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PAGXUtils.h" +#include +#include + +namespace pagx { + +std::shared_ptr Base64Decode(const std::string& encodedString) { + static const std::array decodingTable = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, + 64, 64, 64, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 64, 64, 64, 64, 64, 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64}; + + size_t inLength = encodedString.size(); + if (inLength % 4 != 0) { + return nullptr; + } + + size_t outLength = inLength / 4 * 3; + if (encodedString[inLength - 1] == '=') { + outLength--; + } + if (encodedString[inLength - 2] == '=') { + outLength--; + } + + auto out = static_cast(malloc(outLength)); + auto outData = tgfx::Data::MakeAdopted(out, outLength, tgfx::Data::FreeProc); + + for (size_t i = 0, j = 0; i < inLength;) { + uint32_t a = encodedString[i] == '=' + ? 0 & i++ + : decodingTable[static_cast(encodedString[i++])]; + uint32_t b = encodedString[i] == '=' + ? 0 & i++ + : decodingTable[static_cast(encodedString[i++])]; + uint32_t c = encodedString[i] == '=' + ? 0 & i++ + : decodingTable[static_cast(encodedString[i++])]; + uint32_t d = encodedString[i] == '=' + ? 0 & i++ + : decodingTable[static_cast(encodedString[i++])]; + + uint32_t triple = (a << 18) + (b << 12) + (c << 6) + d; + + if (j < outLength) { + out[j++] = (triple >> 16) & 0xFF; + } + if (j < outLength) { + out[j++] = (triple >> 8) & 0xFF; + } + if (j < outLength) { + out[j++] = triple & 0xFF; + } + } + + return outData; +} + +} // namespace pagx diff --git a/pagx/src/layers/PAGXUtils.h b/pagx/src/layers/PAGXUtils.h new file mode 100644 index 0000000000..0663472a33 --- /dev/null +++ b/pagx/src/layers/PAGXUtils.h @@ -0,0 +1,29 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "tgfx/core/Data.h" + +namespace pagx { + +std::shared_ptr Base64Decode(const std::string& encodedString); + +} // namespace pagx diff --git a/pagx/src/layers/TextLayouter.cpp b/pagx/src/layers/TextLayouter.cpp new file mode 100644 index 0000000000..e386f1a751 --- /dev/null +++ b/pagx/src/layers/TextLayouter.cpp @@ -0,0 +1,170 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/layers/TextLayouter.h" +#include +#include "tgfx/core/TextBlobBuilder.h" +#include "tgfx/core/Typeface.h" +#include "tgfx/core/UTF.h" + +namespace pagx { + +using namespace tgfx; + +struct GlyphInfo { + Unichar unichar = 0; + GlyphID glyphID = 0; + std::shared_ptr typeface = nullptr; +}; + +static std::mutex& FallbackMutex = *new std::mutex; +static std::vector> FallbackTypefaces = {}; + +void TextLayouter::SetFallbackTypefaces(std::vector> typefaces) { + std::lock_guard lock(FallbackMutex); + FallbackTypefaces = std::move(typefaces); +} + +std::vector> TextLayouter::GetFallbackTypefaces() { + std::lock_guard lock(FallbackMutex); + return FallbackTypefaces; +} + +static std::vector ShapeText(const std::string& text, + const std::shared_ptr& typeface, + const std::vector>& fallbacks) { + if (text.empty()) { + return {}; + } + + std::vector glyphs = {}; + glyphs.reserve(text.size()); + + const char* head = text.data(); + const char* tail = head + text.size(); + + while (head < tail) { + auto unichar = UTF::NextUTF8(&head, tail); + + GlyphID glyphID = typeface ? typeface->getGlyphID(unichar) : 0; + std::shared_ptr matchedTypeface = typeface; + + if (glyphID == 0) { + for (const auto& fallback : fallbacks) { + if (fallback == nullptr) { + continue; + } + glyphID = fallback->getGlyphID(unichar); + if (glyphID > 0) { + matchedTypeface = fallback; + break; + } + } + } + + glyphs.push_back({unichar, glyphID, matchedTypeface}); + } + + return glyphs; +} + +std::shared_ptr TextLayouter::Layout(const std::string& text, const Font& font) { + if (text.empty()) { + return nullptr; + } + + auto typeface = font.getTypeface(); + auto fallbacks = GetFallbackTypefaces(); + + // If primary typeface is empty or invalid, try to use first fallback or system default + if (typeface == nullptr || typeface->fontFamily().empty()) { + if (!fallbacks.empty() && fallbacks[0] != nullptr) { + typeface = fallbacks[0]; + } else { + // Try system default fonts (platform-specific) + static const char* defaultFonts[] = {"Helvetica", "Arial", "sans-serif", nullptr}; + for (int i = 0; defaultFonts[i] != nullptr && typeface == nullptr; i++) { + typeface = Typeface::MakeFromName(defaultFonts[i], ""); + } + if (typeface == nullptr) { + return nullptr; + } + } + } + + auto glyphs = ShapeText(text, typeface, fallbacks); + if (glyphs.empty()) { + return nullptr; + } + + std::vector positions = {}; + positions.reserve(glyphs.size()); + + float xOffset = 0; + float emptyAdvance = font.getSize() / 2.0f; + + for (const auto& glyph : glyphs) { + positions.push_back({xOffset, 0}); + + if (glyph.glyphID > 0 && glyph.typeface != nullptr) { + Font glyphFont = font; + glyphFont.setTypeface(glyph.typeface); + xOffset += glyphFont.getAdvance(glyph.glyphID); + } else { + xOffset += emptyAdvance; + } + } + + // Group glyphs by typeface + std::unordered_map> typefaceToIndices = {}; + for (size_t i = 0; i < glyphs.size(); i++) { + const auto& glyph = glyphs[i]; + if (glyph.glyphID == 0 || glyph.typeface == nullptr) { + continue; + } + typefaceToIndices[glyph.typeface->uniqueID()].push_back(i); + } + + if (typefaceToIndices.empty()) { + return nullptr; + } + + TextBlobBuilder builder; + for (const auto& [typefaceID, indices] : typefaceToIndices) { + if (indices.empty()) { + continue; + } + + auto typeface = glyphs[indices[0]].typeface; + Font runFont = font; + runFont.setTypeface(typeface); + + const auto& buffer = builder.allocRunPos(runFont, indices.size()); + auto* pointPositions = reinterpret_cast(buffer.positions); + + for (size_t i = 0; i < indices.size(); i++) { + auto idx = indices[i]; + buffer.glyphs[i] = glyphs[idx].glyphID; + pointPositions[i] = positions[idx]; + } + } + + return builder.build(); +} + +} // namespace pagx diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp new file mode 100644 index 0000000000..bc2418abc8 --- /dev/null +++ b/pagx/src/svg/SVGImporter.cpp @@ -0,0 +1,68 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/svg/SVGImporter.h" +#include +#include "SVGToPAGXConverter.h" +#include "tgfx/core/Data.h" +#include "tgfx/core/Stream.h" + +namespace pagx { + +std::string SVGImporter::ImportFromFile(const std::string& svgFilePath) { + auto data = tgfx::Data::MakeFromFile(svgFilePath); + if (!data) { + return ""; + } + auto stream = tgfx::Stream::MakeFromData(std::move(data)); + if (!stream) { + return ""; + } + return ImportFromStream(*stream); +} + +std::string SVGImporter::ImportFromStream(tgfx::Stream& svgStream) { + auto svgDOM = tgfx::SVGDOM::Make(svgStream); + if (!svgDOM) { + return ""; + } + return ImportFromDOM(svgDOM); +} + +std::string SVGImporter::ImportFromDOM(const std::shared_ptr& svgDOM) { + if (!svgDOM) { + return ""; + } + SVGToPAGXConverter converter(svgDOM); + return converter.convert(); +} + +bool SVGImporter::SaveToFile(const std::string& pagxContent, const std::string& outputPath) { + if (pagxContent.empty() || outputPath.empty()) { + return false; + } + std::ofstream file(outputPath); + if (!file.is_open()) { + return false; + } + file << pagxContent; + file.close(); + return true; +} + +} // namespace pagx diff --git a/pagx/src/svg/SVGToPAGXConverter.cpp b/pagx/src/svg/SVGToPAGXConverter.cpp new file mode 100644 index 0000000000..68920e48c6 --- /dev/null +++ b/pagx/src/svg/SVGToPAGXConverter.cpp @@ -0,0 +1,1897 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "SVGToPAGXConverter.h" +#include +#include +#include "tgfx/svg/SVGPathParser.h" +#include "tgfx/svg/node/SVGImage.h" +#include "tgfx/svg/node/SVGLinearGradient.h" +#include "tgfx/svg/node/SVGPattern.h" +#include "tgfx/svg/node/SVGRadialGradient.h" +#include "tgfx/svg/node/SVGStop.h" +#include "tgfx/svg/node/SVGUse.h" + +namespace pagx { + +// Helper to get value from SVGProperty +template +static std::optional GetPropertyValue(const SVGProperty& prop) { + return prop.get(); +} + +template +static std::optional GetPropertyValue(const SVGProperty& prop) { + return prop.get(); +} + +static std::string CleanFontFamily(const std::string& family) { + std::string result = family; + if (result.size() >= 2) { + if ((result.front() == '"' && result.back() == '"') || + (result.front() == '\'' && result.back() == '\'')) { + result = result.substr(1, result.size() - 2); + } + } + return result; +} + +SVGToPAGXConverter::SVGToPAGXConverter(const std::shared_ptr& svgDOM) : _svgDOM(svgDOM) { +} + +std::string SVGToPAGXConverter::convert() { + if (!_svgDOM || !_svgDOM->getRoot()) { + return ""; + } + + auto root = _svgDOM->getRoot(); + auto containerSize = _svgDOM->getContainerSize(); + _width = containerSize.width; + _height = containerSize.height; + + if (_width <= 0 || _height <= 0) { + auto viewBox = root->getViewBox(); + if (viewBox.has_value()) { + _width = viewBox->width(); + _height = viewBox->height(); + } + } + + if (_width <= 0) { + _width = 100; + } + if (_height <= 0) { + _height = 100; + } + + collectGradients(); + collectPatterns(); + collectMasks(); + collectUsedMasks(root.get()); + countColorSourceUsages(); + + writeHeader(); + writeResources(); + writeMaskLayers(1); + writeLayers(); + + _output << "\n"; + return _output.str(); +} + +void SVGToPAGXConverter::collectUsedMasks(const SVGNode* node) { + if (!node) { + return; + } + + auto maskId = getMaskId(node); + if (!maskId.empty()) { + _usedMasks.insert(maskId); + } + + // Recursively check children if node is a container + if (node->hasChildren()) { + auto container = static_cast(node); + for (const auto& child : container->getChildren()) { + collectUsedMasks(child.get()); + } + } +} + +void SVGToPAGXConverter::writeHeader() { + _output << "\n"; + _output << "(_width) << "\" height=\"" + << static_cast(_height) << "\">\n"; +} + +void SVGToPAGXConverter::collectGradients() { + // Collect gradients from nodeIDMapper directly instead of traversing the whole tree + for (const auto& [id, svgNode] : _svgDOM->nodeIDMapper()) { + auto tag = svgNode->tag(); + if (tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient) { + _gradients[id] = svgNode.get(); + } + } +} + +void SVGToPAGXConverter::collectPatterns() { + // Collect patterns from nodeIDMapper + for (const auto& [id, svgNode] : _svgDOM->nodeIDMapper()) { + if (svgNode->tag() != SVGTag::Pattern) { + continue; + } + + auto pattern = static_cast(svgNode.get()); + auto container = static_cast(pattern); + + // Check if pattern uses objectBoundingBox units + bool isObjectBoundingBox = + pattern->getPatternUnits().type() == SVGObjectBoundingBoxUnits::Type::ObjectBoundingBox; + + // Pattern may contain elements that reference elements + for (const auto& child : container->getChildren()) { + if (child->tag() != SVGTag::Use) { + continue; + } + + auto use = static_cast(child.get()); + auto href = use->getHref(); + auto imageId = href.iri(); + if (imageId.empty()) { + continue; + } + + // Find the referenced image + auto imageIt = _svgDOM->nodeIDMapper().find(imageId); + if (imageIt == _svgDOM->nodeIDMapper().end() || imageIt->second->tag() != SVGTag::Image) { + continue; + } + + auto image = static_cast(imageIt->second.get()); + auto imageHref = image->getHref(); + auto imageData = imageHref.iri(); + + // Store image if not already stored + if (_images.find(imageId) == _images.end()) { + _images[imageId] = imageData; + } + + // Get image dimensions + float imageWidth = image->getWidth().value(); + float imageHeight = image->getHeight().value(); + + // Get pattern dimensions + auto patternWidth = pattern->getWidth(); + auto patternHeight = pattern->getHeight(); + + PatternInfo patternInfo = {}; + patternInfo.patternId = id; + patternInfo.imageId = imageId; + patternInfo.imageData = imageData; + patternInfo.imageWidth = imageWidth; + patternInfo.imageHeight = imageHeight; + patternInfo.isObjectBoundingBox = isObjectBoundingBox; + + // Store pattern dimensions + if (patternWidth.has_value() && patternHeight.has_value()) { + patternInfo.patternWidth = patternWidth->value(); + patternInfo.patternHeight = patternHeight->value(); + } + + _patterns[id] = patternInfo; + } + } +} + +void SVGToPAGXConverter::collectMasks() { + // Collect masks from nodeIDMapper + for (const auto& [id, svgNode] : _svgDOM->nodeIDMapper()) { + if (svgNode->tag() != SVGTag::Mask) { + continue; + } + + auto mask = static_cast(svgNode.get()); + MaskInfo maskInfo = {}; + maskInfo.maskId = id; + maskInfo.maskNode = mask; + maskInfo.isLuminance = mask->getMaskType().type() == SVGMaskType::Type::Luminance; + + _masks[id] = maskInfo; + } +} + +void SVGToPAGXConverter::countColorSourceUsages() { + auto root = _svgDOM->getRoot(); + for (const auto& child : root->getChildren()) { + countColorSourceUsagesFromNode(child.get()); + } +} + +void SVGToPAGXConverter::countColorSourceUsagesFromNode(const SVGNode* node) { + if (!node) { + return; + } + + auto tag = node->tag(); + + // Skip non-renderable elements + if (tag == SVGTag::Defs || tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient || + tag == SVGTag::Stop || tag == SVGTag::ClipPath || tag == SVGTag::Mask || + tag == SVGTag::Filter || tag == SVGTag::Pattern) { + return; + } + + // Check fill attribute for color source references + auto fillProp = node->getFill(); + auto fillOpt = GetPropertyValue(fillProp); + if (fillOpt.has_value() && fillOpt->type() == SVGPaint::Type::IRI) { + auto iri = fillOpt->iri().iri(); + auto& usage = _colorSourceUsages[iri]; + usage.count++; + if (_gradients.find(iri) != _gradients.end()) { + usage.type = "gradient"; + } else if (_patterns.find(iri) != _patterns.end()) { + usage.type = "pattern"; + } + } + + // Check stroke attribute for color source references + auto strokeProp = node->getStroke(); + auto strokeOpt = GetPropertyValue(strokeProp); + if (strokeOpt.has_value() && strokeOpt->type() == SVGPaint::Type::IRI) { + auto iri = strokeOpt->iri().iri(); + auto& usage = _colorSourceUsages[iri]; + usage.count++; + if (_gradients.find(iri) != _gradients.end()) { + usage.type = "gradient"; + } else if (_patterns.find(iri) != _patterns.end()) { + usage.type = "pattern"; + } + } + + // Recurse into containers + if (tag == SVGTag::G || tag == SVGTag::Svg) { + auto container = static_cast(node); + for (const auto& child : container->getChildren()) { + countColorSourceUsagesFromNode(child.get()); + } + } +} + +void SVGToPAGXConverter::writeResources() { + // Collect resources that need to be in Resources section (referenced more than once) + std::vector sharedGradients = {}; + std::vector sharedPatterns = {}; + + for (const auto& [id, usage] : _colorSourceUsages) { + if (usage.count > 1) { + if (usage.type == "gradient") { + sharedGradients.push_back(id); + } else if (usage.type == "pattern") { + // For patterns with objectBoundingBox, always inline since they depend on shape size + auto patternIt = _patterns.find(id); + if (patternIt != _patterns.end() && !patternIt->second.isObjectBoundingBox) { + sharedPatterns.push_back(id); + } + } + } + } + + // Check if we have any shared resources or images + bool hasSharedResources = !sharedGradients.empty() || !sharedPatterns.empty() || !_images.empty(); + if (!hasSharedResources) { + return; + } + + _output << "\n"; + indent(1); + _output << "\n"; + + // Output shared gradients + for (const auto& id : sharedGradients) { + auto it = _gradients.find(id); + if (it == _gradients.end()) { + continue; + } + auto node = it->second; + auto tag = node->tag(); + if (tag == SVGTag::LinearGradient) { + auto gradient = static_cast(node); + indent(2); + _output << "getX1(), _width) << "\""; + _output << " startY=\"" << lengthToFloat(gradient->getY1(), _height) << "\""; + _output << " endX=\"" << lengthToFloat(gradient->getX2(), _width) << "\""; + _output << " endY=\"" << lengthToFloat(gradient->getY2(), _height) << "\""; + _output << ">\n"; + + auto container = static_cast(node); + for (const auto& child : container->getChildren()) { + if (child->tag() == SVGTag::Stop) { + auto stop = static_cast(child.get()); + indent(3); + auto offset = stop->getOffset(); + float offsetValue = offset.value(); + if (offset.unit() == SVGLength::Unit::Percentage) { + offsetValue = offsetValue / 100.0f; + } + _output << "getStopColor(); + auto stopColorOpt = GetPropertyValue(stopColorProp); + if (stopColorOpt.has_value()) { + auto color = stopColorOpt->color(); + auto opacityProp = stop->getStopOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + float alpha = opacityOpt.has_value() ? opacityOpt.value() : 1.0f; + Color c = color; + c.alpha = alpha; + _output << " color=\"" << colorToHex(c) << "\""; + } + _output << "/>\n"; + } + } + + indent(2); + _output << "\n"; + } else if (tag == SVGTag::RadialGradient) { + auto gradient = static_cast(node); + indent(2); + _output << "getCx(), _width) << "\""; + _output << " centerY=\"" << lengthToFloat(gradient->getCy(), _height) << "\""; + auto r = gradient->getR(); + _output << " radius=\"" << lengthToFloat(r, std::max(_width, _height)) << "\""; + _output << ">\n"; + + auto container = static_cast(node); + for (const auto& child : container->getChildren()) { + if (child->tag() == SVGTag::Stop) { + auto stop = static_cast(child.get()); + indent(3); + auto offset = stop->getOffset(); + float offsetValue = offset.value(); + if (offset.unit() == SVGLength::Unit::Percentage) { + offsetValue = offsetValue / 100.0f; + } + _output << "getStopColor(); + auto stopColorOpt = GetPropertyValue(stopColorProp); + if (stopColorOpt.has_value()) { + auto color = stopColorOpt->color(); + auto opacityProp = stop->getStopOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + float alpha = opacityOpt.has_value() ? opacityOpt.value() : 1.0f; + Color c = color; + c.alpha = alpha; + _output << " color=\"" << colorToHex(c) << "\""; + } + _output << "/>\n"; + } + } + + indent(2); + _output << "\n"; + } + } + + // Output Image resources (always needed for patterns) + for (const auto& [imageId, imageData] : _images) { + indent(2); + _output << "\n"; + } + + // Output shared ImagePattern resources (non-objectBoundingBox patterns referenced multiple times) + for (const auto& patternId : sharedPatterns) { + auto it = _patterns.find(patternId); + if (it == _patterns.end()) { + continue; + } + const auto& patternInfo = it->second; + indent(2); + _output << "\n"; + } + + indent(1); + _output << "\n"; +} + +void SVGToPAGXConverter::writeLayers() { + auto root = _svgDOM->getRoot(); + _output << "\n"; + + convertChildren(root->getChildren(), 1); +} + +void SVGToPAGXConverter::convertNode(const SVGNode* node, int depth, bool /*needScopeIsolation*/) { + if (!node) { + return; + } + + auto tag = node->tag(); + + if (tag == SVGTag::Defs || tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient || + tag == SVGTag::Stop || tag == SVGTag::ClipPath || tag == SVGTag::Mask || + tag == SVGTag::Filter) { + return; + } + + auto visibilityProp = node->getVisibility(); + auto visibilityOpt = GetPropertyValue(visibilityProp); + if (visibilityOpt.has_value() && visibilityOpt.value().type() == SVGVisibility::Type::Hidden) { + return; + } + + switch (tag) { + case SVGTag::G: + case SVGTag::Svg: + convertContainer(static_cast(node), depth); + break; + case SVGTag::Rect: + convertRect(static_cast(node), depth); + break; + case SVGTag::Circle: + convertCircle(static_cast(node), depth); + break; + case SVGTag::Ellipse: + convertEllipse(static_cast(node), depth); + break; + case SVGTag::Path: + convertPath(static_cast(node), depth); + break; + case SVGTag::Polygon: + case SVGTag::Polyline: + convertPoly(static_cast(node), depth); + break; + case SVGTag::Line: + convertLine(static_cast(node), depth); + break; + case SVGTag::Text: + convertText(static_cast(node), depth); + break; + default: + break; + } +} + +void SVGToPAGXConverter::convertContainer(const SVGContainer* container, int depth) { + if (!container || !container->hasChildren()) { + return; + } + + int renderableCount = countRenderableChildren(container); + + // If container has only one renderable child, don't create a wrapper Layer for the container + // Just output the child directly + if (renderableCount == 1) { + convertChildren(container->getChildren(), depth); + return; + } + + // Container with multiple children needs a wrapper Layer + indent(depth); + _output << "(container); + auto transform = transformable->getTransform(); + if (!transform.isIdentity()) { + _output << " matrix=\"" << matrixToString(transform) << "\""; + } + + auto opacityProp = container->getOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + if (opacityOpt.has_value() && opacityOpt.value() < 1.0f) { + _output << " alpha=\"" << opacityOpt.value() << "\""; + } + + _output << ">\n"; + + convertChildren(container->getChildren(), depth + 1); + + indent(depth); + _output << "\n"; +} + +void SVGToPAGXConverter::convertRect(const SVGRect* rect, int depth) { + if (!rect) { + return; + } + + float x = std::stof(lengthToFloat(rect->getX(), _width)); + float y = std::stof(lengthToFloat(rect->getY(), _height)); + float width = std::stof(lengthToFloat(rect->getWidth(), _width)); + float height = std::stof(lengthToFloat(rect->getHeight(), _height)); + + if (width <= 0 || height <= 0) { + return; + } + + float centerX = x + width / 2.0f; + float centerY = y + height / 2.0f; + + float rx = 0; + float ry = 0; + auto rxOpt = rect->getRx(); + auto ryOpt = rect->getRy(); + if (rxOpt.has_value()) { + rx = std::stof(lengthToFloat(*rxOpt, _width)); + } + if (ryOpt.has_value()) { + ry = std::stof(lengthToFloat(*ryOpt, _height)); + } + if (rx > 0 && ry <= 0) { + ry = rx; + } + if (ry > 0 && rx <= 0) { + rx = ry; + } + + auto transformable = static_cast(rect); + auto transform = transformable->getTransform(); + bool hasTransform = !transform.isIdentity(); + + auto opacityProp = rect->getOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; + + // Check for mask + std::string maskId = getMaskId(rect); + if (!maskId.empty()) { + _usedMasks.insert(maskId); + } + + // Each shape element always gets its own Layer + indent(depth); + _output << "second.isLuminance) { + _output << " maskType=\"luminance\""; + } + } + _output << ">\n"; + indent(depth + 1); + _output << "\n"; + + // Group is needed when there's a transform or opacity to apply + int contentDepth = depth + 2; + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + contentDepth = depth + 3; + } + + indent(contentDepth); + _output << " 0 || ry > 0) { + float roundness = std::min(rx, ry); + _output << " roundness=\"" << roundness << "\""; + } + _output << "/>\n"; + + writeFillStyle(rect, contentDepth, x, y, width, height); + writeStrokeStyle(rect, contentDepth, x, y, width, height); + + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + } + + indent(depth + 1); + _output << "\n"; + indent(depth); + _output << "\n"; +} + +void SVGToPAGXConverter::convertCircle(const SVGCircle* circle, int depth) { + if (!circle) { + return; + } + + float cx = std::stof(lengthToFloat(circle->getCx(), _width)); + float cy = std::stof(lengthToFloat(circle->getCy(), _height)); + float r = std::stof(lengthToFloat(circle->getR(), std::max(_width, _height))); + + if (r <= 0) { + return; + } + + auto transformable = static_cast(circle); + auto transform = transformable->getTransform(); + bool hasTransform = !transform.isIdentity(); + + auto opacityProp = circle->getOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; + + std::string maskId = getMaskId(circle); + if (!maskId.empty()) { + _usedMasks.insert(maskId); + } + + indent(depth); + _output << "second.isLuminance) { + _output << " maskType=\"luminance\""; + } + } + _output << ">\n"; + indent(depth + 1); + _output << "\n"; + + int contentDepth = depth + 2; + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + contentDepth = depth + 3; + } + + indent(contentDepth); + _output << "\n"; + + writeFillStyle(circle, contentDepth, cx - r, cy - r, r * 2, r * 2); + writeStrokeStyle(circle, contentDepth, cx - r, cy - r, r * 2, r * 2); + + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + } + + indent(depth + 1); + _output << "\n"; + indent(depth); + _output << "\n"; +} + +void SVGToPAGXConverter::convertEllipse(const SVGEllipse* ellipse, int depth) { + if (!ellipse) { + return; + } + + float cx = std::stof(lengthToFloat(ellipse->getCx(), _width)); + float cy = std::stof(lengthToFloat(ellipse->getCy(), _height)); + + float rx = 0; + float ry = 0; + auto rxOpt = ellipse->getRx(); + auto ryOpt = ellipse->getRy(); + if (rxOpt.has_value()) { + rx = std::stof(lengthToFloat(*rxOpt, _width)); + } + if (ryOpt.has_value()) { + ry = std::stof(lengthToFloat(*ryOpt, _height)); + } + + if (rx <= 0 || ry <= 0) { + return; + } + + auto transformable = static_cast(ellipse); + auto transform = transformable->getTransform(); + bool hasTransform = !transform.isIdentity(); + + auto opacityProp = ellipse->getOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; + + std::string maskId = getMaskId(ellipse); + if (!maskId.empty()) { + _usedMasks.insert(maskId); + } + + indent(depth); + _output << "second.isLuminance) { + _output << " maskType=\"luminance\""; + } + } + _output << ">\n"; + indent(depth + 1); + _output << "\n"; + + int contentDepth = depth + 2; + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + contentDepth = depth + 3; + } + + indent(contentDepth); + _output << "\n"; + + writeFillStyle(ellipse, contentDepth, cx - rx, cy - ry, rx * 2, ry * 2); + writeStrokeStyle(ellipse, contentDepth, cx - rx, cy - ry, rx * 2, ry * 2); + + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + } + + indent(depth + 1); + _output << "\n"; + indent(depth); + _output << "\n"; +} + +void SVGToPAGXConverter::convertPath(const SVGPath* path, int depth) { + if (!path) { + return; + } + + auto shapePath = path->getShapePath(); + if (shapePath.isEmpty()) { + return; + } + + auto transformable = static_cast(path); + auto transform = transformable->getTransform(); + bool hasTransform = !transform.isIdentity(); + + auto opacityProp = path->getOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; + + std::string maskId = getMaskId(path); + if (!maskId.empty()) { + _usedMasks.insert(maskId); + } + + indent(depth); + _output << "second.isLuminance) { + _output << " maskType=\"luminance\""; + } + } + _output << ">\n"; + indent(depth + 1); + _output << "\n"; + + int contentDepth = depth + 2; + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + contentDepth = depth + 3; + } + + indent(contentDepth); + _output << "\n"; + + auto pathBounds = shapePath.getBounds(); + writeFillStyle(path, contentDepth, pathBounds.left, pathBounds.top, pathBounds.width(), + pathBounds.height()); + writeStrokeStyle(path, contentDepth, pathBounds.left, pathBounds.top, pathBounds.width(), + pathBounds.height()); + + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + } + + indent(depth + 1); + _output << "\n"; + indent(depth); + _output << "\n"; +} + +void SVGToPAGXConverter::convertLine(const SVGLine* line, int depth) { + if (!line) { + return; + } + + auto x1 = line->getX1().value(); + auto y1 = line->getY1().value(); + auto x2 = line->getX2().value(); + auto y2 = line->getY2().value(); + + Path linePath; + linePath.moveTo(x1, y1); + linePath.lineTo(x2, y2); + + if (linePath.isEmpty()) { + return; + } + + auto transformable = static_cast(line); + auto transform = transformable->getTransform(); + bool hasTransform = !transform.isIdentity(); + + auto opacityProp = line->getOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; + + std::string maskId = getMaskId(line); + if (!maskId.empty()) { + _usedMasks.insert(maskId); + } + + indent(depth); + _output << "second.isLuminance) { + _output << " maskType=\"luminance\""; + } + } + _output << ">\n"; + indent(depth + 1); + _output << "\n"; + + int contentDepth = depth + 2; + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + contentDepth = depth + 3; + } + + indent(contentDepth); + _output << "\n"; + + auto lineBounds = linePath.getBounds(); + writeFillStyle(line, contentDepth, lineBounds.left, lineBounds.top, lineBounds.width(), + lineBounds.height()); + writeStrokeStyle(line, contentDepth, lineBounds.left, lineBounds.top, lineBounds.width(), + lineBounds.height()); + + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + } + + indent(depth + 1); + _output << "\n"; + indent(depth); + _output << "\n"; +} + +void SVGToPAGXConverter::convertPoly(const SVGPoly* poly, int depth) { + if (!poly) { + return; + } + + const auto& points = poly->getPoints(); + if (points.empty()) { + return; + } + + Path polyPath; + polyPath.moveTo(points[0]); + for (size_t i = 1; i < points.size(); ++i) { + polyPath.lineTo(points[i]); + } + if (poly->tag() == SVGTag::Polygon) { + polyPath.close(); + } + + if (polyPath.isEmpty()) { + return; + } + + auto transformable = static_cast(poly); + auto transform = transformable->getTransform(); + bool hasTransform = !transform.isIdentity(); + + auto opacityProp = poly->getOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; + + std::string maskId = getMaskId(poly); + if (!maskId.empty()) { + _usedMasks.insert(maskId); + } + + indent(depth); + _output << "second.isLuminance) { + _output << " maskType=\"luminance\""; + } + } + _output << ">\n"; + indent(depth + 1); + _output << "\n"; + + int contentDepth = depth + 2; + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + contentDepth = depth + 3; + } + + indent(contentDepth); + _output << "\n"; + + auto polyBounds = polyPath.getBounds(); + writeFillStyle(poly, contentDepth, polyBounds.left, polyBounds.top, polyBounds.width(), + polyBounds.height()); + writeStrokeStyle(poly, contentDepth, polyBounds.left, polyBounds.top, polyBounds.width(), + polyBounds.height()); + + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + } + + indent(depth + 1); + _output << "\n"; + indent(depth); + _output << "\n"; +} + +void SVGToPAGXConverter::convertText(const SVGText* text, int depth) { + if (!text) { + return; + } + + const auto& textChildren = text->getTextChildren(); + if (textChildren.empty()) { + return; + } + + auto transformable = static_cast(text); + auto transform = transformable->getTransform(); + bool hasTransform = !transform.isIdentity(); + + auto opacityProp = text->getOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; + + // Get text-level x, y position + auto xList = text->getX(); + auto yList = text->getY(); + float baseX = xList.empty() ? 0.0f : std::stof(lengthToFloat(xList[0], _width)); + float baseY = yList.empty() ? 0.0f : std::stof(lengthToFloat(yList[0], _height)); + + // Get font properties from text element + auto fontFamilyProp = text->getFontFamily(); + auto fontFamilyOpt = GetPropertyValue(fontFamilyProp); + std::string fontFamily = + fontFamilyOpt.has_value() ? CleanFontFamily(fontFamilyOpt->family()) : ""; + + auto fontSizeProp = text->getFontSize(); + auto fontSizeOpt = GetPropertyValue(fontSizeProp); + float fontSize = fontSizeOpt.has_value() ? std::stof(lengthToFloat(fontSizeOpt->size(), _height)) + : 12.0f; + + // Get text-anchor property + auto textAnchorProp = text->getTextAnchor(); + auto textAnchorOpt = GetPropertyValue(textAnchorProp); + std::string textAnchor = "start"; + if (textAnchorOpt.has_value()) { + switch (textAnchorOpt->type()) { + case SVGTextAnchor::Type::Middle: + textAnchor = "middle"; + break; + case SVGTextAnchor::Type::End: + textAnchor = "end"; + break; + default: + textAnchor = "start"; + break; + } + } + + std::string maskId = getMaskId(text); + if (!maskId.empty()) { + _usedMasks.insert(maskId); + } + + indent(depth); + _output << "second.isLuminance) { + _output << " maskType=\"luminance\""; + } + } + _output << ">\n"; + + indent(depth + 1); + _output << "\n"; + + // Process each text child (TextLiteral or TSpan) + for (const auto& child : textChildren) { + auto tag = child->tag(); + if (tag == SVGTag::TextLiteral) { + auto literal = static_cast(child.get()); + const std::string& textContent = literal->getText(); + if (textContent.empty()) { + continue; + } + + indent(depth + 2); + _output << "\n"; + + indent(depth + 3); + _output << "\n"; + + writeFillStyle(text, depth + 3, 0, 0, 0, 0); + writeStrokeStyle(text, depth + 3, 0, 0, 0, 0); + + indent(depth + 2); + _output << "\n"; + } else if (tag == SVGTag::TSpan) { + auto tspan = static_cast(child.get()); + const auto& tspanChildren = tspan->getTextChildren(); + + // Get tspan-specific position (if specified) + auto tspanX = tspan->getX(); + auto tspanY = tspan->getY(); + float spanX = tspanX.empty() ? baseX : std::stof(lengthToFloat(tspanX[0], _width)); + float spanY = tspanY.empty() ? baseY : std::stof(lengthToFloat(tspanY[0], _height)); + + // Get tspan-specific font properties (inherit from text if not specified) + auto tspanFontFamilyProp = tspan->getFontFamily(); + auto tspanFontFamilyOpt = GetPropertyValue(tspanFontFamilyProp); + std::string spanFontFamily = + tspanFontFamilyOpt.has_value() ? CleanFontFamily(tspanFontFamilyOpt->family()) : fontFamily; + + auto tspanFontSizeProp = tspan->getFontSize(); + auto tspanFontSizeOpt = GetPropertyValue(tspanFontSizeProp); + float spanFontSize = tspanFontSizeOpt.has_value() + ? std::stof(lengthToFloat(tspanFontSizeOpt->size(), _height)) + : fontSize; + + // Get tspan-specific text-anchor (inherit from text if not specified) + auto tspanTextAnchorProp = tspan->getTextAnchor(); + auto tspanTextAnchorOpt = GetPropertyValue(tspanTextAnchorProp); + std::string spanTextAnchor = textAnchor; + if (tspanTextAnchorOpt.has_value()) { + switch (tspanTextAnchorOpt->type()) { + case SVGTextAnchor::Type::Middle: + spanTextAnchor = "middle"; + break; + case SVGTextAnchor::Type::End: + spanTextAnchor = "end"; + break; + default: + spanTextAnchor = "start"; + break; + } + } + + for (const auto& tspanChild : tspanChildren) { + if (tspanChild->tag() != SVGTag::TextLiteral) { + continue; + } + auto literal = static_cast(tspanChild.get()); + const std::string& textContent = literal->getText(); + if (textContent.empty()) { + continue; + } + + indent(depth + 2); + _output << "\n"; + + indent(depth + 3); + _output << "\n"; + + // Use tspan's fill/stroke if specified, otherwise inherit from text + writeFillStyle(tspan, depth + 3, 0, 0, 0, 0); + writeStrokeStyle(tspan, depth + 3, 0, 0, 0, 0); + + indent(depth + 2); + _output << "\n"; + } + } + } + + indent(depth + 1); + _output << "\n"; + indent(depth); + _output << "\n"; +} + +void SVGToPAGXConverter::writeFillStyle(const SVGNode* node, int depth, float shapeX, float shapeY, + float shapeWidth, float shapeHeight) { + auto fillProp = node->getFill(); + auto fillOpt = GetPropertyValue(fillProp); + + // Only output Fill if fill attribute is explicitly set + if (!fillOpt.has_value()) { + return; + } + + auto fillValue = fillOpt.value(); + if (fillValue.type() == SVGPaint::Type::None) { + return; + } + + auto fillRuleProp = node->getFillRule(); + auto fillRuleOpt = GetPropertyValue(fillRuleProp); + bool hasEvenOddFillRule = + fillRuleOpt.has_value() && fillRuleOpt.value().type() == SVGFillRule::Type::EvenOdd; + + if (fillValue.type() == SVGPaint::Type::IRI) { + auto iri = fillValue.iri().iri(); + if (iri.empty()) { + return; + } + + // Check if this color source should be inlined + auto usageIt = _colorSourceUsages.find(iri); + bool shouldInline = + (usageIt == _colorSourceUsages.end()) || (usageIt->second.count == 1) || + (_patterns.find(iri) != _patterns.end() && _patterns.at(iri).isObjectBoundingBox); + + if (shouldInline) { + // Check if it's a gradient + auto gradientIt = _gradients.find(iri); + if (gradientIt != _gradients.end()) { + indent(depth); + _output << "\n"; + writeInlineGradient(iri, depth + 1); + indent(depth); + _output << "\n"; + return; + } + + // Check if it's a pattern + auto patternIt = _patterns.find(iri); + if (patternIt != _patterns.end()) { + indent(depth); + _output << "\n"; + writeInlineImagePattern(patternIt->second, depth + 1, shapeX, shapeY, shapeWidth, + shapeHeight); + indent(depth); + _output << "\n"; + return; + } + } + + // Reference to shared resource + indent(depth); + _output << "\n"; + return; + } + + // Solid color + Color fillColor = Color::Black(); + if (fillValue.type() == SVGPaint::Type::Color) { + fillColor = fillValue.color().color(); + auto fillOpacityProp = node->getFillOpacity(); + auto fillOpacityOpt = GetPropertyValue(fillOpacityProp); + if (fillOpacityOpt.has_value()) { + fillColor.alpha = fillOpacityOpt.value(); + } + } + + indent(depth); + _output << "\n"; +} + +void SVGToPAGXConverter::writeStrokeStyle(const SVGNode* node, int depth, float shapeX, + float shapeY, float shapeWidth, float shapeHeight) { + auto strokeProp = node->getStroke(); + auto strokeOpt = GetPropertyValue(strokeProp); + if (!strokeOpt.has_value()) { + return; + } + + auto strokeValue = strokeOpt.value(); + if (strokeValue.type() == SVGPaint::Type::None) { + return; + } + + // Collect stroke attributes + auto strokeWidthProp = node->getStrokeWidth(); + auto strokeWidthOpt = GetPropertyValue(strokeWidthProp); + float width = 1.0f; + if (strokeWidthOpt.has_value()) { + width = std::stof(lengthToFloat(strokeWidthOpt.value(), _width)); + } + + std::string capAttr = {}; + auto lineCapProp = node->getStrokeLineCap(); + auto lineCapOpt = GetPropertyValue(lineCapProp); + if (lineCapOpt.has_value()) { + auto cap = lineCapOpt.value(); + if (cap == SVGLineCap::Round) { + capAttr = " cap=\"round\""; + } else if (cap == SVGLineCap::Square) { + capAttr = " cap=\"square\""; + } + } + + std::string joinAttr = {}; + auto lineJoinProp = node->getStrokeLineJoin(); + auto lineJoinOpt = GetPropertyValue(lineJoinProp); + if (lineJoinOpt.has_value()) { + auto joinType = lineJoinOpt.value().type(); + if (joinType == SVGLineJoin::Type::Round) { + joinAttr = " join=\"round\""; + } else if (joinType == SVGLineJoin::Type::Bevel) { + joinAttr = " join=\"bevel\""; + } + } + + std::string miterAttr = {}; + auto miterLimitProp = node->getStrokeMiterLimit(); + auto miterLimitOpt = GetPropertyValue(miterLimitProp); + if (miterLimitOpt.has_value() && miterLimitOpt.value() != 4.0f) { + std::ostringstream ss; + ss << " miterLimit=\"" << miterLimitOpt.value() << "\""; + miterAttr = ss.str(); + } + + std::string dashesAttr = {}; + auto dashArrayProp = node->getStrokeDashArray(); + auto dashArrayOpt = GetPropertyValue(dashArrayProp); + if (dashArrayOpt.has_value()) { + auto& dashes = dashArrayOpt.value().dashArray(); + if (!dashes.empty()) { + std::ostringstream ss; + ss << " dashes=\""; + for (size_t i = 0; i < dashes.size(); ++i) { + if (i > 0) { + ss << ","; + } + ss << lengthToFloat(dashes[i], _width); + } + ss << "\""; + dashesAttr = ss.str(); + } + } + + std::string dashOffsetAttr = {}; + auto dashOffsetProp = node->getStrokeDashOffset(); + auto dashOffsetOpt = GetPropertyValue(dashOffsetProp); + if (dashOffsetOpt.has_value()) { + float offset = std::stof(lengthToFloat(dashOffsetOpt.value(), _width)); + if (offset != 0.0f) { + std::ostringstream ss; + ss << " dashOffset=\"" << offset << "\""; + dashOffsetAttr = ss.str(); + } + } + + // Handle IRI reference (gradient or pattern) + if (strokeValue.type() == SVGPaint::Type::IRI) { + auto iri = strokeValue.iri().iri(); + if (iri.empty()) { + return; + } + + // Check if this color source should be inlined + auto usageIt = _colorSourceUsages.find(iri); + bool shouldInline = + (usageIt == _colorSourceUsages.end()) || (usageIt->second.count == 1) || + (_patterns.find(iri) != _patterns.end() && _patterns.at(iri).isObjectBoundingBox); + + if (shouldInline) { + // Check if it's a gradient + auto gradientIt = _gradients.find(iri); + if (gradientIt != _gradients.end()) { + indent(depth); + _output << "\n"; + writeInlineGradient(iri, depth + 1); + indent(depth); + _output << "\n"; + return; + } + + // Check if it's a pattern + auto patternIt = _patterns.find(iri); + if (patternIt != _patterns.end()) { + indent(depth); + _output << "\n"; + writeInlineImagePattern(patternIt->second, depth + 1, shapeX, shapeY, shapeWidth, + shapeHeight); + indent(depth); + _output << "\n"; + return; + } + } + + // Reference to shared resource + indent(depth); + _output << "\n"; + return; + } + + // Solid color + indent(depth); + _output << "getStrokeOpacity(); + auto strokeOpacityOpt = GetPropertyValue(strokeOpacityProp); + if (strokeOpacityOpt.has_value()) { + color.alpha = strokeOpacityOpt.value(); + } + _output << " color=\"" << colorToHex(color) << "\""; + } + _output << " width=\"" << width << "\"" << capAttr << joinAttr << miterAttr << dashesAttr + << dashOffsetAttr << "/>\n"; +} + +void SVGToPAGXConverter::writeInlineGradient(const std::string& gradientId, int depth) { + auto it = _gradients.find(gradientId); + if (it == _gradients.end()) { + return; + } + + auto node = it->second; + auto tag = node->tag(); + + if (tag == SVGTag::LinearGradient) { + auto gradient = static_cast(node); + indent(depth); + _output << "getX1(), _width) << "\""; + _output << " startY=\"" << lengthToFloat(gradient->getY1(), _height) << "\""; + _output << " endX=\"" << lengthToFloat(gradient->getX2(), _width) << "\""; + _output << " endY=\"" << lengthToFloat(gradient->getY2(), _height) << "\""; + _output << ">\n"; + + auto container = static_cast(node); + for (const auto& child : container->getChildren()) { + if (child->tag() == SVGTag::Stop) { + auto stop = static_cast(child.get()); + indent(depth + 1); + auto offset = stop->getOffset(); + float offsetValue = offset.value(); + if (offset.unit() == SVGLength::Unit::Percentage) { + offsetValue = offsetValue / 100.0f; + } + _output << "getStopColor(); + auto stopColorOpt = GetPropertyValue(stopColorProp); + if (stopColorOpt.has_value()) { + auto color = stopColorOpt->color(); + auto opacityProp = stop->getStopOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + float alpha = opacityOpt.has_value() ? opacityOpt.value() : 1.0f; + Color c = color; + c.alpha = alpha; + _output << " color=\"" << colorToHex(c) << "\""; + } + _output << "/>\n"; + } + } + + indent(depth); + _output << "\n"; + } else if (tag == SVGTag::RadialGradient) { + auto gradient = static_cast(node); + indent(depth); + _output << "getCx(), _width) << "\""; + _output << " centerY=\"" << lengthToFloat(gradient->getCy(), _height) << "\""; + auto r = gradient->getR(); + _output << " radius=\"" << lengthToFloat(r, std::max(_width, _height)) << "\""; + _output << ">\n"; + + auto container = static_cast(node); + for (const auto& child : container->getChildren()) { + if (child->tag() == SVGTag::Stop) { + auto stop = static_cast(child.get()); + indent(depth + 1); + auto offset = stop->getOffset(); + float offsetValue = offset.value(); + if (offset.unit() == SVGLength::Unit::Percentage) { + offsetValue = offsetValue / 100.0f; + } + _output << "getStopColor(); + auto stopColorOpt = GetPropertyValue(stopColorProp); + if (stopColorOpt.has_value()) { + auto color = stopColorOpt->color(); + auto opacityProp = stop->getStopOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + float alpha = opacityOpt.has_value() ? opacityOpt.value() : 1.0f; + Color c = color; + c.alpha = alpha; + _output << " color=\"" << colorToHex(c) << "\""; + } + _output << "/>\n"; + } + } + + indent(depth); + _output << "\n"; + } +} + +void SVGToPAGXConverter::writeInlineImagePattern(const PatternInfo& patternInfo, int depth, + float shapeX, float shapeY, float shapeWidth, + float shapeHeight) { + indent(depth); + _output << " 0 && + patternInfo.patternHeight > 0 && shapeWidth > 0 && shapeHeight > 0) { + float tileWidth = shapeWidth * patternInfo.patternWidth; + float tileHeight = shapeHeight * patternInfo.patternHeight; + + if (patternInfo.imageWidth > 0 && patternInfo.imageHeight > 0 && tileWidth > 0 && + tileHeight > 0) { + float scaleX = tileWidth / patternInfo.imageWidth; + float scaleY = tileHeight / patternInfo.imageHeight; + // Translate to align pattern with shape's top-left corner + float tx = shapeX; + float ty = shapeY; + _output << " matrix=\"" << scaleX << ",0,0," << scaleY << "," << tx << "," << ty << "\""; + } + } + + _output << "/>\n"; +} + +std::string SVGToPAGXConverter::colorToHex(const Color& color) const { + auto toHex = [](float value) -> int { return static_cast(std::round(value * 255)); }; + + std::ostringstream ss; + ss << "#" << std::uppercase << std::hex << std::setfill('0'); + ss << std::setw(2) << toHex(color.red); + ss << std::setw(2) << toHex(color.green); + ss << std::setw(2) << toHex(color.blue); + + if (color.alpha < 1.0f) { + ss << std::setw(2) << toHex(color.alpha); + } + + return ss.str(); +} + +std::string SVGToPAGXConverter::matrixToString(const Matrix& matrix) const { + std::ostringstream ss; + ss << matrix.getScaleX() << "," << matrix.getSkewY() << "," << matrix.getSkewX() << "," + << matrix.getScaleY() << "," << matrix.getTranslateX() << "," << matrix.getTranslateY(); + return ss.str(); +} + +std::string SVGToPAGXConverter::lengthToFloat(const SVGLength& length, float containerSize) const { + float value = length.value(); + auto unit = length.unit(); + + switch (unit) { + case SVGLength::Unit::Percentage: + value = value * containerSize / 100.0f; + break; + case SVGLength::Unit::PX: + case SVGLength::Unit::Number: + case SVGLength::Unit::Unknown: + default: + break; + } + + std::ostringstream ss; + ss << value; + return ss.str(); +} + +void SVGToPAGXConverter::indent(int depth) { + for (int i = 0; i < depth; ++i) { + _output << " "; + } +} + +int SVGToPAGXConverter::countRenderableChildren(const SVGContainer* container) const { + int count = 0; + for (const auto& child : container->getChildren()) { + auto tag = child->tag(); + if (tag == SVGTag::Defs || tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient || + tag == SVGTag::Stop || tag == SVGTag::ClipPath || tag == SVGTag::Mask || + tag == SVGTag::Filter || tag == SVGTag::G || tag == SVGTag::Svg) { + continue; + } + auto visibilityProp = child->getVisibility(); + auto visibilityOpt = GetPropertyValue(visibilityProp); + if (visibilityOpt.has_value() && visibilityOpt.value().type() == SVGVisibility::Type::Hidden) { + continue; + } + count++; + } + return count; +} + +bool SVGToPAGXConverter::hasOnlyFill(const SVGNode* node) const { + auto fillProp = node->getFill(); + auto fillOpt = GetPropertyValue(fillProp); + bool hasFill = fillOpt.has_value() && fillOpt->type() != SVGPaint::Type::None; + + auto strokeProp = node->getStroke(); + auto strokeOpt = GetPropertyValue(strokeProp); + bool hasStroke = strokeOpt.has_value() && strokeOpt->type() != SVGPaint::Type::None; + + return hasFill && !hasStroke; +} + +bool SVGToPAGXConverter::hasOnlyStroke(const SVGNode* node) const { + auto fillProp = node->getFill(); + auto fillOpt = GetPropertyValue(fillProp); + bool hasFill = fillOpt.has_value() && fillOpt->type() != SVGPaint::Type::None; + + auto strokeProp = node->getStroke(); + auto strokeOpt = GetPropertyValue(strokeProp); + bool hasStroke = strokeOpt.has_value() && strokeOpt->type() != SVGPaint::Type::None; + + return !hasFill && hasStroke; +} + +bool SVGToPAGXConverter::areRectsEqual(const SVGRect* a, const SVGRect* b) const { + if (!a || !b) { + return false; + } + + auto ax = lengthToFloat(a->getX(), _width); + auto ay = lengthToFloat(a->getY(), _height); + auto aw = lengthToFloat(a->getWidth(), _width); + auto ah = lengthToFloat(a->getHeight(), _height); + + auto bx = lengthToFloat(b->getX(), _width); + auto by = lengthToFloat(b->getY(), _height); + auto bw = lengthToFloat(b->getWidth(), _width); + auto bh = lengthToFloat(b->getHeight(), _height); + + if (ax != bx || ay != by || aw != bw || ah != bh) { + return false; + } + + float arx = 0, ary = 0, brx = 0, bry = 0; + if (a->getRx().has_value()) { + arx = std::stof(lengthToFloat(*a->getRx(), _width)); + } + if (a->getRy().has_value()) { + ary = std::stof(lengthToFloat(*a->getRy(), _height)); + } + if (b->getRx().has_value()) { + brx = std::stof(lengthToFloat(*b->getRx(), _width)); + } + if (b->getRy().has_value()) { + bry = std::stof(lengthToFloat(*b->getRy(), _height)); + } + + return arx == brx && ary == bry; +} + +void SVGToPAGXConverter::convertChildren(const std::vector>& children, + int depth) { + size_t i = 0; + while (i < children.size()) { + auto& child = children[i]; + auto tag = child->tag(); + + // Skip non-renderable elements + if (tag == SVGTag::Defs || tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient || + tag == SVGTag::Stop || tag == SVGTag::ClipPath || tag == SVGTag::Mask || + tag == SVGTag::Filter || tag == SVGTag::Pattern) { + i++; + continue; + } + + auto visibilityProp = child->getVisibility(); + auto visibilityOpt = GetPropertyValue(visibilityProp); + if (visibilityOpt.has_value() && visibilityOpt.value().type() == SVGVisibility::Type::Hidden) { + i++; + continue; + } + + // Check for consecutive identical rects (fill-only followed by stroke-only) + if (tag == SVGTag::Rect && hasOnlyFill(child.get()) && i + 1 < children.size()) { + auto& next = children[i + 1]; + if (next->tag() == SVGTag::Rect && hasOnlyStroke(next.get())) { + auto fillRect = static_cast(child.get()); + auto strokeRect = static_cast(next.get()); + if (areRectsEqual(fillRect, strokeRect)) { + convertRectWithStroke(fillRect, strokeRect, depth); + i += 2; + continue; + } + } + } + + // Normal processing + if (tag == SVGTag::G || tag == SVGTag::Svg) { + convertContainer(static_cast(child.get()), depth); + } else { + convertNode(child.get(), depth, false); + } + i++; + } +} + +void SVGToPAGXConverter::convertRectWithStroke(const SVGRect* fillRect, const SVGRect* strokeRect, + int depth) { + float x = std::stof(lengthToFloat(fillRect->getX(), _width)); + float y = std::stof(lengthToFloat(fillRect->getY(), _height)); + float width = std::stof(lengthToFloat(fillRect->getWidth(), _width)); + float height = std::stof(lengthToFloat(fillRect->getHeight(), _height)); + + if (width <= 0 || height <= 0) { + return; + } + + float centerX = x + width / 2.0f; + float centerY = y + height / 2.0f; + + float rx = 0; + float ry = 0; + auto rxOpt = fillRect->getRx(); + auto ryOpt = fillRect->getRy(); + if (rxOpt.has_value()) { + rx = std::stof(lengthToFloat(*rxOpt, _width)); + } + if (ryOpt.has_value()) { + ry = std::stof(lengthToFloat(*ryOpt, _height)); + } + if (rx > 0 && ry <= 0) { + ry = rx; + } + if (ry > 0 && rx <= 0) { + rx = ry; + } + + auto transformable = static_cast(fillRect); + auto transform = transformable->getTransform(); + bool hasTransform = !transform.isIdentity(); + + auto opacityProp = fillRect->getOpacity(); + auto opacityOpt = GetPropertyValue(opacityProp); + bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; + + indent(depth); + _output << "\n"; + indent(depth + 1); + _output << "\n"; + + int contentDepth = depth + 2; + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + contentDepth = depth + 3; + } + + indent(contentDepth); + _output << " 0 || ry > 0) { + float roundness = std::min(rx, ry); + _output << " roundness=\"" << roundness << "\""; + } + _output << "/>\n"; + + writeFillStyle(fillRect, contentDepth, x, y, width, height); + writeStrokeStyle(strokeRect, contentDepth, x, y, width, height); + + if (hasTransform || hasOpacity) { + indent(depth + 2); + _output << "\n"; + } + + indent(depth + 1); + _output << "\n"; + indent(depth); + _output << "\n"; +} + +std::string SVGToPAGXConverter::getMaskId(const SVGNode* node) const { + if (!node) { + return ""; + } + + auto maskProp = node->getMask(); + auto maskOpt = GetPropertyValue(maskProp); + if (!maskOpt.has_value() || maskOpt->type() != SVGFuncIRI::Type::IRI) { + return ""; + } + + auto maskIri = maskOpt->iri().iri(); + if (maskIri.empty()) { + return ""; + } + + // Check if this mask exists in our collected masks + if (_masks.find(maskIri) == _masks.end()) { + return ""; + } + + return maskIri; +} + +void SVGToPAGXConverter::writeMaskLayers(int depth) { + // Write mask layers that are referenced by content layers + for (const auto& [maskId, maskInfo] : _masks) { + // Only output masks that will be used + if (_usedMasks.find(maskId) == _usedMasks.end()) { + continue; + } + + auto maskNode = static_cast(maskInfo.maskNode); + auto container = static_cast(maskNode); + if (!container->hasChildren()) { + continue; + } + + indent(depth); + _output << "\n"; + indent(depth + 1); + _output << "\n"; + + // Convert mask children - we need to output shapes directly without wrapping in Layer + for (const auto& child : container->getChildren()) { + auto tag = child->tag(); + if (tag == SVGTag::Rect) { + auto rect = static_cast(child.get()); + float x = std::stof(lengthToFloat(rect->getX(), _width)); + float y = std::stof(lengthToFloat(rect->getY(), _height)); + float width = std::stof(lengthToFloat(rect->getWidth(), _width)); + float height = std::stof(lengthToFloat(rect->getHeight(), _height)); + float centerX = x + width / 2.0f; + float centerY = y + height / 2.0f; + + indent(depth + 2); + _output << "\n"; + writeFillStyle(rect, depth + 2, x, y, width, height); + } else if (tag == SVGTag::Circle) { + auto circle = static_cast(child.get()); + float cx = std::stof(lengthToFloat(circle->getCx(), _width)); + float cy = std::stof(lengthToFloat(circle->getCy(), _height)); + float r = std::stof(lengthToFloat(circle->getR(), std::max(_width, _height))); + + indent(depth + 2); + _output << "\n"; + writeFillStyle(circle, depth + 2, cx - r, cy - r, r * 2, r * 2); + } else if (tag == SVGTag::Ellipse) { + auto ellipse = static_cast(child.get()); + float cx = std::stof(lengthToFloat(ellipse->getCx(), _width)); + float cy = std::stof(lengthToFloat(ellipse->getCy(), _height)); + auto rxOpt = ellipse->getRx(); + auto ryOpt = ellipse->getRy(); + float rx = rxOpt.has_value() ? std::stof(lengthToFloat(*rxOpt, _width)) : 0; + float ry = ryOpt.has_value() ? std::stof(lengthToFloat(*ryOpt, _height)) : 0; + + indent(depth + 2); + _output << "\n"; + writeFillStyle(ellipse, depth + 2, cx - rx, cy - ry, rx * 2, ry * 2); + } else if (tag == SVGTag::Path) { + auto path = static_cast(child.get()); + auto shapePath = path->getShapePath(); + if (!shapePath.isEmpty()) { + indent(depth + 2); + _output << "\n"; + auto bounds = shapePath.getBounds(); + writeFillStyle(path, depth + 2, bounds.left, bounds.top, bounds.width(), bounds.height()); + } + } + } + + indent(depth + 1); + _output << "\n"; + indent(depth); + _output << "\n"; + } +} + +} // namespace pagx diff --git a/pagx/src/svg/SVGToPAGXConverter.h b/pagx/src/svg/SVGToPAGXConverter.h new file mode 100644 index 0000000000..287e885e4e --- /dev/null +++ b/pagx/src/svg/SVGToPAGXConverter.h @@ -0,0 +1,137 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "tgfx/core/Color.h" +#include "tgfx/core/Matrix.h" +#include "tgfx/svg/SVGDOM.h" +#include "tgfx/svg/node/SVGCircle.h" +#include "tgfx/svg/node/SVGContainer.h" +#include "tgfx/svg/node/SVGEllipse.h" +#include "tgfx/svg/node/SVGGradient.h" +#include "tgfx/svg/node/SVGLine.h" +#include "tgfx/svg/node/SVGMask.h" +#include "tgfx/svg/node/SVGNode.h" +#include "tgfx/svg/node/SVGPath.h" +#include "tgfx/svg/node/SVGPoly.h" +#include "tgfx/svg/node/SVGRect.h" +#include "tgfx/svg/node/SVGRoot.h" +#include "tgfx/svg/node/SVGText.h" + +namespace pagx { + +using namespace tgfx; + +struct PatternInfo { + std::string patternId = {}; + std::string imageId = {}; + std::string imageData = {}; + float imageWidth = 0.0f; + float imageHeight = 0.0f; + float patternWidth = 0.0f; // in objectBoundingBox units (0-1) + float patternHeight = 0.0f; // in objectBoundingBox units (0-1) + bool isObjectBoundingBox = true; +}; + +struct ColorSourceUsage { + int count = 0; + std::string type = {}; // "gradient" or "pattern" +}; + +struct MaskInfo { + std::string maskId = {}; + const SVGNode* maskNode = nullptr; + bool isLuminance = false; +}; + +class SVGToPAGXConverter { + public: + explicit SVGToPAGXConverter(const std::shared_ptr& svgDOM); + + std::string convert(); + + private: + void writeHeader(); + void writeResources(); + void writeLayers(); + + void convertNode(const SVGNode* node, int depth, bool needScopeIsolation); + void convertContainer(const SVGContainer* container, int depth); + void convertRect(const SVGRect* rect, int depth); + void convertCircle(const SVGCircle* circle, int depth); + void convertEllipse(const SVGEllipse* ellipse, int depth); + void convertPath(const SVGPath* path, int depth); + void convertLine(const SVGLine* line, int depth); + void convertPoly(const SVGPoly* poly, int depth); + void convertText(const SVGText* text, int depth); + + void writeFillStyle(const SVGNode* node, int depth, float shapeX = 0, float shapeY = 0, + float shapeWidth = 0, float shapeHeight = 0); + void writeStrokeStyle(const SVGNode* node, int depth, float shapeX = 0, float shapeY = 0, + float shapeWidth = 0, float shapeHeight = 0); + + void writeInlineGradient(const std::string& gradientId, int depth); + void writeInlineImagePattern(const PatternInfo& patternInfo, int depth, float shapeX, + float shapeY, float shapeWidth, float shapeHeight); + + std::string colorToHex(const Color& color) const; + std::string colorToString(const SVGPaint& paint) const; + std::string matrixToString(const Matrix& matrix) const; + std::string lengthToFloat(const SVGLength& length, float containerSize) const; + + void indent(int depth); + void writeAttribute(const std::string& name, const std::string& value); + void writeAttribute(const std::string& name, float value); + + void collectGradients(); + void collectPatterns(); + void collectMasks(); + void collectUsedMasks(const SVGNode* node); + void countColorSourceUsages(); + void countColorSourceUsagesFromNode(const SVGNode* node); + int countRenderableChildren(const SVGContainer* container) const; + + bool hasOnlyFill(const SVGNode* node) const; + bool hasOnlyStroke(const SVGNode* node) const; + bool areRectsEqual(const SVGRect* a, const SVGRect* b) const; + void convertChildren(const std::vector>& children, int depth); + void convertRectWithStroke(const SVGRect* fillRect, const SVGRect* strokeRect, int depth); + + std::string getMaskId(const SVGNode* node) const; + void writeMaskLayers(int depth); + + std::shared_ptr _svgDOM = nullptr; + std::ostringstream _output = {}; + float _width = 0.0f; + float _height = 0.0f; + std::map _gradients = {}; + std::map _patterns = {}; + std::map _images = {}; + std::map _colorSourceUsages = {}; + std::map _masks = {}; + std::set _usedMasks = {}; +}; + +} // namespace pagx diff --git a/pagx/viewer/.gitignore b/pagx/viewer/.gitignore new file mode 100644 index 0000000000..dcf8ac2c29 --- /dev/null +++ b/pagx/viewer/.gitignore @@ -0,0 +1,9 @@ +# Build outputs +wasm-mt/ +dist/ + +# Build cache +script/build-pagx-viewer/ +script/wasm-mt/ +script/.build.lock +script/.*.md5 diff --git a/pagx/viewer/CMakeLists.txt b/pagx/viewer/CMakeLists.txt new file mode 100644 index 0000000000..e2dda6e3c9 --- /dev/null +++ b/pagx/viewer/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.13) +project(PAGXViewer) + +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 17) + +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") +endif () + +# Set TGFX_DIR for libpag structure (tgfx is in third_party/tgfx) +if (NOT TGFX_DIR) + set(TGFX_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/tgfx) +endif () +get_filename_component(TGFX_DIR "${TGFX_DIR}" REALPATH) + +# Add tgfx and pagx as dependencies +if (NOT TARGET tgfx) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + set(TGFX_BUILD_SVG ON CACHE BOOL "" FORCE) + set(TGFX_BUILD_LAYERS ON CACHE BOOL "" FORCE) + add_subdirectory(${TGFX_DIR} ${CMAKE_CURRENT_BINARY_DIR}/tgfx) +endif () + +if (NOT TARGET pagx) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/pagx) +endif () + +# For Emscripten builds, pagx needs the same compile options as tgfx +if (DEFINED EMSCRIPTEN) + target_compile_options(pagx PRIVATE -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0) +endif () + +file(GLOB_RECURSE PAGX_VIEWER_FILES src/*.cpp) + +if (DEFINED EMSCRIPTEN) + add_executable(pagx-viewer ${PAGX_VIEWER_FILES}) + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0) + list(APPEND PAGX_VIEWER_LINK_OPTIONS --no-entry -lembind -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 -sEXPORT_NAME='PAGXWasm' -sWASM=1 + -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['GL','HEAPU8'] -sMODULARIZE=1 + -sENVIRONMENT=web,worker -sEXPORT_ES6=1) + set(unsupported_emsdk_versions "4.0.11") + foreach (unsupported_version IN LISTS unsupported_emsdk_versions) + if (${EMSCRIPTEN_VERSION} VERSION_EQUAL ${unsupported_version}) + message(FATAL_ERROR "Emscripten version ${EMSCRIPTEN_VERSION} is not supported.") + endif () + endforeach () + if (EMSCRIPTEN_PTHREADS) + list(APPEND PAGX_VIEWER_LINK_OPTIONS -sUSE_PTHREADS=1 -sINITIAL_MEMORY=32MB -sALLOW_MEMORY_GROWTH=1 + -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency + -sEXIT_RUNTIME=0 -sINVOKE_RUN=0 -sMALLOC=mimalloc) + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -fPIC -pthread) + else () + list(APPEND PAGX_VIEWER_LINK_OPTIONS -sALLOW_MEMORY_GROWTH=1) + endif () + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -O0 -g3) + list(APPEND PAGX_VIEWER_LINK_OPTIONS -O0 -g3 -sSAFE_HEAP=1 -Wno-limited-postlink-optimizations) + else () + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -Oz) + list(APPEND PAGX_VIEWER_LINK_OPTIONS -Oz) + endif () +else () + add_library(pagx-viewer SHARED ${PAGX_VIEWER_FILES}) +endif () + +target_compile_options(pagx-viewer PUBLIC ${PAGX_VIEWER_COMPILE_OPTIONS}) +target_link_options(pagx-viewer PUBLIC ${PAGX_VIEWER_LINK_OPTIONS}) +target_link_libraries(pagx-viewer pagx) diff --git a/pagx/viewer/index.css b/pagx/viewer/index.css new file mode 100644 index 0000000000..e206f2d853 --- /dev/null +++ b/pagx/viewer/index.css @@ -0,0 +1,167 @@ +html, body { + padding: 0; + border: 0; + margin: 0; + height: 100%; + overflow: hidden; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; +} + +.container { + position: relative; + overflow: hidden; + margin: auto; + width: 100%; + height: 100%; + background: #1a1a2e; +} + +.canvas { + cursor: grab; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 1; +} + +.canvas:active { + cursor: grabbing; +} + +.drop-zone { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + background: rgba(26, 26, 46, 0.95); + transition: background 0.3s ease; +} + +.drop-zone.drag-over { + background: rgba(26, 26, 46, 0.98); +} + +.drop-zone.drag-over .drop-zone-content { + border-color: #4a90d9; + transform: scale(1.02); +} + +.drop-zone.hidden { + display: none; +} + +.drop-zone-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 60px 80px; + border: 2px dashed #555; + border-radius: 16px; + background: rgba(255, 255, 255, 0.02); + transition: all 0.3s ease; +} + +.drop-icon { + width: 64px; + height: 64px; + color: #888; + margin-bottom: 20px; +} + +.drop-text { + color: #ccc; + font-size: 20px; + margin: 0 0 8px 0; +} + +.drop-subtext { + color: #666; + font-size: 14px; + margin: 0 0 16px 0; +} + +.file-btn { + padding: 12px 32px; + font-size: 16px; + color: #fff; + background: #4a90d9; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s ease; +} + +.file-btn:hover { + background: #5a9ee9; +} + +.file-btn:active { + background: #3a80c9; +} + +.toolbar { + position: absolute; + top: 16px; + left: 16px; + z-index: 50; + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + background: rgba(0, 0, 0, 0.6); + border-radius: 8px; + backdrop-filter: blur(8px); +} + +.toolbar.hidden { + display: none; +} + +.toolbar-btn { + width: 36px; + height: 36px; + padding: 6px; + background: transparent; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s ease; +} + +.toolbar-btn svg { + width: 100%; + height: 100%; + color: #ccc; +} + +.toolbar-btn:hover { + background: rgba(255, 255, 255, 0.1); +} + +.toolbar-btn:hover svg { + color: #fff; +} + +.toolbar-btn:active { + background: rgba(255, 255, 255, 0.2); +} + +.file-name { + color: #aaa; + font-size: 14px; + padding-left: 12px; + padding-right: 8px; + margin-left: 4px; + border-left: 1px solid rgba(255, 255, 255, 0.2); + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/pagx/viewer/index.html b/pagx/viewer/index.html new file mode 100644 index 0000000000..1dd078cdf5 --- /dev/null +++ b/pagx/viewer/index.html @@ -0,0 +1,45 @@ + + + + + + + + PAGX Viewer + + + +
+ +
+
+ + + + + +

Drag & Drop PAGX file here

+

or

+ + +
+
+ +
+ + + diff --git a/pagx/viewer/index.ts b/pagx/viewer/index.ts new file mode 100644 index 0000000000..21b11879fd --- /dev/null +++ b/pagx/viewer/index.ts @@ -0,0 +1,428 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import { PAGXModule, PAGXView } from './types'; +import PAGXWasm from './wasm-mt/pagx-viewer'; + +const MIN_ZOOM = 0.001; +const MAX_ZOOM = 1000.0; + +class ViewerState { + module: PAGXModule | null = null; + pagxView: PAGXView | null = null; + animationFrameId: number | null = null; + isPageVisible: boolean = true; + resized: boolean = false; + zoom: number = 1.0; + offsetX: number = 0; + offsetY: number = 0; +} + +enum ScaleGestureState { + SCALE_START = 0, + SCALE_CHANGE = 1, + SCALE_END = 2, +} + +enum ScrollGestureState { + SCROLL_START = 0, + SCROLL_CHANGE = 1, + SCROLL_END = 2, +} + +enum DeviceType { + TOUCH = 0, + MOUSE = 1, +} + +class GestureManager { + private scaleY = 1.0; + private pinchTimeout = 150; + private timer: number | undefined; + private scaleStartZoom = 1.0; + private lastEventTime = 0; + private lastDeltaY = 0; + private timeThreshold = 50; + private deltaYThreshold = 50; + private deltaYChangeThreshold = 10; + private mouseWheelRatio = 800; + private touchWheelRatio = 100; + + public zoom = 1.0; + public offsetX = 0; + public offsetY = 0; + + private handleScrollEvent( + event: WheelEvent, + state: ScrollGestureState, + viewerState: ViewerState, + ) { + if (state === ScrollGestureState.SCROLL_CHANGE) { + this.scaleStartZoom = this.zoom; + this.scaleY = 1.0; + if (event.shiftKey && event.deltaX === 0 && event.deltaY !== 0) { + this.offsetX -= event.deltaY * window.devicePixelRatio; + } else { + this.offsetX -= event.deltaX * window.devicePixelRatio; + this.offsetY -= event.deltaY * window.devicePixelRatio; + } + viewerState.pagxView?.updateZoomScaleAndOffset(this.zoom, this.offsetX, this.offsetY); + } + } + + private handleScaleEvent( + event: WheelEvent, + state: ScaleGestureState, + canvas: HTMLElement, + viewerState: ViewerState, + ) { + if (state === ScaleGestureState.SCALE_START) { + this.scaleY = 1.0; + this.scaleStartZoom = this.zoom; + } + if (state === ScaleGestureState.SCALE_CHANGE) { + const rect = canvas.getBoundingClientRect(); + const pixelX = (event.clientX - rect.left) * window.devicePixelRatio; + const pixelY = (event.clientY - rect.top) * window.devicePixelRatio; + const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.scaleStartZoom * this.scaleY)); + this.offsetX = (this.offsetX - pixelX) * (newZoom / this.zoom) + pixelX; + this.offsetY = (this.offsetY - pixelY) * (newZoom / this.zoom) + pixelY; + this.zoom = newZoom; + } + if (state === ScaleGestureState.SCALE_END) { + this.scaleY = 1.0; + this.scaleStartZoom = this.zoom; + } + viewerState.pagxView?.updateZoomScaleAndOffset(this.zoom, this.offsetX, this.offsetY); + } + + public clearState() { + this.scaleY = 1.0; + this.timer = undefined; + } + + public resetTransform(viewerState: ViewerState) { + this.zoom = 1.0; + this.offsetX = 0; + this.offsetY = 0; + this.clearState(); + viewerState.pagxView?.updateZoomScaleAndOffset(this.zoom, this.offsetX, this.offsetY); + } + + private resetScrollTimeout( + event: WheelEvent, + viewerState: ViewerState, + ) { + clearTimeout(this.timer); + this.timer = window.setTimeout(() => { + this.timer = undefined; + this.handleScrollEvent(event, ScrollGestureState.SCROLL_END, viewerState); + this.clearState(); + }, this.pinchTimeout); + } + + private resetScaleTimeout( + event: WheelEvent, + canvas: HTMLElement, + viewerState: ViewerState, + ) { + clearTimeout(this.timer); + this.timer = window.setTimeout(() => { + this.timer = undefined; + this.handleScaleEvent(event, ScaleGestureState.SCALE_END, canvas, viewerState); + this.clearState(); + }, this.pinchTimeout); + } + + private getDeviceType(event: WheelEvent): DeviceType { + const now = Date.now(); + const timeDifference = now - this.lastEventTime; + const deltaYChange = Math.abs(event.deltaY - this.lastDeltaY); + let isTouchpad = false; + if (event.deltaMode === event.DOM_DELTA_PIXEL && timeDifference < this.timeThreshold) { + if (Math.abs(event.deltaY) < this.deltaYThreshold && deltaYChange < this.deltaYChangeThreshold) { + isTouchpad = true; + } + } + this.lastEventTime = now; + this.lastDeltaY = event.deltaY; + return isTouchpad ? DeviceType.TOUCH : DeviceType.MOUSE; + } + + public onWheel(event: WheelEvent, canvas: HTMLElement, viewerState: ViewerState) { + const deviceType = this.getDeviceType(event); + let wheelRatio = (deviceType === DeviceType.MOUSE ? this.mouseWheelRatio : this.touchWheelRatio); + if (!event.deltaY || (!event.ctrlKey && !event.metaKey)) { + this.resetScrollTimeout(event, viewerState); + this.handleScrollEvent(event, ScrollGestureState.SCROLL_CHANGE, viewerState); + } else { + this.scaleY *= Math.exp(-(event.deltaY) / wheelRatio); + if (!this.timer) { + this.resetScaleTimeout(event, canvas, viewerState); + this.handleScaleEvent(event, ScaleGestureState.SCALE_START, canvas, viewerState); + } else { + this.resetScaleTimeout(event, canvas, viewerState); + this.handleScaleEvent(event, ScaleGestureState.SCALE_CHANGE, canvas, viewerState); + } + } + } +} + +const viewerState = new ViewerState(); +const gestureManager = new GestureManager(); +let animationLoopRunning = false; + +function updateSize() { + if (!viewerState.pagxView) { + return; + } + viewerState.resized = false; + const canvas = document.getElementById('pagx-canvas') as HTMLCanvasElement; + const container = document.getElementById('container') as HTMLDivElement; + const screenRect = container.getBoundingClientRect(); + const scaleFactor = window.devicePixelRatio; + canvas.width = screenRect.width * scaleFactor; + canvas.height = screenRect.height * scaleFactor; + canvas.style.width = screenRect.width + "px"; + canvas.style.height = screenRect.height + "px"; + viewerState.pagxView.updateSize(); +} + +function draw() { + viewerState.pagxView?.draw(); +} + +function animationLoop() { + if (animationLoopRunning) { + return; + } + animationLoopRunning = true; + const frame = () => { + if (!viewerState.pagxView || !viewerState.isPageVisible) { + animationLoopRunning = false; + viewerState.animationFrameId = null; + return; + } + draw(); + viewerState.animationFrameId = requestAnimationFrame(frame); + }; + viewerState.animationFrameId = requestAnimationFrame(frame); +} + +function handleVisibilityChange() { + viewerState.isPageVisible = !document.hidden; + if (viewerState.isPageVisible && viewerState.animationFrameId === null) { + animationLoop(); + } +} + +function setupVisibilityListeners() { + document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener('beforeunload', () => { + if (viewerState.animationFrameId !== null) { + cancelAnimationFrame(viewerState.animationFrameId); + viewerState.animationFrameId = null; + } + }); +} + +function bindCanvasEvents(canvas: HTMLElement) { + canvas.addEventListener('wheel', (e: WheelEvent) => { + e.preventDefault(); + gestureManager.onWheel(e, canvas, viewerState); + }, { passive: false }); +} + +async function loadPAGXFile(file: File) { + const dropZone = document.getElementById('drop-zone') as HTMLDivElement; + const toolbar = document.getElementById('toolbar') as HTMLDivElement; + const fileName = document.getElementById('file-name') as HTMLSpanElement; + const dropText = dropZone.querySelector('.drop-text') as HTMLParagraphElement; + const originalText = dropText.textContent || ''; + dropText.textContent = 'Loading...'; + await new Promise(resolve => setTimeout(resolve, 10)); + try { + const arrayBuffer = await file.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + viewerState.pagxView?.loadPAGX(uint8Array); + gestureManager.resetTransform(viewerState); + updateSize(); + dropZone.classList.add('hidden'); + toolbar.classList.remove('hidden'); + fileName.textContent = file.name; + } catch (error) { + console.error('Failed to load PAGX file:', error); + dropText.textContent = originalText; + alert('Failed to load PAGX file. Please check the file format.'); + } +} + +function setupDragAndDrop() { + const dropZone = document.getElementById('drop-zone') as HTMLDivElement; + const container = document.getElementById('container') as HTMLDivElement; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const fileBtn = document.getElementById('file-btn') as HTMLButtonElement; + const openBtn = document.getElementById('open-btn') as HTMLButtonElement; + const resetBtn = document.getElementById('reset-btn') as HTMLButtonElement; + + const preventDefaults = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + }; + + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + container.addEventListener(eventName, preventDefaults, false); + document.body.addEventListener(eventName, preventDefaults, false); + }); + + ['dragenter', 'dragover'].forEach(eventName => { + container.addEventListener(eventName, () => { + dropZone.classList.add('drag-over'); + dropZone.classList.remove('hidden'); + }, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + container.addEventListener(eventName, () => { + dropZone.classList.remove('drag-over'); + }, false); + }); + + container.addEventListener('drop', (e: DragEvent) => { + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + const file = files[0]; + if (file.name.endsWith('.pagx')) { + loadPAGXFile(file); + } else { + alert('Please drop a .pagx file'); + } + } + }, false); + + fileBtn.addEventListener('click', () => { + fileInput.click(); + }); + + openBtn.addEventListener('click', () => { + fileInput.click(); + }); + + resetBtn.addEventListener('click', () => { + gestureManager.resetTransform(viewerState); + }); + + fileInput.addEventListener('change', () => { + const files = fileInput.files; + if (files && files.length > 0) { + loadPAGXFile(files[0]); + } + fileInput.value = ''; + }); +} + +function checkWebGL2Support(): boolean { + const canvas = document.createElement('canvas'); + const gl = canvas.getContext('webgl2'); + return gl !== null; +} + +function checkWasmSupport(): boolean { + try { + if (typeof WebAssembly === 'object' && + typeof WebAssembly.instantiate === 'function') { + const module = new WebAssembly.Module(new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00])); + return module instanceof WebAssembly.Module; + } + } catch (e) { + // WebAssembly not supported + } + return false; +} + +const BROWSER_REQUIREMENTS = ` +Minimum browser versions required: +• Chrome 57+ +• Firefox 52+ +• Safari 15+ +• Edge 79+ +`.trim(); + +if (typeof window !== 'undefined') { + window.onload = async () => { + if (!checkWasmSupport()) { + alert('Your browser does not support WebAssembly.\n\n' + BROWSER_REQUIREMENTS); + return; + } + if (!checkWebGL2Support()) { + alert('Your browser does not support WebGL 2.0.\n\n' + BROWSER_REQUIREMENTS); + return; + } + try { + viewerState.module = await PAGXWasm({ + locateFile: (file: string) => './wasm-mt/' + file, + mainScriptUrlOrBlob: './wasm-mt/pagx-viewer.js' + }) as PAGXModule; + + // Bind tgfx helper functions required by WebGLDevice + viewerState.module.tgfx = { + setColorSpace: (GL: any, colorSpace: number) => { + // WindowColorSpace: None=0, SRGB=1, DisplayP3=2, Others=3 + if (colorSpace === 3) { + return false; + } + const gl = GL.currentContext?.GLctx as WebGLRenderingContext; + if ('drawingBufferColorSpace' in gl) { + if (colorSpace === 0 || colorSpace === 1) { + (gl as any).drawingBufferColorSpace = 'srgb'; + } else { + (gl as any).drawingBufferColorSpace = 'display-p3'; + } + return true; + } else if (colorSpace === 2) { + return false; + } + return true; + } + }; + + viewerState.pagxView = viewerState.module.PAGXView.MakeFrom('#pagx-canvas'); + updateSize(); + viewerState.pagxView.updateZoomScaleAndOffset(1.0, 0, 0); + + const canvas = document.getElementById('pagx-canvas') as HTMLCanvasElement; + bindCanvasEvents(canvas); + setupDragAndDrop(); + animationLoop(); + setupVisibilityListeners(); + } catch (error) { + console.error(error); + throw new Error("PAGX Viewer init failed. Please check the .wasm file path!."); + } + }; + + window.onresize = () => { + if (!viewerState.pagxView || viewerState.resized) { + return; + } + viewerState.resized = true; + window.setTimeout(() => { + updateSize(); + }, 300); + }; +} diff --git a/pagx/viewer/package.json b/pagx/viewer/package.json new file mode 100644 index 0000000000..846f141b83 --- /dev/null +++ b/pagx/viewer/package.json @@ -0,0 +1,31 @@ +{ + "name": "pagx-viewer", + "version": "1.0.0", + "description": "PAGX File Viewer", + "type": "module", + "scripts": { + "clean": "rimraf wasm-mt build-pagx .pagx.wasm-mt.md5", + "build": "node script/cmake.js -a wasm-mt && rollup -c ./script/rollup.js", + "build:debug": "node script/cmake.js -a wasm-mt --debug && rollup -c ./script/rollup.js", + "build:release": "node script/cmake.js -a wasm-mt && BUILD_MODE=release rollup -c ./script/rollup.js", + "server": "node server.js" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "~28.0.3", + "@rollup/plugin-json": "~6.1.0", + "@rollup/plugin-node-resolve": "~16.0.1", + "@types/emscripten": "~1.39.6", + "esbuild": "~0.15.14", + "rimraf": "~5.0.10", + "rollup": "~2.79.1", + "rollup-plugin-esbuild": "~4.10.3", + "rollup-plugin-terser": "~7.0.2", + "tslib": "~2.4.1", + "typescript": "~5.0.3" + }, + "dependencies": { + "express": "^4.21.1" + }, + "license": "Apache-2.0", + "author": "Tencent" +} diff --git a/pagx/viewer/script/cmake.js b/pagx/viewer/script/cmake.js new file mode 100644 index 0000000000..06056c1692 --- /dev/null +++ b/pagx/viewer/script/cmake.js @@ -0,0 +1,47 @@ +#!/usr/bin/env node +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import { fileURLToPath } from 'url'; +import { copyFileSync, existsSync, mkdirSync } from 'fs'; +import path from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +process.chdir(__dirname); + +process.argv.push("-s"); +process.argv.push("../"); +process.argv.push("-o"); +process.argv.push("./"); +process.argv.push("-p"); +process.argv.push("web"); +process.argv.push("pagx-viewer"); + +// Use build_tgfx from third_party/tgfx +const buildTgfx = await import("../../../../third_party/tgfx/build_tgfx"); + +// Copy wasm files to viewer/wasm-mt/ directory +const srcDir = path.resolve(__dirname, 'wasm-mt'); +const destDir = path.resolve(__dirname, '../wasm-mt'); +if (!existsSync(destDir)) { + mkdirSync(destDir, { recursive: true }); +} +copyFileSync(path.join(srcDir, 'pagx-viewer.js'), path.join(destDir, 'pagx-viewer.js')); +copyFileSync(path.join(srcDir, 'pagx-viewer.wasm'), path.join(destDir, 'pagx-viewer.wasm')); diff --git a/pagx/viewer/script/rollup.js b/pagx/viewer/script/rollup.js new file mode 100644 index 0000000000..b2766ae52d --- /dev/null +++ b/pagx/viewer/script/rollup.js @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import esbuild from 'rollup-plugin-esbuild'; +import resolve from '@rollup/plugin-node-resolve'; +import commonJs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import path from "path"; +import {readFileSync} from "node:fs"; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const fileHeaderPath = path.resolve(__dirname, '../../../.idea/fileTemplates/includes/PAG File Header.h'); +const banner = readFileSync(fileHeaderPath, 'utf-8'); +const isRelease = process.env.BUILD_MODE === 'release'; + +const plugins = [ + esbuild({tsconfig: path.resolve(__dirname, "../tsconfig.json"), minify: isRelease}), + json(), + resolve(), + commonJs(), + { + name: 'preserve-import-meta-url', + resolveImportMeta(property, options) { + // Preserve the original behavior of `import.meta.url`. + if (property === 'url') { + return 'import.meta.url'; + } + return null; + }, + }, +]; + +export default [ + { + input: path.resolve(__dirname, '../index.ts'), + output: { + banner, + file: path.resolve(__dirname, '../wasm-mt/index.js'), + format: 'esm', + sourcemap: !isRelease + }, + plugins: plugins, + } +]; diff --git a/pagx/viewer/server.js b/pagx/viewer/server.js new file mode 100644 index 0000000000..a7169f02e6 --- /dev/null +++ b/pagx/viewer/server.js @@ -0,0 +1,54 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import express from 'express'; +import path from 'path'; +import { exec } from 'child_process'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); + +// Enable SharedArrayBuffer +app.use((req, res, next) => { + res.set('Cross-Origin-Opener-Policy', 'same-origin'); + res.set('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); + +app.use('', express.static(__dirname, { + setHeaders: (res, filePath) => { + if (filePath.endsWith('.wasm')) { + res.set('Content-Type', 'application/wasm'); + } + } +})); + +app.get('/', (req, res) => { + res.redirect('/index.html'); +}); + +const port = 8082; +app.listen(port, () => { + const url = `http://localhost:${port}/`; + const start = (process.platform === 'darwin' ? 'open' : 'start'); + exec(start + ' ' + url); + console.log(`PAGX Viewer running at ${url}`); +}); diff --git a/pagx/viewer/src/GridBackground.cpp b/pagx/viewer/src/GridBackground.cpp new file mode 100644 index 0000000000..025f9c1032 --- /dev/null +++ b/pagx/viewer/src/GridBackground.cpp @@ -0,0 +1,65 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "GridBackground.h" +#include "tgfx/layers/LayerRecorder.h" + +namespace pagx { + +std::shared_ptr GridBackgroundLayer::Make(int width, int height, + float density) { + return std::shared_ptr(new GridBackgroundLayer(width, height, density)); +} + +GridBackgroundLayer::GridBackgroundLayer(int width, int height, float density) + : width(width), height(height), density(density) { + invalidateContent(); +} + +void GridBackgroundLayer::onUpdateContent(tgfx::LayerRecorder* recorder) { + tgfx::LayerPaint backgroundPaint(tgfx::Color::White()); + recorder->addRect(tgfx::Rect::MakeWH(static_cast(width), static_cast(height)), + backgroundPaint); + + tgfx::LayerPaint tilePaint(tgfx::Color{0.8f, 0.8f, 0.8f, 1.f}); + // Use fixed logical size (32px) so the grid looks the same on all screens. + int logicalTileSize = 32; + int tileSize = static_cast(static_cast(logicalTileSize) * density); + if (tileSize <= 0) { + tileSize = logicalTileSize; + } + for (int y = 0; y < height; y += tileSize) { + bool draw = (y / tileSize) % 2 == 1; + for (int x = 0; x < width; x += tileSize) { + if (draw) { + recorder->addRect( + tgfx::Rect::MakeXYWH(static_cast(x), static_cast(y), + static_cast(tileSize), static_cast(tileSize)), + tilePaint); + } + draw = !draw; + } + } +} + +void DrawBackground(tgfx::Canvas* canvas, int width, int height, float density) { + auto layer = GridBackgroundLayer::Make(width, height, density); + layer->draw(canvas); +} + +} // namespace pagx diff --git a/pagx/viewer/src/GridBackground.h b/pagx/viewer/src/GridBackground.h new file mode 100644 index 0000000000..e3ba22465b --- /dev/null +++ b/pagx/viewer/src/GridBackground.h @@ -0,0 +1,42 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "tgfx/layers/Layer.h" + +namespace pagx { + +class GridBackgroundLayer : public tgfx::Layer { + public: + static std::shared_ptr Make(int width, int height, float density); + + protected: + void onUpdateContent(tgfx::LayerRecorder* recorder) override; + + private: + GridBackgroundLayer(int width, int height, float density); + + int width = 0; + int height = 0; + float density = 1.f; +}; + +void DrawBackground(tgfx::Canvas* canvas, int width, int height, float density); + +} // namespace pagx diff --git a/pagx/viewer/src/PAGXView.cpp b/pagx/viewer/src/PAGXView.cpp new file mode 100644 index 0000000000..69e606623e --- /dev/null +++ b/pagx/viewer/src/PAGXView.cpp @@ -0,0 +1,192 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PAGXView.h" +#include +#include "GridBackground.h" +#include "pagx/layers/TextLayouter.h" +#include "tgfx/core/Data.h" +#include "tgfx/core/Stream.h" +#include "tgfx/core/Typeface.h" + +using namespace emscripten; + +namespace pagx { + +static std::shared_ptr GetDataFromEmscripten(const val& emscriptenData) { + if (emscriptenData.isUndefined()) { + return nullptr; + } + unsigned int length = emscriptenData["length"].as(); + if (length == 0) { + return nullptr; + } + auto buffer = new (std::nothrow) uint8_t[length]; + if (buffer) { + auto memory = val::module_property("HEAPU8")["buffer"]; + auto memoryView = emscriptenData["constructor"].new_( + memory, static_cast(reinterpret_cast(buffer)), length); + memoryView.call("set", emscriptenData); + return tgfx::Data::MakeAdopted(buffer, length, tgfx::Data::DeleteProc); + } + return nullptr; +} + +PAGXView::PAGXView(const std::string& canvasID) : canvasID(canvasID) { + displayList.setRenderMode(tgfx::RenderMode::Tiled); + displayList.setAllowZoomBlur(true); + displayList.setMaxTileCount(512); +} + +void PAGXView::registerFonts(const val& fontVal, const val& emojiFontVal) { + std::vector> fallbackTypefaces; + auto fontData = GetDataFromEmscripten(fontVal); + if (fontData) { + auto typeface = tgfx::Typeface::MakeFromData(fontData, 0); + if (typeface) { + fallbackTypefaces.push_back(std::move(typeface)); + } + } + auto emojiFontData = GetDataFromEmscripten(emojiFontVal); + if (emojiFontData) { + auto typeface = tgfx::Typeface::MakeFromData(emojiFontData, 0); + if (typeface) { + fallbackTypefaces.push_back(std::move(typeface)); + } + } + TextLayouter::SetFallbackTypefaces(fallbackTypefaces); +} + +void PAGXView::loadPAGX(const val& pagxData) { + auto data = GetDataFromEmscripten(pagxData); + if (!data) { + return; + } + auto stream = tgfx::Stream::MakeFromData(data); + if (!stream) { + return; + } + auto content = pagx::LayerBuilder::FromStream(*stream); + if (!content.root) { + return; + } + contentLayer = content.root; + pagxWidth = content.width; + pagxHeight = content.height; + displayList.root()->removeChildren(); + displayList.root()->addChild(contentLayer); + applyCenteringTransform(); +} + +void PAGXView::updateSize() { + if (window == nullptr) { + window = tgfx::WebGLWindow::MakeFrom(canvasID); + } + if (window == nullptr) { + return; + } + window->invalidSize(); + auto device = window->getDevice(); + auto context = device->lockContext(); + if (context == nullptr) { + return; + } + auto surface = window->getSurface(context); + if (surface == nullptr) { + device->unlock(); + return; + } + if (surface->width() != lastSurfaceWidth || surface->height() != lastSurfaceHeight) { + lastSurfaceWidth = surface->width(); + lastSurfaceHeight = surface->height(); + applyCenteringTransform(); + presentImmediately = true; + } + device->unlock(); +} + +void PAGXView::applyCenteringTransform() { + if (lastSurfaceWidth <= 0 || lastSurfaceHeight <= 0 || !contentLayer) { + return; + } + if (pagxWidth <= 0 || pagxHeight <= 0) { + return; + } + float scaleX = static_cast(lastSurfaceWidth) / pagxWidth; + float scaleY = static_cast(lastSurfaceHeight) / pagxHeight; + float scale = std::min(scaleX, scaleY); + float offsetX = (static_cast(lastSurfaceWidth) - pagxWidth * scale) * 0.5f; + float offsetY = (static_cast(lastSurfaceHeight) - pagxHeight * scale) * 0.5f; + auto matrix = tgfx::Matrix::MakeTrans(offsetX, offsetY); + matrix.preScale(scale, scale); + contentLayer->setMatrix(matrix); +} + +void PAGXView::updateZoomScaleAndOffset(float zoom, float offsetX, float offsetY) { + displayList.setZoomScale(zoom); + displayList.setContentOffset(offsetX, offsetY); +} + +void PAGXView::draw() { + if (window == nullptr) { + window = tgfx::WebGLWindow::MakeFrom(canvasID); + } + if (window == nullptr) { + return; + } + bool hasContentChanged = displayList.hasContentChanged(); + bool hasLastRecording = (lastRecording != nullptr); + if (!hasContentChanged && !hasLastRecording) { + return; + } + auto device = window->getDevice(); + auto context = device->lockContext(); + if (context == nullptr) { + return; + } + auto surface = window->getSurface(context); + if (surface == nullptr) { + device->unlock(); + return; + } + auto canvas = surface->getCanvas(); + canvas->clear(); + int width = 0; + int height = 0; + emscripten_get_canvas_element_size(canvasID.c_str(), &width, &height); + auto density = static_cast(surface->width()) / static_cast(width); + pagx::DrawBackground(canvas, surface->width(), surface->height(), density); + displayList.render(surface.get(), false); + auto recording = context->flush(); + if (presentImmediately) { + presentImmediately = false; + if (recording) { + context->submit(std::move(recording)); + window->present(context); + } + } else { + std::swap(lastRecording, recording); + if (recording) { + context->submit(std::move(recording)); + window->present(context); + } + } + device->unlock(); +} + +} // namespace pagx diff --git a/pagx/viewer/src/PAGXView.h b/pagx/viewer/src/PAGXView.h new file mode 100644 index 0000000000..d59d9e1e3c --- /dev/null +++ b/pagx/viewer/src/PAGXView.h @@ -0,0 +1,66 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "tgfx/gpu/Recording.h" +#include "tgfx/gpu/opengl/webgl/WebGLWindow.h" +#include "tgfx/layers/DisplayList.h" +#include "pagx/layers/LayerBuilder.h" + +namespace pagx { + +class PAGXView { + public: + PAGXView(const std::string& canvasID); + + void registerFonts(const emscripten::val& fontVal, const emscripten::val& emojiFontVal); + + void loadPAGX(const emscripten::val& pagxData); + + void updateSize(); + + void updateZoomScaleAndOffset(float zoom, float offsetX, float offsetY); + + void draw(); + + float contentWidth() const { + return pagxWidth; + } + + float contentHeight() const { + return pagxHeight; + } + + private: + void applyCenteringTransform(); + + std::string canvasID = {}; + std::shared_ptr window = nullptr; + tgfx::DisplayList displayList = {}; + std::shared_ptr contentLayer = nullptr; + std::unique_ptr lastRecording = nullptr; + int lastSurfaceWidth = 0; + int lastSurfaceHeight = 0; + bool presentImmediately = true; + float pagxWidth = 0.0f; + float pagxHeight = 0.0f; +}; + +} // namespace pagx diff --git a/pagx/viewer/src/binding.cpp b/pagx/viewer/src/binding.cpp new file mode 100644 index 0000000000..7ce6708128 --- /dev/null +++ b/pagx/viewer/src/binding.cpp @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include "PAGXView.h" + +using namespace emscripten; + +EMSCRIPTEN_BINDINGS(PAGXViewer) { + class_("PAGXView") + .smart_ptr>("PAGXView") + .class_function("MakeFrom", optional_override([](const std::string& canvasID) { + if (canvasID.empty()) { + return std::shared_ptr(nullptr); + } + return std::make_shared(canvasID); + })) + .function("registerFonts", &pagx::PAGXView::registerFonts) + .function("loadPAGX", &pagx::PAGXView::loadPAGX) + .function("updateSize", &pagx::PAGXView::updateSize) + .function("updateZoomScaleAndOffset", &pagx::PAGXView::updateZoomScaleAndOffset) + .function("draw", &pagx::PAGXView::draw) + .function("contentWidth", &pagx::PAGXView::contentWidth) + .function("contentHeight", &pagx::PAGXView::contentHeight); +} diff --git a/pagx/viewer/tsconfig.json b/pagx/viewer/tsconfig.json new file mode 100644 index 0000000000..85cd22ca37 --- /dev/null +++ b/pagx/viewer/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "lib": ["ES5", "ES6", "DOM", "DOM.Iterable"], + "target": "ES2020", + "module": "ES2020", + "allowJs": true, + "sourceMap": true, + "esModuleInterop": true, + "moduleResolution": "Node", + "experimentalDecorators": true, + "strict": true, + "outDir": "./wasm-mt", + "baseUrl": ".", + "resolveJsonModule": true + }, + "include": ["*.ts", "src/**/*.ts"] +} diff --git a/pagx/viewer/types.ts b/pagx/viewer/types.ts new file mode 100644 index 0000000000..711f84ccfd --- /dev/null +++ b/pagx/viewer/types.ts @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +export interface EmscriptenGLContext { + handle: number; + GLctx: WebGLRenderingContext; + attributes: EmscriptenGLContextAttributes; + initExtensionsDone: boolean; + version: number; +} + +export type EmscriptenGLContextAttributes = + { majorVersion: number; minorVersion: number } + & WebGLContextAttributes; + +export interface EmscriptenGL { + contexts: (EmscriptenGLContext | null)[]; + createContext: ( + canvas: HTMLCanvasElement | OffscreenCanvas, + webGLContextAttributes: EmscriptenGLContextAttributes, + ) => number; + currentContext?: EmscriptenGLContext; + deleteContext: (contextHandle: number) => void; + framebuffers: (WebGLFramebuffer | null)[]; + getContext: (contextHandle: number) => EmscriptenGLContext; + getNewId: (array: any[]) => number; + makeContextCurrent: (contextHandle: number) => boolean; + registerContext: (ctx: WebGLRenderingContext, webGLContextAttributes: EmscriptenGLContextAttributes) => number; + textures: (WebGLTexture | null)[]; +} + +export interface PAGXModule extends EmscriptenModule { + GL: EmscriptenGL; + PAGXView: { + MakeFrom: (canvasID: string) => PAGXView | null; + }; + [key: string]: any; +} + +export interface PAGXView { + loadPAGX: (pagxData: Uint8Array) => void; + updateSize: () => void; + updateZoomScaleAndOffset: (zoom: number, offsetX: number, offsetY: number) => void; + draw: () => void; + contentWidth: () => number; + contentHeight: () => number; +} diff --git a/resources/apitest/SVG/4w_node.svg b/resources/apitest/SVG/4w_node.svg new file mode 100644 index 0000000000..e1baa88539 --- /dev/null +++ b/resources/apitest/SVG/4w_node.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/apitest/SVG/Baseline.svg b/resources/apitest/SVG/Baseline.svg new file mode 100644 index 0000000000..195195bf39 --- /dev/null +++ b/resources/apitest/SVG/Baseline.svg @@ -0,0 +1,1174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/ColorPicker.svg b/resources/apitest/SVG/ColorPicker.svg new file mode 100644 index 0000000000..d6be723228 --- /dev/null +++ b/resources/apitest/SVG/ColorPicker.svg @@ -0,0 +1,563 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/resources/apitest/SVG/Guidelines.svg b/resources/apitest/SVG/Guidelines.svg new file mode 100644 index 0000000000..ec24007886 --- /dev/null +++ b/resources/apitest/SVG/Guidelines.svg @@ -0,0 +1,540 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/Overview.svg b/resources/apitest/SVG/Overview.svg new file mode 100644 index 0000000000..0ac6f28c76 --- /dev/null +++ b/resources/apitest/SVG/Overview.svg @@ -0,0 +1,1328 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/Switch.svg b/resources/apitest/SVG/Switch.svg new file mode 100644 index 0000000000..26ef508abc --- /dev/null +++ b/resources/apitest/SVG/Switch.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/UIkit.svg b/resources/apitest/SVG/UIkit.svg new file mode 100644 index 0000000000..3792d0a4b8 --- /dev/null +++ b/resources/apitest/SVG/UIkit.svg @@ -0,0 +1,898 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/blur.svg b/resources/apitest/SVG/blur.svg new file mode 100644 index 0000000000..809f1aec23 --- /dev/null +++ b/resources/apitest/SVG/blur.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/apitest/SVG/complex1.svg b/resources/apitest/SVG/complex1.svg new file mode 100644 index 0000000000..224471c844 --- /dev/null +++ b/resources/apitest/SVG/complex1.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/resources/apitest/SVG/complex2.svg b/resources/apitest/SVG/complex2.svg new file mode 100644 index 0000000000..3b5ba1707e --- /dev/null +++ b/resources/apitest/SVG/complex2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/apitest/SVG/complex3.svg b/resources/apitest/SVG/complex3.svg new file mode 100644 index 0000000000..4d34e2ab07 --- /dev/null +++ b/resources/apitest/SVG/complex3.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/complex4.svg b/resources/apitest/SVG/complex4.svg new file mode 100644 index 0000000000..91b3264ebf --- /dev/null +++ b/resources/apitest/SVG/complex4.svg @@ -0,0 +1 @@ +5日6日7日8日9日10日11日00.20.40.6商品用券情况用券未使用券 \ No newline at end of file diff --git a/resources/apitest/SVG/complex5.svg b/resources/apitest/SVG/complex5.svg new file mode 100644 index 0000000000..55d46b9198 --- /dev/null +++ b/resources/apitest/SVG/complex5.svg @@ -0,0 +1,1502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/complex6.svg b/resources/apitest/SVG/complex6.svg new file mode 100644 index 0000000000..e9a04ef324 --- /dev/null +++ b/resources/apitest/SVG/complex6.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/complex7.svg b/resources/apitest/SVG/complex7.svg new file mode 100644 index 0000000000..9a0d53d3c6 --- /dev/null +++ b/resources/apitest/SVG/complex7.svg @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/displayp3.svg b/resources/apitest/SVG/displayp3.svg new file mode 100644 index 0000000000..3298ffb5a1 --- /dev/null +++ b/resources/apitest/SVG/displayp3.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/apitest/SVG/drawImageRect.svg b/resources/apitest/SVG/drawImageRect.svg new file mode 100644 index 0000000000..f832f4b61a --- /dev/null +++ b/resources/apitest/SVG/drawImageRect.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/emoji.svg b/resources/apitest/SVG/emoji.svg new file mode 100644 index 0000000000..7d1f52bbd8 --- /dev/null +++ b/resources/apitest/SVG/emoji.svg @@ -0,0 +1,8 @@ + + + + SVG 😀 + diff --git a/resources/apitest/SVG/jpg.svg b/resources/apitest/SVG/jpg.svg new file mode 100644 index 0000000000..0771a79eba --- /dev/null +++ b/resources/apitest/SVG/jpg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/apitest/SVG/mask.svg b/resources/apitest/SVG/mask.svg new file mode 100644 index 0000000000..1f98244997 --- /dev/null +++ b/resources/apitest/SVG/mask.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/apitest/SVG/path.svg b/resources/apitest/SVG/path.svg new file mode 100644 index 0000000000..b516f00af9 --- /dev/null +++ b/resources/apitest/SVG/path.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/apitest/SVG/png.svg b/resources/apitest/SVG/png.svg new file mode 100644 index 0000000000..921c2a13d7 --- /dev/null +++ b/resources/apitest/SVG/png.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/apitest/SVG/radialGradient.svg b/resources/apitest/SVG/radialGradient.svg new file mode 100644 index 0000000000..4747a517bc --- /dev/null +++ b/resources/apitest/SVG/radialGradient.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/apitest/SVG/refStyle.svg b/resources/apitest/SVG/refStyle.svg new file mode 100644 index 0000000000..75a6cee404 --- /dev/null +++ b/resources/apitest/SVG/refStyle.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/resources/apitest/SVG/text.svg b/resources/apitest/SVG/text.svg new file mode 100644 index 0000000000..d242aab002 --- /dev/null +++ b/resources/apitest/SVG/text.svg @@ -0,0 +1,8 @@ + + + + SVG GVS + diff --git a/resources/apitest/SVG/textFont.svg b/resources/apitest/SVG/textFont.svg new file mode 100644 index 0000000000..32c6f0432a --- /dev/null +++ b/resources/apitest/SVG/textFont.svg @@ -0,0 +1,9 @@ + + + + SVG GVS + SVG GVS + diff --git a/resources/apitest/SVG/widegamut.svg b/resources/apitest/SVG/widegamut.svg new file mode 100644 index 0000000000..08c65d4d23 --- /dev/null +++ b/resources/apitest/SVG/widegamut.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp new file mode 100644 index 0000000000..839ef02e0b --- /dev/null +++ b/test/src/PAGXTest.cpp @@ -0,0 +1,157 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include "pagx/layers/LayerBuilder.h" +#include "pagx/layers/TextLayouter.h" +#include "pagx/svg/SVGImporter.h" +#include "tgfx/core/Data.h" +#include "tgfx/core/Stream.h" +#include "tgfx/core/Typeface.h" +#include "tgfx/core/Surface.h" +#include "tgfx/svg/SVGDOM.h" +#include "tgfx/svg/TextShaper.h" +#include "utils/Baseline.h" +#include "utils/DevicePool.h" +#include "utils/ProjectPath.h" +#include "utils/TestUtils.h" + +namespace pag { +using namespace tgfx; + +static std::vector> GetFallbackTypefaces() { + static std::vector> typefaces; + static bool initialized = false; + if (!initialized) { + initialized = true; + auto regularTypeface = + Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf")); + if (regularTypeface) { + typefaces.push_back(regularTypeface); + } + auto emojiTypeface = + Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoColorEmoji.ttf")); + if (emojiTypeface) { + typefaces.push_back(emojiTypeface); + } + } + return typefaces; +} + +static void SetupFallbackFonts() { + pagx::TextLayouter::SetFallbackTypefaces(GetFallbackTypefaces()); +} + +static void SaveFile(const std::shared_ptr& data, const std::string& key) { + if (!data) { + return; + } + auto outPath = ProjectPath::Absolute("test/out/" + key); + auto dirPath = std::filesystem::path(outPath).parent_path(); + if (!std::filesystem::exists(dirPath)) { + std::filesystem::create_directories(dirPath); + } + auto file = fopen(outPath.c_str(), "wb"); + if (file) { + fwrite(data->data(), 1, data->size(), file); + fclose(file); + } +} + +/** + * Test case: Convert all SVG files in apitest/SVG directory to PAGX format and render them + */ +PAG_TEST(PAGXTest, SVGToPAGXAll) { + SetupFallbackFonts(); + + std::string svgDir = ProjectPath::Absolute("resources/apitest/SVG"); + std::vector svgFiles = {}; + + for (const auto& entry : std::filesystem::directory_iterator(svgDir)) { + if (entry.path().extension() == ".svg") { + svgFiles.push_back(entry.path().string()); + } + } + + std::sort(svgFiles.begin(), svgFiles.end()); + + auto device = DevicePool::Make(); + ASSERT_TRUE(device != nullptr); + auto context = device->lockContext(); + ASSERT_TRUE(context != nullptr); + + // Create text shaper for SVG rendering + auto textShaper = TextShaper::Make(GetFallbackTypefaces()); + + for (const auto& svgPath : svgFiles) { + std::string baseName = std::filesystem::path(svgPath).stem().string(); + + // Load original SVG with text shaper + auto svgStream = Stream::MakeFromFile(svgPath); + if (svgStream == nullptr) { + continue; + } + auto svgDOM = SVGDOM::Make(*svgStream, textShaper); + if (svgDOM == nullptr) { + continue; + } + + auto containerSize = svgDOM->getContainerSize(); + int width = static_cast(containerSize.width); + int height = static_cast(containerSize.height); + if (width <= 0 || height <= 0) { + continue; + } + + // Render original SVG + auto svgSurface = Surface::Make(context, width, height); + auto svgCanvas = svgSurface->getCanvas(); + svgDOM->render(svgCanvas); + EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); + + // Convert to PAGX + auto pagxContent = pagx::SVGImporter::ImportFromFile(svgPath); + if (pagxContent.empty()) { + continue; + } + + // Save PAGX file to output directory + auto pagxData = Data::MakeWithCopy(pagxContent.data(), pagxContent.size()); + SaveFile(pagxData, "PAGXTest/" + baseName + ".pagx"); + + auto pagxStream = Stream::MakeFromData(pagxData); + if (pagxStream == nullptr) { + continue; + } + + auto content = pagx::LayerBuilder::FromStream(*pagxStream); + if (content.root == nullptr) { + continue; + } + + // Render PAGX + auto pagxSurface = Surface::Make(context, width, height); + auto pagxCanvas = pagxSurface->getCanvas(); + content.root->draw(pagxCanvas); + EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName + "_pagx")); + } + + device->unlock(); +} + +} // namespace pag From 16bc95b35f8fb8a1f11c90a098f432e94e2ee23b Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 17:40:39 +0800 Subject: [PATCH 002/678] Fix copyright header format to match libpag style. --- pagx/include/pagx/layers/LayerBuilder.h | 4 ++-- pagx/include/pagx/layers/TextLayouter.h | 4 ++-- pagx/include/pagx/svg/SVGImporter.h | 4 ++-- pagx/src/layers/LayerBuilder.cpp | 4 ++-- pagx/src/layers/PAGXAttributes.cpp | 4 ++-- pagx/src/layers/PAGXAttributes.h | 4 ++-- pagx/src/layers/PAGXParser.cpp | 4 ++-- pagx/src/layers/PAGXParser.h | 4 ++-- pagx/src/layers/PAGXUtils.cpp | 4 ++-- pagx/src/layers/PAGXUtils.h | 4 ++-- pagx/src/layers/TextLayouter.cpp | 4 ++-- pagx/src/svg/SVGImporter.cpp | 4 ++-- pagx/src/svg/SVGToPAGXConverter.cpp | 4 ++-- pagx/src/svg/SVGToPAGXConverter.h | 4 ++-- pagx/viewer/index.ts | 4 ++-- pagx/viewer/script/rollup.js | 4 ++-- pagx/viewer/server.js | 4 ++-- pagx/viewer/src/GridBackground.cpp | 4 ++-- pagx/viewer/src/GridBackground.h | 4 ++-- pagx/viewer/types.ts | 4 ++-- 20 files changed, 40 insertions(+), 40 deletions(-) diff --git a/pagx/include/pagx/layers/LayerBuilder.h b/pagx/include/pagx/layers/LayerBuilder.h index 5becaaa9fa..690bd4b7b7 100644 --- a/pagx/include/pagx/layers/LayerBuilder.h +++ b/pagx/include/pagx/layers/LayerBuilder.h @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/include/pagx/layers/TextLayouter.h b/pagx/include/pagx/layers/TextLayouter.h index 80b9630a70..d999b0c77c 100644 --- a/pagx/include/pagx/layers/TextLayouter.h +++ b/pagx/include/pagx/layers/TextLayouter.h @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/include/pagx/svg/SVGImporter.h b/pagx/include/pagx/svg/SVGImporter.h index 3537636c3b..fac7f6cbd7 100644 --- a/pagx/include/pagx/svg/SVGImporter.h +++ b/pagx/include/pagx/svg/SVGImporter.h @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/layers/LayerBuilder.cpp b/pagx/src/layers/LayerBuilder.cpp index 8eb7a03e6c..2d2b53ebae 100644 --- a/pagx/src/layers/LayerBuilder.cpp +++ b/pagx/src/layers/LayerBuilder.cpp @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/layers/PAGXAttributes.cpp b/pagx/src/layers/PAGXAttributes.cpp index 900b2e74ae..07becf54bb 100644 --- a/pagx/src/layers/PAGXAttributes.cpp +++ b/pagx/src/layers/PAGXAttributes.cpp @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/layers/PAGXAttributes.h b/pagx/src/layers/PAGXAttributes.h index 383fcf1637..3ecddcb37c 100644 --- a/pagx/src/layers/PAGXAttributes.h +++ b/pagx/src/layers/PAGXAttributes.h @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/layers/PAGXParser.cpp b/pagx/src/layers/PAGXParser.cpp index b3a3738fbc..0d871b5477 100644 --- a/pagx/src/layers/PAGXParser.cpp +++ b/pagx/src/layers/PAGXParser.cpp @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/layers/PAGXParser.h b/pagx/src/layers/PAGXParser.h index bb33e77f45..d9d4fca97f 100644 --- a/pagx/src/layers/PAGXParser.h +++ b/pagx/src/layers/PAGXParser.h @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/layers/PAGXUtils.cpp b/pagx/src/layers/PAGXUtils.cpp index b421731a66..df804d2b6e 100644 --- a/pagx/src/layers/PAGXUtils.cpp +++ b/pagx/src/layers/PAGXUtils.cpp @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/layers/PAGXUtils.h b/pagx/src/layers/PAGXUtils.h index 0663472a33..2303024bb9 100644 --- a/pagx/src/layers/PAGXUtils.h +++ b/pagx/src/layers/PAGXUtils.h @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/layers/TextLayouter.cpp b/pagx/src/layers/TextLayouter.cpp index e386f1a751..e5ee88a1af 100644 --- a/pagx/src/layers/TextLayouter.cpp +++ b/pagx/src/layers/TextLayouter.cpp @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index bc2418abc8..e9e5530103 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/svg/SVGToPAGXConverter.cpp b/pagx/src/svg/SVGToPAGXConverter.cpp index 68920e48c6..389ee4165b 100644 --- a/pagx/src/svg/SVGToPAGXConverter.cpp +++ b/pagx/src/svg/SVGToPAGXConverter.cpp @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/src/svg/SVGToPAGXConverter.h b/pagx/src/svg/SVGToPAGXConverter.h index 287e885e4e..7fd657b541 100644 --- a/pagx/src/svg/SVGToPAGXConverter.h +++ b/pagx/src/svg/SVGToPAGXConverter.h @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/viewer/index.ts b/pagx/viewer/index.ts index 21b11879fd..da207012fe 100644 --- a/pagx/viewer/index.ts +++ b/pagx/viewer/index.ts @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/viewer/script/rollup.js b/pagx/viewer/script/rollup.js index b2766ae52d..f18ec50767 100644 --- a/pagx/viewer/script/rollup.js +++ b/pagx/viewer/script/rollup.js @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/viewer/server.js b/pagx/viewer/server.js index a7169f02e6..ce09a7e43f 100644 --- a/pagx/viewer/server.js +++ b/pagx/viewer/server.js @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/viewer/src/GridBackground.cpp b/pagx/viewer/src/GridBackground.cpp index 025f9c1032..ece6481ede 100644 --- a/pagx/viewer/src/GridBackground.cpp +++ b/pagx/viewer/src/GridBackground.cpp @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/viewer/src/GridBackground.h b/pagx/viewer/src/GridBackground.h index e3ba22465b..1942bbbda6 100644 --- a/pagx/viewer/src/GridBackground.h +++ b/pagx/viewer/src/GridBackground.h @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // diff --git a/pagx/viewer/types.ts b/pagx/viewer/types.ts index 711f84ccfd..4cb5bf5d2f 100644 --- a/pagx/viewer/types.ts +++ b/pagx/viewer/types.ts @@ -4,8 +4,8 @@ // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // From 881d862a0640d70e40c4ac43ea1a57566d5e58bb Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 17:50:07 +0800 Subject: [PATCH 003/678] Fix pagx viewer cmake.js to use libpag vendor_tools for web build. --- pagx/viewer/script/cmake.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pagx/viewer/script/cmake.js b/pagx/viewer/script/cmake.js index 06056c1692..2e6f8d5cf3 100644 --- a/pagx/viewer/script/cmake.js +++ b/pagx/viewer/script/cmake.js @@ -19,10 +19,12 @@ import { fileURLToPath } from 'url'; import { copyFileSync, existsSync, mkdirSync } from 'fs'; +import { createRequire } from 'module'; import path from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const require = createRequire(import.meta.url); process.chdir(__dirname); @@ -34,8 +36,8 @@ process.argv.push("-p"); process.argv.push("web"); process.argv.push("pagx-viewer"); -// Use build_tgfx from third_party/tgfx -const buildTgfx = await import("../../../../third_party/tgfx/build_tgfx"); +// Use vendor_tools from libpag +require("../../../third_party/vendor_tools/lib-build"); // Copy wasm files to viewer/wasm-mt/ directory const srcDir = path.resolve(__dirname, 'wasm-mt'); From 5cd407432e43ce8c3c9aa971777e1ca790e3fda5 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 17:50:41 +0800 Subject: [PATCH 004/678] Add pagx viewer related directories to gitignore. --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 850e904227..b772324029 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,10 @@ mac/PAG linux/vendor/ /PAG .cache + +# PAGX Viewer +pagx/viewer/wasm-mt +pagx/viewer/build-pagx +pagx/viewer/.pagx.wasm-mt.md5 +pagx/viewer/node_modules +pagx/viewer/package-lock.json From 73d64a08f30e621dada05ef77c6f90770a27e4fe Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 17:52:35 +0800 Subject: [PATCH 005/678] Remove version number from pagx spec filename for continuous updates. --- pagx/docs/{pagx_spec_v1.0.md => pagx_spec.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pagx/docs/{pagx_spec_v1.0.md => pagx_spec.md} (100%) diff --git a/pagx/docs/pagx_spec_v1.0.md b/pagx/docs/pagx_spec.md similarity index 100% rename from pagx/docs/pagx_spec_v1.0.md rename to pagx/docs/pagx_spec.md From d07fd474da31623f472a763a8acc6513a15ab635 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 18:25:45 +0800 Subject: [PATCH 006/678] Refactor pagx module to be completely independent of tgfx with custom data structures and parsers. --- pagx/CMakeLists.txt | 45 +- pagx/include/pagx/LayerBuilder.h | 92 + pagx/include/pagx/PAGXDocument.h | 166 ++ pagx/include/pagx/PAGXNode.h | 295 +++ pagx/include/pagx/PAGXSVGParser.h | 72 + pagx/include/pagx/PAGXTypes.h | 268 +++ pagx/include/pagx/PathData.h | 176 ++ pagx/include/pagx/layers/LayerBuilder.h | 72 - pagx/include/pagx/svg/SVGImporter.h | 63 - pagx/src/PAGXDocument.cpp | 125 ++ pagx/src/PAGXNode.cpp | 365 ++++ pagx/src/PAGXXMLParser.cpp | 322 +++ .../TextLayouter.h => src/PAGXXMLParser.h} | 27 +- pagx/src/PAGXXMLWriter.cpp | 135 ++ .../{layers/PAGXUtils.h => PAGXXMLWriter.h} | 14 +- pagx/src/PathData.cpp | 615 ++++++ pagx/src/layers/LayerBuilder.cpp | 62 - pagx/src/layers/PAGXAttributes.cpp | 336 --- pagx/src/layers/PAGXAttributes.h | 84 - pagx/src/layers/PAGXParser.cpp | 796 ------- pagx/src/layers/PAGXParser.h | 137 -- pagx/src/layers/PAGXUtils.cpp | 80 - pagx/src/layers/TextLayouter.cpp | 170 -- pagx/src/svg/PAGXSVGParser.cpp | 1168 ++++++++++ pagx/src/svg/SVGImporter.cpp | 68 - pagx/src/svg/SVGParserInternal.h | 98 + pagx/src/svg/SVGToPAGXConverter.cpp | 1897 ----------------- pagx/src/svg/SVGToPAGXConverter.h | 137 -- pagx/src/tgfx/LayerBuilder.cpp | 669 ++++++ test/src/PAGXTest.cpp | 278 ++- 30 files changed, 4806 insertions(+), 4026 deletions(-) create mode 100644 pagx/include/pagx/LayerBuilder.h create mode 100644 pagx/include/pagx/PAGXDocument.h create mode 100644 pagx/include/pagx/PAGXNode.h create mode 100644 pagx/include/pagx/PAGXSVGParser.h create mode 100644 pagx/include/pagx/PAGXTypes.h create mode 100644 pagx/include/pagx/PathData.h delete mode 100644 pagx/include/pagx/layers/LayerBuilder.h delete mode 100644 pagx/include/pagx/svg/SVGImporter.h create mode 100644 pagx/src/PAGXDocument.cpp create mode 100644 pagx/src/PAGXNode.cpp create mode 100644 pagx/src/PAGXXMLParser.cpp rename pagx/{include/pagx/layers/TextLayouter.h => src/PAGXXMLParser.h} (51%) create mode 100644 pagx/src/PAGXXMLWriter.cpp rename pagx/src/{layers/PAGXUtils.h => PAGXXMLWriter.h} (81%) create mode 100644 pagx/src/PathData.cpp delete mode 100644 pagx/src/layers/LayerBuilder.cpp delete mode 100644 pagx/src/layers/PAGXAttributes.cpp delete mode 100644 pagx/src/layers/PAGXAttributes.h delete mode 100644 pagx/src/layers/PAGXParser.cpp delete mode 100644 pagx/src/layers/PAGXParser.h delete mode 100644 pagx/src/layers/PAGXUtils.cpp delete mode 100644 pagx/src/layers/TextLayouter.cpp create mode 100644 pagx/src/svg/PAGXSVGParser.cpp delete mode 100644 pagx/src/svg/SVGImporter.cpp create mode 100644 pagx/src/svg/SVGParserInternal.h delete mode 100644 pagx/src/svg/SVGToPAGXConverter.cpp delete mode 100644 pagx/src/svg/SVGToPAGXConverter.h create mode 100644 pagx/src/tgfx/LayerBuilder.cpp diff --git a/pagx/CMakeLists.txt b/pagx/CMakeLists.txt index 8e72c3239f..34388992c7 100644 --- a/pagx/CMakeLists.txt +++ b/pagx/CMakeLists.txt @@ -4,30 +4,46 @@ project(PAGX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -option(PAGX_BUILD_SVG "Build PAGX SVG converter module" ON) - -# tgfx must be available as a target (added by parent project or find_package) -if (NOT TARGET tgfx) - message(FATAL_ERROR "tgfx target not found. Please add pagx as a subdirectory of a project that includes tgfx.") -endif() - -# PAGX library sources -file(GLOB PAGX_LAYERS_SOURCES src/layers/*.cpp) -list(APPEND PAGX_SOURCES ${PAGX_LAYERS_SOURCES}) +option(PAGX_BUILD_SVG "Build PAGX SVG parser module" ON) +option(PAGX_BUILD_TGFX_ADAPTER "Build PAGX to tgfx adapter (LayerBuilder)" OFF) + +# ============== Core PAGX Library (Independent, no tgfx dependency) ============== + +# Core sources +file(GLOB PAGX_CORE_SOURCES + src/PAGXNode.cpp + src/PAGXDocument.cpp + src/PAGXXMLParser.cpp + src/PAGXXMLWriter.cpp + src/PathData.cpp +) +list(APPEND PAGX_SOURCES ${PAGX_CORE_SOURCES}) +# SVG parser (also independent of tgfx) if (PAGX_BUILD_SVG) file(GLOB PAGX_SVG_SOURCES src/svg/*.cpp) list(APPEND PAGX_SOURCES ${PAGX_SVG_SOURCES}) endif() +# tgfx adapter sources (requires tgfx) +if (PAGX_BUILD_TGFX_ADAPTER) + if (NOT TARGET tgfx) + message(FATAL_ERROR "tgfx target not found. Set PAGX_BUILD_TGFX_ADAPTER=OFF to build without tgfx adapter.") + endif() + file(GLOB PAGX_TGFX_SOURCES src/tgfx/*.cpp) + list(APPEND PAGX_SOURCES ${PAGX_TGFX_SOURCES}) +endif() + add_library(pagx STATIC ${PAGX_SOURCES}) +# Public include directory target_include_directories(pagx PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) +# Private include directories target_include_directories(pagx PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/layers + ${CMAKE_CURRENT_SOURCE_DIR}/src ) if (PAGX_BUILD_SVG) @@ -36,4 +52,9 @@ if (PAGX_BUILD_SVG) ) endif() -target_link_libraries(pagx PUBLIC tgfx) +if (PAGX_BUILD_TGFX_ADAPTER) + target_include_directories(pagx PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/tgfx + ) + target_link_libraries(pagx PUBLIC tgfx) +endif() diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h new file mode 100644 index 0000000000..d4b47c4d67 --- /dev/null +++ b/pagx/include/pagx/LayerBuilder.h @@ -0,0 +1,92 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/PAGXDocument.h" +#include "tgfx/core/Typeface.h" +#include "tgfx/layers/Layer.h" + +namespace pagx { + +/** + * Result of building a layer tree from a PAGXDocument. + */ +struct PAGXContent { + /** + * The root layer of the built layer tree. + */ + std::shared_ptr root = nullptr; + + /** + * The width of the content. + */ + float width = 0; + + /** + * The height of the content. + */ + float height = 0; +}; + +/** + * LayerBuilder converts PAGXDocument to tgfx::Layer tree for rendering. + * This is the bridge between the independent pagx module and tgfx rendering. + */ +class LayerBuilder { + public: + struct Options { + /** + * Fallback typefaces for text rendering. + */ + std::vector> fallbackTypefaces = {}; + + /** + * Base path for resolving relative resource paths. + */ + std::string basePath = {}; + + Options() = default; + }; + + /** + * Builds a layer tree from a PAGXDocument. + */ + static PAGXContent Build(const PAGXDocument& document, const Options& options = Options()); + + /** + * Builds a layer tree from a PAGX file. + */ + static PAGXContent FromFile(const std::string& filePath, const Options& options = Options()); + + /** + * Builds a layer tree from PAGX XML data. + */ + static PAGXContent FromData(const uint8_t* data, size_t length, const Options& options = Options()); + + /** + * Builds a layer tree from an SVG file. + * This is a convenience method that first parses the SVG, then builds the layer tree. + */ + static PAGXContent FromSVGFile(const std::string& filePath, const Options& options = Options()); +}; + +} // namespace pagx diff --git a/pagx/include/pagx/PAGXDocument.h b/pagx/include/pagx/PAGXDocument.h new file mode 100644 index 0000000000..747c150a58 --- /dev/null +++ b/pagx/include/pagx/PAGXDocument.h @@ -0,0 +1,166 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/PAGXNode.h" + +namespace pagx { + +/** + * PAGXDocument is the central data structure for the PAGX format. + * It can be created from various sources (XML, SVG, PDF) and exported + * to various formats (XML, PAG binary). + */ +class PAGXDocument { + public: + /** + * Creates an empty PAGX document with the specified dimensions. + */ + static std::shared_ptr Make(float width, float height); + + /** + * Creates a PAGX document from a file. + * Supports .pagx (XML) and .svg files. + */ + static std::shared_ptr FromFile(const std::string& filePath); + + /** + * Creates a PAGX document from XML content. + */ + static std::shared_ptr FromXML(const std::string& xmlContent); + + /** + * Creates a PAGX document from XML data. + */ + static std::shared_ptr FromXML(const uint8_t* data, size_t length); + + /** + * Returns the width of the document. + */ + float width() const { + return _width; + } + + /** + * Returns the height of the document. + */ + float height() const { + return _height; + } + + /** + * Sets the document dimensions. + */ + void setSize(float width, float height); + + /** + * Returns the PAGX version string. + */ + const std::string& version() const { + return _version; + } + + /** + * Returns the root node of the document tree. + */ + PAGXNode* root() const { + return _root.get(); + } + + /** + * Sets the root node of the document. + */ + void setRoot(std::unique_ptr root); + + /** + * Creates a new node with the specified type. + */ + std::unique_ptr createNode(PAGXNodeType type); + + // ============== Resource Management ============== + + /** + * Returns a resource node by its ID. + */ + PAGXNode* getResourceById(const std::string& id) const; + + /** + * Adds a resource to the document. + * The resource must have a unique ID. + */ + void addResource(std::unique_ptr resource); + + /** + * Returns all resource IDs. + */ + std::vector getResourceIds() const; + + /** + * Returns the resources node. + */ + PAGXNode* resources() const { + return _resources.get(); + } + + // ============== Export ============== + + /** + * Exports the document to PAGX XML format. + */ + std::string toXML() const; + + /** + * Saves the document to a file. + */ + bool saveToFile(const std::string& filePath) const; + + // ============== Base Path ============== + + /** + * Returns the base path for resolving relative resource paths. + */ + const std::string& basePath() const { + return _basePath; + } + + /** + * Sets the base path for resolving relative resource paths. + */ + void setBasePath(const std::string& path) { + _basePath = path; + } + + private: + PAGXDocument() = default; + + float _width = 0.0f; + float _height = 0.0f; + std::string _version = "1.0"; + std::string _basePath = {}; + std::unique_ptr _root = nullptr; + std::unique_ptr _resources = nullptr; + std::unordered_map _resourceMap = {}; + + friend class PAGXXMLParser; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h new file mode 100644 index 0000000000..27bad2ecc6 --- /dev/null +++ b/pagx/include/pagx/PAGXNode.h @@ -0,0 +1,295 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include "pagx/PAGXTypes.h" +#include "pagx/PathData.h" + +namespace pagx { + +/** + * Types of nodes in a PAGX document. + */ +enum class PAGXNodeType { + // Document root + Document, + Resources, + + // Layers + Layer, + + // Vector Elements + Group, + Rectangle, + Ellipse, + Polystar, + Path, + Text, + + // Styles + Fill, + Stroke, + TrimPath, + RoundCorner, + MergePath, + Repeater, + + // Color Sources + SolidColor, + LinearGradient, + RadialGradient, + ConicGradient, + DiamondGradient, + ImagePattern, + + // Effects + BlurFilter, + DropShadowFilter, + DropShadowStyle, + InnerShadowStyle, + + // Text + TextSpan, + + // Masks + Mask, + ClipPath, + + // Image + Image, + + // Unknown/Custom + Unknown +}; + +/** + * Returns the string name of a node type. + */ +const char* PAGXNodeTypeName(PAGXNodeType type); + +/** + * Parses a node type from its string name. + */ +PAGXNodeType PAGXNodeTypeFromName(const std::string& name); + +/** + * PAGXNode represents a node in the PAGX document tree. + * Each node has a type, attributes, and optional children. + */ +class PAGXNode { + public: + virtual ~PAGXNode() = default; + + /** + * Creates a new node with the specified type. + */ + static std::unique_ptr Make(PAGXNodeType type); + + /** + * Returns the type of this node. + */ + PAGXNodeType type() const { + return _type; + } + + /** + * Returns the ID of this node. + */ + const std::string& id() const { + return _id; + } + + /** + * Sets the ID of this node. + */ + void setId(const std::string& id) { + _id = id; + } + + // ============== Attribute Access ============== + + /** + * Returns true if the node has an attribute with the given name. + */ + bool hasAttribute(const std::string& name) const; + + /** + * Returns the string value of an attribute. + */ + std::string getAttribute(const std::string& name, const std::string& defaultValue = "") const; + + /** + * Returns the float value of an attribute. + */ + float getFloatAttribute(const std::string& name, float defaultValue = 0.0f) const; + + /** + * Returns the integer value of an attribute. + */ + int getIntAttribute(const std::string& name, int defaultValue = 0) const; + + /** + * Returns the boolean value of an attribute. + */ + bool getBoolAttribute(const std::string& name, bool defaultValue = true) const; + + /** + * Returns the color value of an attribute. + */ + Color getColorAttribute(const std::string& name, const Color& defaultValue = Color::Black()) const; + + /** + * Returns the matrix value of an attribute. + */ + Matrix getMatrixAttribute(const std::string& name) const; + + /** + * Returns the path data value of an attribute. + */ + PathData getPathAttribute(const std::string& name) const; + + /** + * Sets a string attribute. + */ + void setAttribute(const std::string& name, const std::string& value); + + /** + * Sets a float attribute. + */ + void setFloatAttribute(const std::string& name, float value); + + /** + * Sets an integer attribute. + */ + void setIntAttribute(const std::string& name, int value); + + /** + * Sets a boolean attribute. + */ + void setBoolAttribute(const std::string& name, bool value); + + /** + * Sets a color attribute. + */ + void setColorAttribute(const std::string& name, const Color& color); + + /** + * Sets a matrix attribute. + */ + void setMatrixAttribute(const std::string& name, const Matrix& matrix); + + /** + * Sets a path data attribute. + */ + void setPathAttribute(const std::string& name, const PathData& path); + + /** + * Removes an attribute. + */ + void removeAttribute(const std::string& name); + + /** + * Returns all attribute names. + */ + std::vector getAttributeNames() const; + + // ============== Children ============== + + /** + * Returns the child nodes. + */ + const std::vector>& children() const { + return _children; + } + + /** + * Returns the number of children. + */ + size_t childCount() const { + return _children.size(); + } + + /** + * Returns the child at the specified index. + */ + PAGXNode* childAt(size_t index) const; + + /** + * Appends a child node. + */ + void appendChild(std::unique_ptr child); + + /** + * Inserts a child node at the specified index. + */ + void insertChild(size_t index, std::unique_ptr child); + + /** + * Removes and returns the child at the specified index. + */ + std::unique_ptr removeChild(size_t index); + + /** + * Removes all children. + */ + void clearChildren(); + + // ============== Parent ============== + + /** + * Returns the parent node (weak reference). + */ + PAGXNode* parent() const { + return _parent; + } + + // ============== Traversal ============== + + /** + * Finds the first descendant node with the given ID. + */ + PAGXNode* findById(const std::string& id); + + /** + * Finds all descendant nodes of the given type. + */ + std::vector findByType(PAGXNodeType type); + + /** + * Creates a deep copy of this node and all its children. + */ + std::unique_ptr clone() const; + + protected: + explicit PAGXNode(PAGXNodeType type) : _type(type) { + } + + private: + PAGXNodeType _type = PAGXNodeType::Unknown; + std::string _id = {}; + std::unordered_map _attributes = {}; + std::vector> _children = {}; + PAGXNode* _parent = nullptr; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/PAGXSVGParser.h b/pagx/include/pagx/PAGXSVGParser.h new file mode 100644 index 0000000000..380ba99beb --- /dev/null +++ b/pagx/include/pagx/PAGXSVGParser.h @@ -0,0 +1,72 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/PAGXDocument.h" + +namespace pagx { + +/** + * PAGXSVGParser converts SVG documents to PAGXDocument. + * This parser is independent of tgfx and preserves complete SVG information. + */ +class PAGXSVGParser { + public: + struct Options { + /** + * If true, unsupported SVG elements are preserved as Unknown nodes. + */ + bool preserveUnknownElements; + + /** + * If true, references are expanded to actual content. + */ + bool expandUseReferences; + + /** + * If true, nested transforms are flattened into single matrices. + */ + bool flattenTransforms; + + Options() : preserveUnknownElements(false), expandUseReferences(true), flattenTransforms(false) { + } + }; + + /** + * Parses an SVG file and creates a PAGXDocument. + */ + static std::shared_ptr Parse(const std::string& filePath, + const Options& options = Options()); + + /** + * Parses SVG data and creates a PAGXDocument. + */ + static std::shared_ptr Parse(const uint8_t* data, size_t length, + const Options& options = Options()); + + /** + * Parses an SVG string and creates a PAGXDocument. + */ + static std::shared_ptr ParseString(const std::string& svgContent, + const Options& options = Options()); +}; + +} // namespace pagx diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h new file mode 100644 index 0000000000..7878c3b307 --- /dev/null +++ b/pagx/include/pagx/PAGXTypes.h @@ -0,0 +1,268 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace pagx { + +/** + * Represents a 2D point with x and y coordinates. + */ +struct Point { + float x = 0.0f; + float y = 0.0f; + + static Point Make(float x, float y) { + return {x, y}; + } + + static Point Zero() { + return {0.0f, 0.0f}; + } + + bool operator==(const Point& other) const { + return x == other.x && y == other.y; + } + + bool operator!=(const Point& other) const { + return !(*this == other); + } + + Point operator+(const Point& other) const { + return {x + other.x, y + other.y}; + } + + Point operator-(const Point& other) const { + return {x - other.x, y - other.y}; + } + + Point operator*(float scalar) const { + return {x * scalar, y * scalar}; + } +}; + +/** + * Represents a rectangle with left, top, right, and bottom coordinates. + */ +struct Rect { + float left = 0.0f; + float top = 0.0f; + float right = 0.0f; + float bottom = 0.0f; + + static Rect MakeEmpty() { + return {0.0f, 0.0f, 0.0f, 0.0f}; + } + + static Rect MakeWH(float width, float height) { + return {0.0f, 0.0f, width, height}; + } + + static Rect MakeXYWH(float x, float y, float width, float height) { + return {x, y, x + width, y + height}; + } + + static Rect MakeLTRB(float left, float top, float right, float bottom) { + return {left, top, right, bottom}; + } + + float x() const { + return left; + } + + float y() const { + return top; + } + + float width() const { + return right - left; + } + + float height() const { + return bottom - top; + } + + bool isEmpty() const { + return left >= right || top >= bottom; + } + + Point center() const { + return {(left + right) * 0.5f, (top + bottom) * 0.5f}; + } + + void setEmpty() { + left = top = right = bottom = 0.0f; + } + + void join(const Rect& other) { + if (other.isEmpty()) { + return; + } + if (isEmpty()) { + *this = other; + return; + } + left = std::min(left, other.left); + top = std::min(top, other.top); + right = std::max(right, other.right); + bottom = std::max(bottom, other.bottom); + } +}; + +/** + * Represents a color with RGBA components in floating point (0.0 to 1.0). + */ +struct Color { + float red = 0.0f; + float green = 0.0f; + float blue = 0.0f; + float alpha = 1.0f; + + static Color Black() { + return {0.0f, 0.0f, 0.0f, 1.0f}; + } + + static Color White() { + return {1.0f, 1.0f, 1.0f, 1.0f}; + } + + static Color Transparent() { + return {0.0f, 0.0f, 0.0f, 0.0f}; + } + + static Color FromRGBA(float r, float g, float b, float a = 1.0f) { + return {r, g, b, a}; + } + + /** + * Creates a color from a 32-bit hex value in AARRGGBB format. + */ + static Color FromHex(uint32_t hex) { + return {static_cast((hex >> 16) & 0xFF) / 255.0f, + static_cast((hex >> 8) & 0xFF) / 255.0f, + static_cast(hex & 0xFF) / 255.0f, + static_cast((hex >> 24) & 0xFF) / 255.0f}; + } + + /** + * Converts the color to a 32-bit hex value in AARRGGBB format. + */ + uint32_t toHex() const { + auto r = static_cast(red * 255.0f + 0.5f); + auto g = static_cast(green * 255.0f + 0.5f); + auto b = static_cast(blue * 255.0f + 0.5f); + auto a = static_cast(alpha * 255.0f + 0.5f); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + bool operator==(const Color& other) const { + return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; + } + + bool operator!=(const Color& other) const { + return !(*this == other); + } + + Color withAlpha(float newAlpha) const { + return {red, green, blue, newAlpha}; + } +}; + +/** + * Represents a 3x3 transformation matrix for 2D graphics. + * The matrix is stored in row-major order: + * | scaleX skewX transX | + * | skewY scaleY transY | + * | 0 0 1 | + */ +struct Matrix { + float scaleX = 1.0f; + float skewX = 0.0f; + float transX = 0.0f; + float skewY = 0.0f; + float scaleY = 1.0f; + float transY = 0.0f; + + static Matrix Identity() { + return {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f}; + } + + static Matrix Translate(float tx, float ty) { + return {1.0f, 0.0f, tx, 0.0f, 1.0f, ty}; + } + + static Matrix Scale(float sx, float sy) { + return {sx, 0.0f, 0.0f, 0.0f, sy, 0.0f}; + } + + static Matrix Rotate(float degrees) { + auto radians = degrees * 3.14159265358979323846f / 180.0f; + auto cosValue = std::cos(radians); + auto sinValue = std::sin(radians); + return {cosValue, -sinValue, 0.0f, sinValue, cosValue, 0.0f}; + } + + static Matrix MakeAll(float scaleX, float skewX, float transX, float skewY, float scaleY, + float transY) { + return {scaleX, skewX, transX, skewY, scaleY, transY}; + } + + bool isIdentity() const { + return scaleX == 1.0f && skewX == 0.0f && transX == 0.0f && skewY == 0.0f && scaleY == 1.0f && + transY == 0.0f; + } + + Matrix operator*(const Matrix& other) const { + return {scaleX * other.scaleX + skewX * other.skewY, + scaleX * other.skewX + skewX * other.scaleY, + scaleX * other.transX + skewX * other.transY + transX, + skewY * other.scaleX + scaleY * other.skewY, + skewY * other.skewX + scaleY * other.scaleY, + skewY * other.transX + scaleY * other.transY + transY}; + } + + Point mapPoint(const Point& point) const { + return {scaleX * point.x + skewX * point.y + transX, + skewY * point.x + scaleY * point.y + transY}; + } + + void preTranslate(float tx, float ty) { + transX += scaleX * tx + skewX * ty; + transY += skewY * tx + scaleY * ty; + } + + void preScale(float sx, float sy) { + scaleX *= sx; + skewX *= sy; + skewY *= sx; + scaleY *= sy; + } + + void preConcat(const Matrix& other) { + *this = other * (*this); + } + + void postConcat(const Matrix& other) { + *this = (*this) * other; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/PathData.h b/pagx/include/pagx/PathData.h new file mode 100644 index 0000000000..d03bb11db7 --- /dev/null +++ b/pagx/include/pagx/PathData.h @@ -0,0 +1,176 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/PAGXTypes.h" + +namespace pagx { + +/** + * Path command types. + */ +enum class PathVerb : uint8_t { + Move, // 1 point: destination + Line, // 1 point: end point + Quad, // 2 points: control point, end point + Cubic, // 3 points: control point 1, control point 2, end point + Close // 0 points +}; + +/** + * PathData stores path commands in a format optimized for fast iteration + * and serialization. Unlike tgfx::Path, it exposes raw data arrays directly. + */ +class PathData { + public: + PathData() = default; + + /** + * Creates a PathData from an SVG path data string (d attribute). + */ + static PathData FromSVGString(const std::string& d); + + /** + * Starts a new contour at the specified point. + */ + void moveTo(float x, float y); + + /** + * Adds a line from the current point to the specified point. + */ + void lineTo(float x, float y); + + /** + * Adds a quadratic Bezier curve from the current point. + * @param cx control point x + * @param cy control point y + * @param x end point x + * @param y end point y + */ + void quadTo(float cx, float cy, float x, float y); + + /** + * Adds a cubic Bezier curve from the current point. + * @param c1x first control point x + * @param c1y first control point y + * @param c2x second control point x + * @param c2y second control point y + * @param x end point x + * @param y end point y + */ + void cubicTo(float c1x, float c1y, float c2x, float c2y, float x, float y); + + /** + * Closes the current contour by connecting to the starting point. + */ + void close(); + + /** + * Adds a rectangle to the path. + */ + void addRect(const Rect& rect); + + /** + * Adds an oval inscribed in the specified rectangle. + */ + void addOval(const Rect& rect); + + /** + * Adds a rounded rectangle to the path. + */ + void addRoundRect(const Rect& rect, float radiusX, float radiusY); + + /** + * Returns the array of path commands. + */ + const std::vector& verbs() const { + return _verbs; + } + + /** + * Returns the array of point coordinates. + * Points are stored as [x0, y0, x1, y1, ...]. + */ + const std::vector& points() const { + return _points; + } + + /** + * Returns the number of point coordinates. + */ + size_t countPoints() const { + return _points.size() / 2; + } + + /** + * Iterates over all path commands with a visitor function. + * The visitor receives the verb and a pointer to the point data. + */ + template + void forEach(Visitor&& visitor) const { + size_t pointIndex = 0; + for (auto verb : _verbs) { + const float* pts = _points.data() + pointIndex; + visitor(verb, pts); + pointIndex += PointsPerVerb(verb) * 2; + } + } + + /** + * Converts the path to an SVG path data string. + */ + std::string toSVGString() const; + + /** + * Returns the bounding rectangle of the path. + */ + Rect getBounds() const; + + /** + * Returns true if the path contains no commands. + */ + bool isEmpty() const { + return _verbs.empty(); + } + + /** + * Clears all path data. + */ + void clear(); + + /** + * Transforms all points in the path by the given matrix. + */ + void transform(const Matrix& matrix); + + /** + * Returns the number of points used by the given verb. + */ + static int PointsPerVerb(PathVerb verb); + + private: + std::vector _verbs = {}; + std::vector _points = {}; + mutable Rect _cachedBounds = {}; + mutable bool _boundsDirty = true; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/layers/LayerBuilder.h b/pagx/include/pagx/layers/LayerBuilder.h deleted file mode 100644 index 690bd4b7b7..0000000000 --- a/pagx/include/pagx/layers/LayerBuilder.h +++ /dev/null @@ -1,72 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "tgfx/core/Stream.h" -#include "tgfx/layers/Layer.h" - -namespace pagx { - -/** - * PAGXContent represents the result of parsing a PAGX file, containing the root layer - * and canvas dimensions. - */ -struct PAGXContent { - /** - * The root layer of the PAGX content. nullptr if parsing failed. - */ - std::shared_ptr root = nullptr; - /** - * The canvas width specified in the PAGX file. - */ - float width = 0.0f; - /** - * The canvas height specified in the PAGX file. - */ - float height = 0.0f; -}; - -/** - * LayerBuilder provides functionality to build tgfx vector layer trees from PAGX files. - * PAGX (Portable Animated Graphics XML) is an XML-based markup language for describing - * animated vector graphics. - */ -class LayerBuilder { - public: - /** - * Builds a Layer tree from a PAGX file. - * @param filePath The path to the PAGX file. The file's directory is used as the base path - * for resolving relative resource paths (e.g., images). - * @return PAGXContent containing the root layer and canvas dimensions. - */ - static PAGXContent FromFile(const std::string& filePath); - - /** - * Builds a Layer tree from a stream containing PAGX XML data. - * @param stream The stream containing PAGX XML data. - * @param basePath The base directory path for resolving relative resource paths. - * If empty, relative paths cannot be resolved. - * @return PAGXContent containing the root layer and canvas dimensions. - */ - static PAGXContent FromStream(tgfx::Stream& stream, const std::string& basePath = ""); -}; - -} // namespace pagx diff --git a/pagx/include/pagx/svg/SVGImporter.h b/pagx/include/pagx/svg/SVGImporter.h deleted file mode 100644 index fac7f6cbd7..0000000000 --- a/pagx/include/pagx/svg/SVGImporter.h +++ /dev/null @@ -1,63 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "tgfx/core/Stream.h" -#include "tgfx/svg/SVGDOM.h" - -namespace pagx { - -/** - * SVGImporter provides functionality to import SVG files and convert them to PAGX format. - */ -class SVGImporter { - public: - /** - * Imports an SVG file and converts it to PAGX format. - * @param svgFilePath The path to the SVG file to import. - * @return The PAGX content as a string, or an empty string if import fails. - */ - static std::string ImportFromFile(const std::string& svgFilePath); - - /** - * Imports SVG content from a stream and converts it to PAGX format. - * @param svgStream The stream containing SVG content. - * @return The PAGX content as a string, or an empty string if import fails. - */ - static std::string ImportFromStream(tgfx::Stream& svgStream); - - /** - * Imports an SVGDOM object and converts it to PAGX format. - * @param svgDOM The SVGDOM object to import. - * @return The PAGX content as a string, or an empty string if import fails. - */ - static std::string ImportFromDOM(const std::shared_ptr& svgDOM); - - /** - * Saves PAGX content to a file. - * @param pagxContent The PAGX content to save. - * @param outputPath The path to save the PAGX file. - * @return True if the file was saved successfully, false otherwise. - */ - static bool SaveToFile(const std::string& pagxContent, const std::string& outputPath); -}; - -} // namespace pagx diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp new file mode 100644 index 0000000000..18ca8925ae --- /dev/null +++ b/pagx/src/PAGXDocument.cpp @@ -0,0 +1,125 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/PAGXDocument.h" +#include +#include +#include "PAGXXMLParser.h" +#include "PAGXXMLWriter.h" + +namespace pagx { + +std::shared_ptr PAGXDocument::Make(float width, float height) { + auto doc = std::shared_ptr(new PAGXDocument()); + doc->_width = width; + doc->_height = height; + doc->_root = PAGXNode::Make(PAGXNodeType::Document); + doc->_root->setFloatAttribute("width", width); + doc->_root->setFloatAttribute("height", height); + doc->_resources = PAGXNode::Make(PAGXNodeType::Resources); + return doc; +} + +std::shared_ptr PAGXDocument::FromFile(const std::string& filePath) { + std::ifstream file(filePath, std::ios::binary); + if (!file.is_open()) { + return nullptr; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + + auto doc = FromXML(content); + if (doc) { + // Extract base path from file path + auto lastSlash = filePath.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + doc->_basePath = filePath.substr(0, lastSlash + 1); + } + } + return doc; +} + +std::shared_ptr PAGXDocument::FromXML(const std::string& xmlContent) { + return FromXML(reinterpret_cast(xmlContent.data()), xmlContent.size()); +} + +std::shared_ptr PAGXDocument::FromXML(const uint8_t* data, size_t length) { + return PAGXXMLParser::Parse(data, length); +} + +void PAGXDocument::setSize(float width, float height) { + _width = width; + _height = height; + if (_root) { + _root->setFloatAttribute("width", width); + _root->setFloatAttribute("height", height); + } +} + +void PAGXDocument::setRoot(std::unique_ptr root) { + _root = std::move(root); +} + +std::unique_ptr PAGXDocument::createNode(PAGXNodeType type) { + return PAGXNode::Make(type); +} + +PAGXNode* PAGXDocument::getResourceById(const std::string& id) const { + auto it = _resourceMap.find(id); + if (it != _resourceMap.end()) { + return it->second; + } + return nullptr; +} + +void PAGXDocument::addResource(std::unique_ptr resource) { + if (!resource || resource->id().empty()) { + return; + } + auto id = resource->id(); + auto* rawPtr = resource.get(); + _resources->appendChild(std::move(resource)); + _resourceMap[id] = rawPtr; +} + +std::vector PAGXDocument::getResourceIds() const { + std::vector ids; + ids.reserve(_resourceMap.size()); + for (const auto& pair : _resourceMap) { + ids.push_back(pair.first); + } + return ids; +} + +std::string PAGXDocument::toXML() const { + return PAGXXMLWriter::Write(this); +} + +bool PAGXDocument::saveToFile(const std::string& filePath) const { + std::string xml = toXML(); + std::ofstream file(filePath, std::ios::binary); + if (!file.is_open()) { + return false; + } + file.write(xml.data(), static_cast(xml.size())); + return file.good(); +} + +} // namespace pagx diff --git a/pagx/src/PAGXNode.cpp b/pagx/src/PAGXNode.cpp new file mode 100644 index 0000000000..a3ee022305 --- /dev/null +++ b/pagx/src/PAGXNode.cpp @@ -0,0 +1,365 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/PAGXNode.h" +#include +#include +#include + +namespace pagx { + +const char* PAGXNodeTypeName(PAGXNodeType type) { + switch (type) { + case PAGXNodeType::Document: + return "Document"; + case PAGXNodeType::Resources: + return "Resources"; + case PAGXNodeType::Layer: + return "Layer"; + case PAGXNodeType::Group: + return "Group"; + case PAGXNodeType::Rectangle: + return "Rectangle"; + case PAGXNodeType::Ellipse: + return "Ellipse"; + case PAGXNodeType::Polystar: + return "Polystar"; + case PAGXNodeType::Path: + return "Path"; + case PAGXNodeType::Text: + return "Text"; + case PAGXNodeType::Fill: + return "Fill"; + case PAGXNodeType::Stroke: + return "Stroke"; + case PAGXNodeType::TrimPath: + return "TrimPath"; + case PAGXNodeType::RoundCorner: + return "RoundCorner"; + case PAGXNodeType::MergePath: + return "MergePath"; + case PAGXNodeType::Repeater: + return "Repeater"; + case PAGXNodeType::SolidColor: + return "SolidColor"; + case PAGXNodeType::LinearGradient: + return "LinearGradient"; + case PAGXNodeType::RadialGradient: + return "RadialGradient"; + case PAGXNodeType::ConicGradient: + return "ConicGradient"; + case PAGXNodeType::DiamondGradient: + return "DiamondGradient"; + case PAGXNodeType::ImagePattern: + return "ImagePattern"; + case PAGXNodeType::BlurFilter: + return "BlurFilter"; + case PAGXNodeType::DropShadowFilter: + return "DropShadowFilter"; + case PAGXNodeType::DropShadowStyle: + return "DropShadowStyle"; + case PAGXNodeType::InnerShadowStyle: + return "InnerShadowStyle"; + case PAGXNodeType::TextSpan: + return "TextSpan"; + case PAGXNodeType::Mask: + return "Mask"; + case PAGXNodeType::ClipPath: + return "ClipPath"; + case PAGXNodeType::Image: + return "Image"; + case PAGXNodeType::Unknown: + default: + return "Unknown"; + } +} + +PAGXNodeType PAGXNodeTypeFromName(const std::string& name) { + if (name == "Document") + return PAGXNodeType::Document; + if (name == "Resources") + return PAGXNodeType::Resources; + if (name == "Layer") + return PAGXNodeType::Layer; + if (name == "Group") + return PAGXNodeType::Group; + if (name == "Rectangle") + return PAGXNodeType::Rectangle; + if (name == "Ellipse") + return PAGXNodeType::Ellipse; + if (name == "Polystar") + return PAGXNodeType::Polystar; + if (name == "Path") + return PAGXNodeType::Path; + if (name == "Text") + return PAGXNodeType::Text; + if (name == "Fill") + return PAGXNodeType::Fill; + if (name == "Stroke") + return PAGXNodeType::Stroke; + if (name == "TrimPath") + return PAGXNodeType::TrimPath; + if (name == "RoundCorner") + return PAGXNodeType::RoundCorner; + if (name == "MergePath") + return PAGXNodeType::MergePath; + if (name == "Repeater") + return PAGXNodeType::Repeater; + if (name == "SolidColor") + return PAGXNodeType::SolidColor; + if (name == "LinearGradient") + return PAGXNodeType::LinearGradient; + if (name == "RadialGradient") + return PAGXNodeType::RadialGradient; + if (name == "ConicGradient") + return PAGXNodeType::ConicGradient; + if (name == "DiamondGradient") + return PAGXNodeType::DiamondGradient; + if (name == "ImagePattern") + return PAGXNodeType::ImagePattern; + if (name == "BlurFilter") + return PAGXNodeType::BlurFilter; + if (name == "DropShadowFilter") + return PAGXNodeType::DropShadowFilter; + if (name == "DropShadowStyle") + return PAGXNodeType::DropShadowStyle; + if (name == "InnerShadowStyle") + return PAGXNodeType::InnerShadowStyle; + if (name == "TextSpan") + return PAGXNodeType::TextSpan; + if (name == "Mask") + return PAGXNodeType::Mask; + if (name == "ClipPath") + return PAGXNodeType::ClipPath; + if (name == "Image") + return PAGXNodeType::Image; + return PAGXNodeType::Unknown; +} + +std::unique_ptr PAGXNode::Make(PAGXNodeType type) { + return std::unique_ptr(new PAGXNode(type)); +} + +bool PAGXNode::hasAttribute(const std::string& name) const { + return _attributes.find(name) != _attributes.end(); +} + +std::string PAGXNode::getAttribute(const std::string& name, const std::string& defaultValue) const { + auto it = _attributes.find(name); + if (it != _attributes.end()) { + return it->second; + } + return defaultValue; +} + +float PAGXNode::getFloatAttribute(const std::string& name, float defaultValue) const { + auto it = _attributes.find(name); + if (it != _attributes.end()) { + return std::stof(it->second); + } + return defaultValue; +} + +int PAGXNode::getIntAttribute(const std::string& name, int defaultValue) const { + auto it = _attributes.find(name); + if (it != _attributes.end()) { + return std::stoi(it->second); + } + return defaultValue; +} + +bool PAGXNode::getBoolAttribute(const std::string& name, bool defaultValue) const { + auto it = _attributes.find(name); + if (it != _attributes.end()) { + const auto& value = it->second; + return value == "true" || value == "1"; + } + return defaultValue; +} + +Color PAGXNode::getColorAttribute(const std::string& name, const Color& defaultValue) const { + auto it = _attributes.find(name); + if (it != _attributes.end()) { + const auto& value = it->second; + if (value.empty()) { + return defaultValue; + } + // Parse hex color like "#RRGGBB" or "#AARRGGBB" + if (value[0] == '#' && (value.length() == 7 || value.length() == 9)) { + uint32_t hex = std::stoul(value.substr(1), nullptr, 16); + if (value.length() == 7) { + hex = 0xFF000000 | hex; // Add full alpha + } + return Color::FromHex(hex); + } + } + return defaultValue; +} + +Matrix PAGXNode::getMatrixAttribute(const std::string& name) const { + auto it = _attributes.find(name); + if (it != _attributes.end()) { + const auto& value = it->second; + // Parse "scaleX skewX transX skewY scaleY transY" + std::istringstream iss(value); + Matrix matrix = Matrix::Identity(); + iss >> matrix.scaleX >> matrix.skewX >> matrix.transX >> matrix.skewY >> matrix.scaleY >> + matrix.transY; + return matrix; + } + return Matrix::Identity(); +} + +PathData PAGXNode::getPathAttribute(const std::string& name) const { + auto it = _attributes.find(name); + if (it != _attributes.end()) { + return PathData::FromSVGString(it->second); + } + return PathData(); +} + +void PAGXNode::setAttribute(const std::string& name, const std::string& value) { + _attributes[name] = value; +} + +void PAGXNode::setFloatAttribute(const std::string& name, float value) { + std::ostringstream oss; + oss << value; + _attributes[name] = oss.str(); +} + +void PAGXNode::setIntAttribute(const std::string& name, int value) { + _attributes[name] = std::to_string(value); +} + +void PAGXNode::setBoolAttribute(const std::string& name, bool value) { + _attributes[name] = value ? "true" : "false"; +} + +void PAGXNode::setColorAttribute(const std::string& name, const Color& color) { + uint32_t hex = color.toHex(); + std::ostringstream oss; + oss << "#" << std::hex << std::uppercase; + oss.width(8); + oss.fill('0'); + oss << hex; + _attributes[name] = oss.str(); +} + +void PAGXNode::setMatrixAttribute(const std::string& name, const Matrix& matrix) { + std::ostringstream oss; + oss << matrix.scaleX << " " << matrix.skewX << " " << matrix.transX << " " << matrix.skewY << " " + << matrix.scaleY << " " << matrix.transY; + _attributes[name] = oss.str(); +} + +void PAGXNode::setPathAttribute(const std::string& name, const PathData& path) { + _attributes[name] = path.toSVGString(); +} + +void PAGXNode::removeAttribute(const std::string& name) { + _attributes.erase(name); +} + +std::vector PAGXNode::getAttributeNames() const { + std::vector names; + names.reserve(_attributes.size()); + for (const auto& pair : _attributes) { + names.push_back(pair.first); + } + return names; +} + +PAGXNode* PAGXNode::childAt(size_t index) const { + if (index < _children.size()) { + return _children[index].get(); + } + return nullptr; +} + +void PAGXNode::appendChild(std::unique_ptr child) { + if (child) { + child->_parent = this; + _children.push_back(std::move(child)); + } +} + +void PAGXNode::insertChild(size_t index, std::unique_ptr child) { + if (child) { + child->_parent = this; + if (index >= _children.size()) { + _children.push_back(std::move(child)); + } else { + _children.insert(_children.begin() + static_cast(index), std::move(child)); + } + } +} + +std::unique_ptr PAGXNode::removeChild(size_t index) { + if (index < _children.size()) { + auto child = std::move(_children[index]); + _children.erase(_children.begin() + static_cast(index)); + child->_parent = nullptr; + return child; + } + return nullptr; +} + +void PAGXNode::clearChildren() { + for (auto& child : _children) { + child->_parent = nullptr; + } + _children.clear(); +} + +PAGXNode* PAGXNode::findById(const std::string& id) { + if (_id == id) { + return this; + } + for (auto& child : _children) { + auto found = child->findById(id); + if (found) { + return found; + } + } + return nullptr; +} + +std::vector PAGXNode::findByType(PAGXNodeType type) { + std::vector result; + if (_type == type) { + result.push_back(this); + } + for (auto& child : _children) { + auto childResults = child->findByType(type); + result.insert(result.end(), childResults.begin(), childResults.end()); + } + return result; +} + +std::unique_ptr PAGXNode::clone() const { + auto copy = PAGXNode::Make(_type); + copy->_id = _id; + copy->_attributes = _attributes; + for (const auto& child : _children) { + copy->appendChild(child->clone()); + } + return copy; +} + +} // namespace pagx diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp new file mode 100644 index 0000000000..4f0aa383fb --- /dev/null +++ b/pagx/src/PAGXXMLParser.cpp @@ -0,0 +1,322 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PAGXXMLParser.h" +#include +#include +#include + +namespace pagx { + +// Simple XML tokenizer and parser (no external dependencies) +class XMLTokenizer { + public: + XMLTokenizer(const char* data, size_t length) : _data(data), _length(length), _pos(0) { + } + + bool isEnd() const { + return _pos >= _length; + } + + void skipWhitespace() { + while (_pos < _length && std::isspace(_data[_pos])) { + ++_pos; + } + } + + bool match(char c) { + if (_pos < _length && _data[_pos] == c) { + ++_pos; + return true; + } + return false; + } + + bool matchString(const char* str) { + size_t len = std::strlen(str); + if (_pos + len <= _length && std::strncmp(_data + _pos, str, len) == 0) { + _pos += len; + return true; + } + return false; + } + + std::string readUntil(char delimiter) { + std::string result; + while (_pos < _length && _data[_pos] != delimiter) { + result += _data[_pos++]; + } + return result; + } + + std::string readName() { + std::string result; + while (_pos < _length && + (std::isalnum(_data[_pos]) || _data[_pos] == '_' || _data[_pos] == '-' || + _data[_pos] == ':' || _data[_pos] == '.')) { + result += _data[_pos++]; + } + return result; + } + + std::string readQuotedString() { + char quote = _data[_pos++]; + std::string result; + while (_pos < _length && _data[_pos] != quote) { + if (_data[_pos] == '&') { + result += readEscapeSequence(); + } else { + result += _data[_pos++]; + } + } + if (_pos < _length) { + ++_pos; // Skip closing quote + } + return result; + } + + std::string readEscapeSequence() { + std::string seq; + ++_pos; // Skip '&' + while (_pos < _length && _data[_pos] != ';') { + seq += _data[_pos++]; + } + if (_pos < _length) { + ++_pos; // Skip ';' + } + if (seq == "lt") { + return "<"; + } + if (seq == "gt") { + return ">"; + } + if (seq == "amp") { + return "&"; + } + if (seq == "quot") { + return "\""; + } + if (seq == "apos") { + return "'"; + } + if (!seq.empty() && seq[0] == '#') { + int code = 0; + if (seq.length() > 1 && seq[1] == 'x') { + code = std::stoi(seq.substr(2), nullptr, 16); + } else { + code = std::stoi(seq.substr(1)); + } + if (code > 0 && code < 128) { + return std::string(1, static_cast(code)); + } + } + return "&" + seq + ";"; + } + + void skipComment() { + while (_pos + 2 < _length) { + if (_data[_pos] == '-' && _data[_pos + 1] == '-' && _data[_pos + 2] == '>') { + _pos += 3; + return; + } + ++_pos; + } + } + + void skipCDATA() { + while (_pos + 2 < _length) { + if (_data[_pos] == ']' && _data[_pos + 1] == ']' && _data[_pos + 2] == '>') { + _pos += 3; + return; + } + ++_pos; + } + } + + void skipProcessingInstruction() { + while (_pos + 1 < _length) { + if (_data[_pos] == '?' && _data[_pos + 1] == '>') { + _pos += 2; + return; + } + ++_pos; + } + } + + private: + const char* _data = nullptr; + size_t _length = 0; + size_t _pos = 0; +}; + +static std::unique_ptr ParseElement(XMLTokenizer& tokenizer, + std::shared_ptr& doc); + +static void ParseAttributes(XMLTokenizer& tokenizer, PAGXNode* node) { + while (true) { + tokenizer.skipWhitespace(); + if (tokenizer.isEnd()) { + break; + } + + std::string name = tokenizer.readName(); + if (name.empty()) { + break; + } + + tokenizer.skipWhitespace(); + if (!tokenizer.match('=')) { + break; + } + + tokenizer.skipWhitespace(); + std::string value = tokenizer.readQuotedString(); + + if (name == "id") { + node->setId(value); + } else { + node->setAttribute(name, value); + } + } +} + +static std::unique_ptr ParseElement(XMLTokenizer& tokenizer, + std::shared_ptr& doc) { + tokenizer.skipWhitespace(); + + if (tokenizer.isEnd() || !tokenizer.match('<')) { + return nullptr; + } + + // Skip comments, CDATA, and processing instructions + while (true) { + if (tokenizer.matchString("!--")) { + tokenizer.skipComment(); + tokenizer.skipWhitespace(); + if (!tokenizer.match('<')) { + return nullptr; + } + } else if (tokenizer.matchString("![CDATA[")) { + tokenizer.skipCDATA(); + tokenizer.skipWhitespace(); + if (!tokenizer.match('<')) { + return nullptr; + } + } else if (tokenizer.matchString("?")) { + tokenizer.skipProcessingInstruction(); + tokenizer.skipWhitespace(); + if (!tokenizer.match('<')) { + return nullptr; + } + } else if (tokenizer.matchString("!DOCTYPE")) { + tokenizer.readUntil('>'); + tokenizer.match('>'); + tokenizer.skipWhitespace(); + if (!tokenizer.match('<')) { + return nullptr; + } + } else { + break; + } + } + + // Check for closing tag + if (tokenizer.match('/')) { + return nullptr; + } + + std::string tagName = tokenizer.readName(); + if (tagName.empty()) { + return nullptr; + } + + auto nodeType = PAGXNodeTypeFromName(tagName); + auto node = PAGXNode::Make(nodeType); + + ParseAttributes(tokenizer, node.get()); + + tokenizer.skipWhitespace(); + + // Self-closing tag + if (tokenizer.match('/')) { + tokenizer.match('>'); + return node; + } + + if (!tokenizer.match('>')) { + return node; + } + + // Parse children + while (true) { + tokenizer.skipWhitespace(); + if (tokenizer.isEnd()) { + break; + } + + // Check for closing tag + size_t savedPos = 0; // Would need to save position for proper lookahead + auto child = ParseElement(tokenizer, doc); + if (!child) { + // Might be a closing tag, try to consume it + tokenizer.skipWhitespace(); + std::string closeTag = tokenizer.readUntil('>'); + tokenizer.match('>'); + break; + } + + // Handle Resources node specially + if (child->type() == PAGXNodeType::Resources && doc) { + for (size_t i = 0; i < child->childCount(); ++i) { + auto resourceChild = child->removeChild(0); + if (resourceChild) { + doc->addResource(std::move(resourceChild)); + } + } + } else { + node->appendChild(std::move(child)); + } + } + + return node; +} + +std::shared_ptr PAGXXMLParser::Parse(const uint8_t* data, size_t length) { + if (!data || length == 0) { + return nullptr; + } + + XMLTokenizer tokenizer(reinterpret_cast(data), length); + + auto doc = std::shared_ptr(new PAGXDocument()); + + auto root = ParseElement(tokenizer, doc); + if (!root) { + return nullptr; + } + + // Extract dimensions from root + doc->_width = root->getFloatAttribute("width", 0); + doc->_height = root->getFloatAttribute("height", 0); + doc->_version = root->getAttribute("version", "1.0"); + + doc->setRoot(std::move(root)); + return doc; +} + +} // namespace pagx diff --git a/pagx/include/pagx/layers/TextLayouter.h b/pagx/src/PAGXXMLParser.h similarity index 51% rename from pagx/include/pagx/layers/TextLayouter.h rename to pagx/src/PAGXXMLParser.h index d999b0c77c..aafad5fcc4 100644 --- a/pagx/include/pagx/layers/TextLayouter.h +++ b/pagx/src/PAGXXMLParser.h @@ -18,36 +18,21 @@ #pragma once +#include #include -#include -#include -#include "tgfx/core/Font.h" -#include "tgfx/core/TextBlob.h" -#include "tgfx/core/Typeface.h" +#include "pagx/PAGXDocument.h" namespace pagx { /** - * TextLayouter provides simple text layout functionality for PAGX text rendering. - * It handles font fallback and basic horizontal text layout. + * Parses PAGX XML documents into PAGXDocument. */ -class TextLayouter { +class PAGXXMLParser { public: /** - * Sets the fallback typefaces for PAGX text rendering. - * When a character cannot be rendered by the primary typeface, the fallback typefaces - * are tried in order until one that supports the character is found. + * Parses XML data and creates a PAGXDocument. */ - static void SetFallbackTypefaces(std::vector> typefaces); - - /** - * Creates a TextBlob from the given text with basic horizontal layout. - * Supports font fallback for characters not available in the primary typeface. - */ - static std::shared_ptr Layout(const std::string& text, const tgfx::Font& font); - - private: - static std::vector> GetFallbackTypefaces(); + static std::shared_ptr Parse(const uint8_t* data, size_t length); }; } // namespace pagx diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp new file mode 100644 index 0000000000..504c57902d --- /dev/null +++ b/pagx/src/PAGXXMLWriter.cpp @@ -0,0 +1,135 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PAGXXMLWriter.h" +#include + +namespace pagx { + +static std::string EscapeXML(const std::string& str) { + std::string result; + result.reserve(str.size()); + for (char c : str) { + switch (c) { + case '<': + result += "<"; + break; + case '>': + result += ">"; + break; + case '&': + result += "&"; + break; + case '"': + result += """; + break; + case '\'': + result += "'"; + break; + default: + result += c; + break; + } + } + return result; +} + +static void WriteNode(std::ostringstream& oss, const PAGXNode* node, int indent) { + std::string indentStr(indent * 2, ' '); + + oss << indentStr << "<" << PAGXNodeTypeName(node->type()); + + // Write ID if present + if (!node->id().empty()) { + oss << " id=\"" << EscapeXML(node->id()) << "\""; + } + + // Write attributes + auto attrNames = node->getAttributeNames(); + for (const auto& name : attrNames) { + std::string value = node->getAttribute(name); + oss << " " << name << "=\"" << EscapeXML(value) << "\""; + } + + if (node->childCount() == 0) { + oss << "/>\n"; + } else { + oss << ">\n"; + for (size_t i = 0; i < node->childCount(); ++i) { + WriteNode(oss, node->childAt(i), indent + 1); + } + oss << indentStr << "type()) << ">\n"; + } +} + +std::string PAGXXMLWriter::Write(const PAGXDocument* document) { + if (!document || !document->root()) { + return ""; + } + + std::ostringstream oss; + oss << "\n"; + + auto root = document->root(); + + oss << "<" << PAGXNodeTypeName(root->type()); + oss << " version=\"" << document->version() << "\""; + oss << " width=\"" << document->width() << "\""; + oss << " height=\"" << document->height() << "\""; + + // Write other root attributes + auto attrNames = root->getAttributeNames(); + for (const auto& name : attrNames) { + if (name == "version" || name == "width" || name == "height") { + continue; + } + std::string value = root->getAttribute(name); + oss << " " << name << "=\"" << EscapeXML(value) << "\""; + } + + // Check if there are resources or children + auto resources = document->resources(); + bool hasResources = resources && resources->childCount() > 0; + bool hasChildren = root->childCount() > 0; + + if (!hasResources && !hasChildren) { + oss << "/>\n"; + } else { + oss << ">\n"; + + // Write resources + if (hasResources) { + oss << " \n"; + for (size_t i = 0; i < resources->childCount(); ++i) { + WriteNode(oss, resources->childAt(i), 2); + } + oss << " \n"; + } + + // Write children + for (size_t i = 0; i < root->childCount(); ++i) { + WriteNode(oss, root->childAt(i), 1); + } + + oss << "type()) << ">\n"; + } + + return oss.str(); +} + +} // namespace pagx diff --git a/pagx/src/layers/PAGXUtils.h b/pagx/src/PAGXXMLWriter.h similarity index 81% rename from pagx/src/layers/PAGXUtils.h rename to pagx/src/PAGXXMLWriter.h index 2303024bb9..53804f14ec 100644 --- a/pagx/src/layers/PAGXUtils.h +++ b/pagx/src/PAGXXMLWriter.h @@ -18,12 +18,20 @@ #pragma once -#include #include -#include "tgfx/core/Data.h" +#include "pagx/PAGXDocument.h" namespace pagx { -std::shared_ptr Base64Decode(const std::string& encodedString); +/** + * Writes PAGXDocument to XML format. + */ +class PAGXXMLWriter { + public: + /** + * Converts a PAGXDocument to XML string. + */ + static std::string Write(const PAGXDocument* document); +}; } // namespace pagx diff --git a/pagx/src/PathData.cpp b/pagx/src/PathData.cpp new file mode 100644 index 0000000000..8410b9484d --- /dev/null +++ b/pagx/src/PathData.cpp @@ -0,0 +1,615 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/PathData.h" +#include +#include +#include + +namespace pagx { + +// Constant for approximating a quarter circle with cubic Bezier curves +static constexpr float CUBIC_ARC_FACTOR = 0.5522847498307936f; + +int PathData::PointsPerVerb(PathVerb verb) { + switch (verb) { + case PathVerb::Move: + return 1; + case PathVerb::Line: + return 1; + case PathVerb::Quad: + return 2; + case PathVerb::Cubic: + return 3; + case PathVerb::Close: + return 0; + } + return 0; +} + +void PathData::moveTo(float x, float y) { + _verbs.push_back(PathVerb::Move); + _points.push_back(x); + _points.push_back(y); + _boundsDirty = true; +} + +void PathData::lineTo(float x, float y) { + _verbs.push_back(PathVerb::Line); + _points.push_back(x); + _points.push_back(y); + _boundsDirty = true; +} + +void PathData::quadTo(float cx, float cy, float x, float y) { + _verbs.push_back(PathVerb::Quad); + _points.push_back(cx); + _points.push_back(cy); + _points.push_back(x); + _points.push_back(y); + _boundsDirty = true; +} + +void PathData::cubicTo(float c1x, float c1y, float c2x, float c2y, float x, float y) { + _verbs.push_back(PathVerb::Cubic); + _points.push_back(c1x); + _points.push_back(c1y); + _points.push_back(c2x); + _points.push_back(c2y); + _points.push_back(x); + _points.push_back(y); + _boundsDirty = true; +} + +void PathData::close() { + _verbs.push_back(PathVerb::Close); +} + +void PathData::addRect(const Rect& rect) { + moveTo(rect.left, rect.top); + lineTo(rect.right, rect.top); + lineTo(rect.right, rect.bottom); + lineTo(rect.left, rect.bottom); + close(); +} + +void PathData::addOval(const Rect& rect) { + float cx = rect.left + rect.width() * 0.5f; + float cy = rect.top + rect.height() * 0.5f; + float rx = rect.width() * 0.5f; + float ry = rect.height() * 0.5f; + + float dx = rx * CUBIC_ARC_FACTOR; + float dy = ry * CUBIC_ARC_FACTOR; + + moveTo(cx + rx, cy); + cubicTo(cx + rx, cy + dy, cx + dx, cy + ry, cx, cy + ry); + cubicTo(cx - dx, cy + ry, cx - rx, cy + dy, cx - rx, cy); + cubicTo(cx - rx, cy - dy, cx - dx, cy - ry, cx, cy - ry); + cubicTo(cx + dx, cy - ry, cx + rx, cy - dy, cx + rx, cy); + close(); +} + +void PathData::addRoundRect(const Rect& rect, float radiusX, float radiusY) { + if (radiusX <= 0 || radiusY <= 0) { + addRect(rect); + return; + } + + float maxRadiusX = rect.width() * 0.5f; + float maxRadiusY = rect.height() * 0.5f; + radiusX = std::min(radiusX, maxRadiusX); + radiusY = std::min(radiusY, maxRadiusY); + + float dx = radiusX * CUBIC_ARC_FACTOR; + float dy = radiusY * CUBIC_ARC_FACTOR; + + moveTo(rect.left + radiusX, rect.top); + lineTo(rect.right - radiusX, rect.top); + cubicTo(rect.right - radiusX + dx, rect.top, rect.right, rect.top + radiusY - dy, rect.right, + rect.top + radiusY); + lineTo(rect.right, rect.bottom - radiusY); + cubicTo(rect.right, rect.bottom - radiusY + dy, rect.right - radiusX + dx, rect.bottom, + rect.right - radiusX, rect.bottom); + lineTo(rect.left + radiusX, rect.bottom); + cubicTo(rect.left + radiusX - dx, rect.bottom, rect.left, rect.bottom - radiusY + dy, rect.left, + rect.bottom - radiusY); + lineTo(rect.left, rect.top + radiusY); + cubicTo(rect.left, rect.top + radiusY - dy, rect.left + radiusX - dx, rect.top, + rect.left + radiusX, rect.top); + close(); +} + +void PathData::clear() { + _verbs.clear(); + _points.clear(); + _cachedBounds.setEmpty(); + _boundsDirty = true; +} + +void PathData::transform(const Matrix& matrix) { + if (matrix.isIdentity()) { + return; + } + for (size_t i = 0; i < _points.size(); i += 2) { + Point p = {_points[i], _points[i + 1]}; + Point transformed = matrix.mapPoint(p); + _points[i] = transformed.x; + _points[i + 1] = transformed.y; + } + _boundsDirty = true; +} + +Rect PathData::getBounds() const { + if (!_boundsDirty) { + return _cachedBounds; + } + + _cachedBounds.setEmpty(); + if (_points.empty()) { + _boundsDirty = false; + return _cachedBounds; + } + + float minX = _points[0]; + float minY = _points[1]; + float maxX = _points[0]; + float maxY = _points[1]; + + for (size_t i = 2; i < _points.size(); i += 2) { + float x = _points[i]; + float y = _points[i + 1]; + minX = std::min(minX, x); + minY = std::min(minY, y); + maxX = std::max(maxX, x); + maxY = std::max(maxY, y); + } + + _cachedBounds = Rect::MakeLTRB(minX, minY, maxX, maxY); + _boundsDirty = false; + return _cachedBounds; +} + +std::string PathData::toSVGString() const { + std::ostringstream oss; + oss.precision(6); + + size_t pointIndex = 0; + for (auto verb : _verbs) { + const float* pts = _points.data() + pointIndex; + switch (verb) { + case PathVerb::Move: + oss << "M" << pts[0] << " " << pts[1]; + break; + case PathVerb::Line: + oss << "L" << pts[0] << " " << pts[1]; + break; + case PathVerb::Quad: + oss << "Q" << pts[0] << " " << pts[1] << " " << pts[2] << " " << pts[3]; + break; + case PathVerb::Cubic: + oss << "C" << pts[0] << " " << pts[1] << " " << pts[2] << " " << pts[3] << " " << pts[4] + << " " << pts[5]; + break; + case PathVerb::Close: + oss << "Z"; + break; + } + pointIndex += PointsPerVerb(verb) * 2; + } + + return oss.str(); +} + +// SVG path parser helper functions +static void SkipWhitespaceAndCommas(const char*& ptr, const char* end) { + while (ptr < end && (std::isspace(*ptr) || *ptr == ',')) { + ++ptr; + } +} + +static bool ParseNumber(const char*& ptr, const char* end, float& result) { + SkipWhitespaceAndCommas(ptr, end); + if (ptr >= end) { + return false; + } + + const char* start = ptr; + if (*ptr == '-' || *ptr == '+') { + ++ptr; + } + bool hasDigits = false; + while (ptr < end && std::isdigit(*ptr)) { + ++ptr; + hasDigits = true; + } + if (ptr < end && *ptr == '.') { + ++ptr; + while (ptr < end && std::isdigit(*ptr)) { + ++ptr; + hasDigits = true; + } + } + if (ptr < end && (*ptr == 'e' || *ptr == 'E')) { + ++ptr; + if (ptr < end && (*ptr == '-' || *ptr == '+')) { + ++ptr; + } + while (ptr < end && std::isdigit(*ptr)) { + ++ptr; + } + } + + if (!hasDigits) { + ptr = start; + return false; + } + + std::string numStr(start, ptr); + result = std::stof(numStr); + return true; +} + +static bool ParseFlag(const char*& ptr, const char* end, bool& result) { + SkipWhitespaceAndCommas(ptr, end); + if (ptr >= end) { + return false; + } + if (*ptr == '0') { + result = false; + ++ptr; + return true; + } + if (*ptr == '1') { + result = true; + ++ptr; + return true; + } + return false; +} + +// Convert arc to cubic Bezier curves +static void ArcToCubics(PathData& path, float x1, float y1, float rx, float ry, float angle, + bool largeArc, bool sweep, float x2, float y2) { + if (rx == 0 || ry == 0) { + path.lineTo(x2, y2); + return; + } + + rx = std::abs(rx); + ry = std::abs(ry); + + float radians = angle * 3.14159265358979323846f / 180.0f; + float cosAngle = std::cos(radians); + float sinAngle = std::sin(radians); + + float dx2 = (x1 - x2) / 2.0f; + float dy2 = (y1 - y2) / 2.0f; + + float x1p = cosAngle * dx2 + sinAngle * dy2; + float y1p = -sinAngle * dx2 + cosAngle * dy2; + + float lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry); + if (lambda > 1.0f) { + float sqrtLambda = std::sqrt(lambda); + rx *= sqrtLambda; + ry *= sqrtLambda; + } + + float rx2 = rx * rx; + float ry2 = ry * ry; + float x1p2 = x1p * x1p; + float y1p2 = y1p * y1p; + + float num = rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2; + float den = rx2 * y1p2 + ry2 * x1p2; + float sq = (num < 0 || den == 0) ? 0.0f : std::sqrt(num / den); + + if (largeArc == sweep) { + sq = -sq; + } + + float cxp = sq * rx * y1p / ry; + float cyp = -sq * ry * x1p / rx; + + float cx = cosAngle * cxp - sinAngle * cyp + (x1 + x2) / 2.0f; + float cy = sinAngle * cxp + cosAngle * cyp + (y1 + y2) / 2.0f; + + auto vectorAngle = [](float ux, float uy, float vx, float vy) -> float { + float n = std::sqrt(ux * ux + uy * uy) * std::sqrt(vx * vx + vy * vy); + if (n == 0) { + return 0; + } + float c = (ux * vx + uy * vy) / n; + c = std::max(-1.0f, std::min(1.0f, c)); + float angle = std::acos(c); + if (ux * vy - uy * vx < 0) { + angle = -angle; + } + return angle; + }; + + float theta1 = vectorAngle(1.0f, 0.0f, (x1p - cxp) / rx, (y1p - cyp) / ry); + float dtheta = vectorAngle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, + (-y1p - cyp) / ry); + + if (!sweep && dtheta > 0) { + dtheta -= 2.0f * 3.14159265358979323846f; + } else if (sweep && dtheta < 0) { + dtheta += 2.0f * 3.14159265358979323846f; + } + + int segments = static_cast(std::ceil(std::abs(dtheta) / (3.14159265358979323846f / 2.0f))); + float segmentAngle = dtheta / segments; + + float t = std::tan(segmentAngle / 2.0f); + float alpha = std::sin(segmentAngle) * (std::sqrt(4.0f + 3.0f * t * t) - 1.0f) / 3.0f; + + float currentAngle = theta1; + float currentX = x1; + float currentY = y1; + + for (int i = 0; i < segments; ++i) { + float nextAngle = currentAngle + segmentAngle; + + float cosStart = std::cos(currentAngle); + float sinStart = std::sin(currentAngle); + float cosEnd = std::cos(nextAngle); + float sinEnd = std::sin(nextAngle); + + float ex = cx + rx * (cosAngle * cosEnd - sinAngle * sinEnd); + float ey = cy + rx * (sinAngle * cosEnd + cosAngle * sinEnd); + + float dx1 = -rx * (cosAngle * sinStart + sinAngle * cosStart); + float dy1 = -rx * (sinAngle * sinStart - cosAngle * cosStart); + float dx2 = -rx * (cosAngle * sinEnd + sinAngle * cosEnd); + float dy2 = -rx * (sinAngle * sinEnd - cosAngle * cosEnd); + + float c1x = currentX + alpha * dx1; + float c1y = currentY + alpha * dy1; + float c2x = ex - alpha * dx2; + float c2y = ey - alpha * dy2; + + path.cubicTo(c1x, c1y, c2x, c2y, ex, ey); + + currentAngle = nextAngle; + currentX = ex; + currentY = ey; + } +} + +PathData PathData::FromSVGString(const std::string& d) { + PathData path; + if (d.empty()) { + return path; + } + + const char* ptr = d.c_str(); + const char* end = ptr + d.length(); + + float currentX = 0; + float currentY = 0; + float startX = 0; + float startY = 0; + float lastControlX = 0; + float lastControlY = 0; + char lastCommand = 0; + + while (ptr < end) { + SkipWhitespaceAndCommas(ptr, end); + if (ptr >= end) { + break; + } + + char command = *ptr; + if (std::isalpha(command)) { + ++ptr; + } else { + command = lastCommand; + } + + bool isRelative = std::islower(command); + char upperCommand = std::toupper(command); + + switch (upperCommand) { + case 'M': { + float x, y; + if (!ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x += currentX; + y += currentY; + } + path.moveTo(x, y); + currentX = startX = x; + currentY = startY = y; + lastCommand = isRelative ? 'l' : 'L'; + break; + } + case 'L': { + float x, y; + if (!ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x += currentX; + y += currentY; + } + path.lineTo(x, y); + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'H': { + float x; + if (!ParseNumber(ptr, end, x)) { + break; + } + if (isRelative) { + x += currentX; + } + path.lineTo(x, currentY); + currentX = x; + lastCommand = command; + break; + } + case 'V': { + float y; + if (!ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + y += currentY; + } + path.lineTo(currentX, y); + currentY = y; + lastCommand = command; + break; + } + case 'C': { + float x1, y1, x2, y2, x, y; + if (!ParseNumber(ptr, end, x1) || !ParseNumber(ptr, end, y1) || + !ParseNumber(ptr, end, x2) || !ParseNumber(ptr, end, y2) || + !ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x1 += currentX; + y1 += currentY; + x2 += currentX; + y2 += currentY; + x += currentX; + y += currentY; + } + path.cubicTo(x1, y1, x2, y2, x, y); + lastControlX = x2; + lastControlY = y2; + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'S': { + float x2, y2, x, y; + if (!ParseNumber(ptr, end, x2) || !ParseNumber(ptr, end, y2) || + !ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x2 += currentX; + y2 += currentY; + x += currentX; + y += currentY; + } + float x1 = currentX; + float y1 = currentY; + char lastUpper = std::toupper(lastCommand); + if (lastUpper == 'C' || lastUpper == 'S') { + x1 = 2 * currentX - lastControlX; + y1 = 2 * currentY - lastControlY; + } + path.cubicTo(x1, y1, x2, y2, x, y); + lastControlX = x2; + lastControlY = y2; + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'Q': { + float x1, y1, x, y; + if (!ParseNumber(ptr, end, x1) || !ParseNumber(ptr, end, y1) || + !ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x1 += currentX; + y1 += currentY; + x += currentX; + y += currentY; + } + path.quadTo(x1, y1, x, y); + lastControlX = x1; + lastControlY = y1; + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'T': { + float x, y; + if (!ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x += currentX; + y += currentY; + } + float x1 = currentX; + float y1 = currentY; + char lastUpper = std::toupper(lastCommand); + if (lastUpper == 'Q' || lastUpper == 'T') { + x1 = 2 * currentX - lastControlX; + y1 = 2 * currentY - lastControlY; + } + path.quadTo(x1, y1, x, y); + lastControlX = x1; + lastControlY = y1; + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'A': { + float rx, ry, angle, x, y; + bool largeArc, sweep; + if (!ParseNumber(ptr, end, rx) || !ParseNumber(ptr, end, ry) || + !ParseNumber(ptr, end, angle) || !ParseFlag(ptr, end, largeArc) || + !ParseFlag(ptr, end, sweep) || !ParseNumber(ptr, end, x) || + !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x += currentX; + y += currentY; + } + ArcToCubics(path, currentX, currentY, rx, ry, angle, largeArc, sweep, x, y); + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'Z': { + path.close(); + currentX = startX; + currentY = startY; + lastCommand = command; + break; + } + default: + ++ptr; + break; + } + } + + return path; +} + +} // namespace pagx diff --git a/pagx/src/layers/LayerBuilder.cpp b/pagx/src/layers/LayerBuilder.cpp deleted file mode 100644 index 2d2b53ebae..0000000000 --- a/pagx/src/layers/LayerBuilder.cpp +++ /dev/null @@ -1,62 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "pagx/layers/LayerBuilder.h" -#include "PAGXParser.h" -#include "tgfx/core/Data.h" -#include "tgfx/svg/xml/XMLDOM.h" - -namespace pagx { - -PAGXContent LayerBuilder::FromFile(const std::string& filePath) { - auto data = tgfx::Data::MakeFromFile(filePath); - if (!data) { - return {}; - } - auto stream = tgfx::Stream::MakeFromData(data); - if (!stream) { - return {}; - } - - std::string basePath; - auto lastSlash = filePath.find_last_of("/\\"); - if (lastSlash != std::string::npos) { - basePath = filePath.substr(0, lastSlash); - } - - return FromStream(*stream, basePath); -} - -PAGXContent LayerBuilder::FromStream(tgfx::Stream& stream, const std::string& basePath) { - auto dom = tgfx::DOM::Make(stream); - if (!dom) { - return {}; - } - auto rootNode = dom->getRootNode(); - if (!rootNode || rootNode->name != "pagx") { - return {}; - } - - PAGXParser parser(rootNode, basePath); - if (!parser.parse()) { - return {}; - } - return {parser.getRoot(), parser.width(), parser.height()}; -} - -} // namespace pagx diff --git a/pagx/src/layers/PAGXAttributes.cpp b/pagx/src/layers/PAGXAttributes.cpp deleted file mode 100644 index 07becf54bb..0000000000 --- a/pagx/src/layers/PAGXAttributes.cpp +++ /dev/null @@ -1,336 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "PAGXAttributes.h" -#include -#include -#include -#include - -namespace pagx { - -float PAGXAttributes::ParseFloat(const std::shared_ptr& node, const std::string& name, - float defaultValue) { - auto [found, value] = node->findAttribute(name); - if (!found || value.empty()) { - return defaultValue; - } - return std::strtof(value.c_str(), nullptr); -} - -bool PAGXAttributes::ParseBool(const std::shared_ptr& node, const std::string& name, - bool defaultValue) { - auto [found, value] = node->findAttribute(name); - if (!found || value.empty()) { - return defaultValue; - } - if (value == "true" || value == "1") { - return true; - } - if (value == "false" || value == "0") { - return false; - } - return defaultValue; -} - -std::string PAGXAttributes::ParseString(const std::shared_ptr& node, - const std::string& name, const std::string& defaultValue) { - auto [found, value] = node->findAttribute(name); - if (!found) { - return defaultValue; - } - return value; -} - -static int HexCharToInt(char c) { - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'a' && c <= 'f') { - return c - 'a' + 10; - } - if (c >= 'A' && c <= 'F') { - return c - 'A' + 10; - } - return 0; -} - -static Color ParseHexColor(const std::string& hex) { - size_t start = (hex[0] == '#') ? 1 : 0; - std::string h = hex.substr(start); - float r = 0.0f; - float g = 0.0f; - float b = 0.0f; - float a = 1.0f; - - if (h.length() == 3) { - // #RGB -> #RRGGBB - r = static_cast(HexCharToInt(h[0]) * 17) / 255.0f; - g = static_cast(HexCharToInt(h[1]) * 17) / 255.0f; - b = static_cast(HexCharToInt(h[2]) * 17) / 255.0f; - } else if (h.length() == 6) { - // #RRGGBB - r = static_cast(HexCharToInt(h[0]) * 16 + HexCharToInt(h[1])) / 255.0f; - g = static_cast(HexCharToInt(h[2]) * 16 + HexCharToInt(h[3])) / 255.0f; - b = static_cast(HexCharToInt(h[4]) * 16 + HexCharToInt(h[5])) / 255.0f; - } else if (h.length() == 8) { - // #RRGGBBAA - r = static_cast(HexCharToInt(h[0]) * 16 + HexCharToInt(h[1])) / 255.0f; - g = static_cast(HexCharToInt(h[2]) * 16 + HexCharToInt(h[3])) / 255.0f; - b = static_cast(HexCharToInt(h[4]) * 16 + HexCharToInt(h[5])) / 255.0f; - a = static_cast(HexCharToInt(h[6]) * 16 + HexCharToInt(h[7])) / 255.0f; - } - return {r, g, b, a}; -} - -static Color ParseRGBColor(const std::string& value) { - // rgb(255,0,0) or rgba(255,0,0,0.5) - bool hasAlpha = value.find("rgba") != std::string::npos; - size_t start = hasAlpha ? 5 : 4; - size_t end = value.find(')'); - std::string content = value.substr(start, end - start); - - std::vector values; - std::istringstream stream(content); - std::string token; - while (std::getline(stream, token, ',')) { - values.push_back(std::strtof(token.c_str(), nullptr)); - } - - float r = values.size() > 0 ? values[0] / 255.0f : 0.0f; - float g = values.size() > 1 ? values[1] / 255.0f : 0.0f; - float b = values.size() > 2 ? values[2] / 255.0f : 0.0f; - float a = values.size() > 3 ? values[3] : 1.0f; - return {r, g, b, a}; -} - -Color PAGXAttributes::ParseColor(const std::string& value) { - if (value.empty()) { - return Color::Black(); - } - if (value[0] == '#') { - return ParseHexColor(value); - } - if (value.find("rgb") == 0) { - return ParseRGBColor(value); - } - // TODO: Add HSL and color() support per spec - // Default fallback - return Color::Black(); -} - -Point PAGXAttributes::ParsePoint(const std::string& value, Point defaultValue) { - if (value.empty()) { - return defaultValue; - } - size_t commaPos = value.find(','); - if (commaPos == std::string::npos) { - return defaultValue; - } - float x = std::strtof(value.substr(0, commaPos).c_str(), nullptr); - float y = std::strtof(value.substr(commaPos + 1).c_str(), nullptr); - return {x, y}; -} - -Matrix PAGXAttributes::ParseMatrix(const std::string& value) { - if (value.empty()) { - return Matrix::I(); - } - std::vector values; - std::istringstream stream(value); - std::string token; - while (std::getline(stream, token, ',')) { - values.push_back(std::strtof(token.c_str(), nullptr)); - } - if (values.size() >= 6) { - // a,b,c,d,tx,ty - return Matrix::MakeAll(values[0], values[2], values[4], values[1], values[3], values[5]); - } - return Matrix::I(); -} - -BlendMode PAGXAttributes::ParseBlendMode(const std::string& value) { - if (value == "multiply") { - return BlendMode::Multiply; - } - if (value == "screen") { - return BlendMode::Screen; - } - if (value == "overlay") { - return BlendMode::Overlay; - } - if (value == "darken") { - return BlendMode::Darken; - } - if (value == "lighten") { - return BlendMode::Lighten; - } - if (value == "colorDodge") { - return BlendMode::ColorDodge; - } - if (value == "colorBurn") { - return BlendMode::ColorBurn; - } - if (value == "hardLight") { - return BlendMode::HardLight; - } - if (value == "softLight") { - return BlendMode::SoftLight; - } - if (value == "difference") { - return BlendMode::Difference; - } - if (value == "exclusion") { - return BlendMode::Exclusion; - } - if (value == "hue") { - return BlendMode::Hue; - } - if (value == "saturation") { - return BlendMode::Saturation; - } - if (value == "color") { - return BlendMode::Color; - } - if (value == "luminosity") { - return BlendMode::Luminosity; - } - if (value == "add") { - return BlendMode::PlusLighter; - } - return BlendMode::SrcOver; -} - -PathFillType PAGXAttributes::ParseFillRule(const std::string& value) { - if (value == "evenOdd") { - return PathFillType::EvenOdd; - } - return PathFillType::Winding; -} - -LineCap PAGXAttributes::ParseLineCap(const std::string& value) { - if (value == "round") { - return LineCap::Round; - } - if (value == "square") { - return LineCap::Square; - } - return LineCap::Butt; -} - -LineJoin PAGXAttributes::ParseLineJoin(const std::string& value) { - if (value == "round") { - return LineJoin::Round; - } - if (value == "bevel") { - return LineJoin::Bevel; - } - return LineJoin::Miter; -} - -TileMode PAGXAttributes::ParseTileMode(const std::string& value) { - if (value == "repeat") { - return TileMode::Repeat; - } - if (value == "mirror") { - return TileMode::Mirror; - } - if (value == "decal") { - return TileMode::Decal; - } - return TileMode::Clamp; -} - -LayerMaskType PAGXAttributes::ParseMaskType(const std::string& value) { - if (value == "luminance") { - return LayerMaskType::Luminance; - } - if (value == "contour") { - return LayerMaskType::Contour; - } - return LayerMaskType::Alpha; -} - -PolystarType PAGXAttributes::ParsePolystarType(const std::string& value) { - if (value == "polygon") { - return PolystarType::Polygon; - } - return PolystarType::Star; -} - -TrimPathType PAGXAttributes::ParseTrimPathType(const std::string& value) { - if (value == "continuous") { - return TrimPathType::Continuous; - } - return TrimPathType::Separate; -} - -MergePathOp PAGXAttributes::ParseMergePathOp(const std::string& value) { - if (value == "union") { - return MergePathOp::Union; - } - if (value == "intersect") { - return MergePathOp::Intersect; - } - if (value == "xor") { - return MergePathOp::XOR; - } - if (value == "difference") { - return MergePathOp::Difference; - } - return MergePathOp::Append; -} - -StrokeAlign PAGXAttributes::ParseStrokeAlign(const std::string& value) { - if (value == "inside") { - return StrokeAlign::Inside; - } - if (value == "outside") { - return StrokeAlign::Outside; - } - return StrokeAlign::Center; -} - -std::vector PAGXAttributes::ParseDashes(const std::string& value) { - if (value.empty()) { - return {}; - } - std::vector dashes; - std::istringstream stream(value); - std::string token; - while (std::getline(stream, token, ',')) { - dashes.push_back(std::strtof(token.c_str(), nullptr)); - } - return dashes; -} - -std::string PAGXAttributes::GetTextContent(const std::shared_ptr& node) { - if (!node) { - return ""; - } - auto child = node->firstChild; - while (child) { - if (child->type == DOMNodeType::Text) { - return child->name; - } - child = child->nextSibling; - } - return ""; -} - -} // namespace pagx diff --git a/pagx/src/layers/PAGXAttributes.h b/pagx/src/layers/PAGXAttributes.h deleted file mode 100644 index 3ecddcb37c..0000000000 --- a/pagx/src/layers/PAGXAttributes.h +++ /dev/null @@ -1,84 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "tgfx/core/BlendMode.h" -#include "tgfx/core/Color.h" -#include "tgfx/core/Matrix.h" -#include "tgfx/core/Path.h" -#include "tgfx/core/Point.h" -#include "tgfx/core/Stroke.h" -#include "tgfx/core/TileMode.h" -#include "tgfx/layers/LayerMaskType.h" -#include "tgfx/layers/StrokeAlign.h" -#include "tgfx/layers/vectors/FillStyle.h" -#include "tgfx/layers/vectors/MergePath.h" -#include "tgfx/layers/vectors/Polystar.h" -#include "tgfx/layers/vectors/TrimPath.h" -#include "tgfx/svg/xml/XMLDOM.h" - -namespace pagx { - -using namespace tgfx; - -class PAGXAttributes { - public: - static float ParseFloat(const std::shared_ptr& node, const std::string& name, - float defaultValue = 0.0f); - - static bool ParseBool(const std::shared_ptr& node, const std::string& name, - bool defaultValue = true); - - static std::string ParseString(const std::shared_ptr& node, const std::string& name, - const std::string& defaultValue = ""); - - static Color ParseColor(const std::string& value); - - static Point ParsePoint(const std::string& value, Point defaultValue = Point::Zero()); - - static Matrix ParseMatrix(const std::string& value); - - static BlendMode ParseBlendMode(const std::string& value); - - static PathFillType ParseFillRule(const std::string& value); - - static LineCap ParseLineCap(const std::string& value); - - static LineJoin ParseLineJoin(const std::string& value); - - static TileMode ParseTileMode(const std::string& value); - - static LayerMaskType ParseMaskType(const std::string& value); - - static PolystarType ParsePolystarType(const std::string& value); - - static TrimPathType ParseTrimPathType(const std::string& value); - - static MergePathOp ParseMergePathOp(const std::string& value); - - static StrokeAlign ParseStrokeAlign(const std::string& value); - - static std::vector ParseDashes(const std::string& value); - - static std::string GetTextContent(const std::shared_ptr& node); -}; - -} // namespace pagx diff --git a/pagx/src/layers/PAGXParser.cpp b/pagx/src/layers/PAGXParser.cpp deleted file mode 100644 index 0d871b5477..0000000000 --- a/pagx/src/layers/PAGXParser.cpp +++ /dev/null @@ -1,796 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "PAGXParser.h" -#include "PAGXAttributes.h" -#include "PAGXUtils.h" -#include "pagx/layers/TextLayouter.h" -#include "tgfx/core/Data.h" -#include "tgfx/core/Font.h" -#include "tgfx/core/Typeface.h" -#include "tgfx/layers/VectorLayer.h" -#include "tgfx/layers/filters/BlurFilter.h" -#include "tgfx/layers/filters/DropShadowFilter.h" -#include "tgfx/layers/layerstyles/DropShadowStyle.h" -#include "tgfx/layers/layerstyles/InnerShadowStyle.h" -#include "tgfx/layers/vectors/Ellipse.h" -#include "tgfx/layers/vectors/FillStyle.h" -#include "tgfx/layers/vectors/Gradient.h" -#include "tgfx/layers/vectors/ImagePattern.h" -#include "tgfx/layers/vectors/MergePath.h" -#include "tgfx/layers/vectors/Polystar.h" -#include "tgfx/layers/vectors/Rectangle.h" -#include "tgfx/layers/vectors/Repeater.h" -#include "tgfx/layers/vectors/RoundCorner.h" -#include "tgfx/layers/vectors/ShapePath.h" -#include "tgfx/layers/vectors/SolidColor.h" -#include "tgfx/layers/vectors/StrokeStyle.h" -#include "tgfx/layers/vectors/TextSpan.h" -#include "tgfx/layers/vectors/TrimPath.h" -#include "tgfx/layers/vectors/VectorGroup.h" -#include "tgfx/svg/SVGPathParser.h" - -namespace pagx { - -PAGXParser::PAGXParser(std::shared_ptr rootNode, const std::string& basePath) - : rootNode_(std::move(rootNode)), basePath_(basePath) { -} - -bool PAGXParser::parse() { - if (!rootNode_ || rootNode_->name != "pagx") { - return false; - } - width_ = PAGXAttributes::ParseFloat(rootNode_, "width", 0.0f); - height_ = PAGXAttributes::ParseFloat(rootNode_, "height", 0.0f); - if (width_ <= 0 || height_ <= 0) { - return false; - } - - rootLayer = Layer::Make(); - rootLayer->setName("root"); - - auto child = rootNode_->firstChild; - while (child) { - if (child->name == "Resources") { - parseResources(child); - } else if (child->name == "Layer") { - auto layer = parseLayer(child); - if (layer) { - rootLayer->addChild(layer); - } - } - child = child->nextSibling; - } - return true; -} - -void PAGXParser::parseResources(const std::shared_ptr& node) { - auto child = node->firstChild; - while (child) { - if (child->name == "Image") { - auto [found, id] = child->findAttribute("id"); - auto [srcFound, source] = child->findAttribute("source"); - if (found && srcFound && !id.empty()) { - auto image = resolveImageReference(source); - if (image) { - images[id] = image; - } - } - } else if (child->name == "SolidColor" || child->name == "LinearGradient" || - child->name == "RadialGradient" || child->name == "ConicGradient" || - child->name == "DiamondGradient" || child->name == "ImagePattern") { - auto [found, id] = child->findAttribute("id"); - if (found && !id.empty()) { - auto colorSource = parseColorSource(child); - if (colorSource) { - colorSources[id] = colorSource; - } - } - } - child = child->nextSibling; - } -} - -std::shared_ptr PAGXParser::parseLayer(const std::shared_ptr& node) { - auto [hasComposition, composition] = node->findAttribute("composition"); - if (hasComposition) { - // Composition reference is not supported in this version - return nullptr; - } - - auto layer = Layer::Make(); - - // Parse layer attributes - auto name = PAGXAttributes::ParseString(node, "name", ""); - layer->setName(name); - - auto visible = PAGXAttributes::ParseBool(node, "visible", true); - layer->setVisible(visible); - - auto alpha = PAGXAttributes::ParseFloat(node, "alpha", 1.0f); - layer->setAlpha(alpha); - - auto blendModeStr = PAGXAttributes::ParseString(node, "blendMode", "normal"); - layer->setBlendMode(PAGXAttributes::ParseBlendMode(blendModeStr)); - - auto x = PAGXAttributes::ParseFloat(node, "x", 0.0f); - auto y = PAGXAttributes::ParseFloat(node, "y", 0.0f); - layer->setPosition({x, y}); - - auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); - if (!matrixStr.empty()) { - layer->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); - } - - auto antiAlias = PAGXAttributes::ParseBool(node, "antiAlias", true); - layer->setAllowsEdgeAntialiasing(antiAlias); - - auto groupOpacity = PAGXAttributes::ParseBool(node, "groupOpacity", false); - layer->setAllowsGroupOpacity(groupOpacity); - - // Parse id for reference - auto [hasId, id] = node->findAttribute("id"); - if (hasId && !id.empty()) { - layerIdMap[id] = layer; - } - - // Parse children nodes - std::vector> contents; - std::vector> filters; - std::vector> styles; - std::string maskRef; - std::string maskTypeStr; - - auto child = node->firstChild; - while (child) { - if (child->name == "contents") { - contents = parseContents(child); - } else if (child->name == "filters") { - filters = parseFilters(child); - } else if (child->name == "styles") { - styles = parseStyles(child); - } else if (child->name == "Layer") { - auto childLayer = parseLayer(child); - if (childLayer) { - layer->addChild(childLayer); - } - } - child = child->nextSibling; - } - - // Apply contents to VectorLayer if present - if (!contents.empty()) { - auto vectorLayer = VectorLayer::Make(); - vectorLayer->setContents(std::move(contents)); - layer->addChildAt(vectorLayer, 0); - } - - // Apply filters and styles - if (!filters.empty()) { - layer->setFilters(filters); - } - if (!styles.empty()) { - layer->setLayerStyles(styles); - } - - // Handle mask reference - auto [hasMask, mask] = node->findAttribute("mask"); - if (hasMask && !mask.empty() && mask[0] == '#') { - maskRef = mask.substr(1); - auto [hasMaskType, maskType] = node->findAttribute("maskType"); - maskTypeStr = hasMaskType ? maskType : "alpha"; - } - - // Mask will be resolved later in a second pass - if (!maskRef.empty()) { - auto it = layerIdMap.find(maskRef); - if (it != layerIdMap.end()) { - layer->setMask(it->second); - layer->setMaskType(PAGXAttributes::ParseMaskType(maskTypeStr)); - } - } - - return layer; -} - -std::vector> PAGXParser::parseContents( - const std::shared_ptr& node) { - std::vector> elements; - auto child = node->firstChild; - while (child) { - auto element = parseVectorElement(child); - if (element) { - elements.push_back(element); - } - child = child->nextSibling; - } - return elements; -} - -std::shared_ptr PAGXParser::parseVectorElement( - const std::shared_ptr& node) { - const auto& name = node->name; - if (name == "Group") { - return parseGroup(node); - } - if (name == "Rectangle") { - return parseRectangle(node); - } - if (name == "Ellipse") { - return parseEllipse(node); - } - if (name == "Polystar") { - return parsePolystar(node); - } - if (name == "Path") { - return parsePath(node); - } - if (name == "TextSpan") { - return parseTextSpan(node); - } - if (name == "Fill") { - return parseFill(node); - } - if (name == "Stroke") { - return parseStroke(node); - } - if (name == "TrimPath") { - return parseTrimPath(node); - } - if (name == "RoundCorner") { - return parseRoundCorner(node); - } - if (name == "MergePath") { - return parseMergePath(node); - } - if (name == "Repeater") { - return parseRepeater(node); - } - return nullptr; -} - -std::shared_ptr PAGXParser::parseGroup(const std::shared_ptr& node) { - auto group = std::make_shared(); - - auto anchorStr = PAGXAttributes::ParseString(node, "anchor", "0,0"); - group->setAnchorPoint(PAGXAttributes::ParsePoint(anchorStr)); - - auto positionStr = PAGXAttributes::ParseString(node, "position", "0,0"); - group->setPosition(PAGXAttributes::ParsePoint(positionStr)); - - auto scaleStr = PAGXAttributes::ParseString(node, "scale", "1,1"); - group->setScale(PAGXAttributes::ParsePoint(scaleStr, {1.0f, 1.0f})); - - group->setRotation(PAGXAttributes::ParseFloat(node, "rotation", 0.0f)); - group->setAlpha(PAGXAttributes::ParseFloat(node, "alpha", 1.0f)); - group->setSkew(PAGXAttributes::ParseFloat(node, "skew", 0.0f)); - group->setSkewAxis(PAGXAttributes::ParseFloat(node, "skewAxis", 0.0f)); - - std::vector> elements; - auto child = node->firstChild; - while (child) { - auto element = parseVectorElement(child); - if (element) { - elements.push_back(element); - } - child = child->nextSibling; - } - group->setElements(std::move(elements)); - return group; -} - -std::shared_ptr PAGXParser::parseRectangle(const std::shared_ptr& node) { - auto rect = std::make_shared(); - auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); - auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); - rect->setCenter({centerX, centerY}); - auto width = PAGXAttributes::ParseFloat(node, "width", 100.0f); - auto height = PAGXAttributes::ParseFloat(node, "height", 100.0f); - rect->setSize({width, height}); - rect->setRoundness(PAGXAttributes::ParseFloat(node, "roundness", 0.0f)); - rect->setReversed(PAGXAttributes::ParseBool(node, "reversed", false)); - return rect; -} - -std::shared_ptr PAGXParser::parseEllipse(const std::shared_ptr& node) { - auto ellipse = std::make_shared(); - auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); - auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); - ellipse->setCenter({centerX, centerY}); - auto width = PAGXAttributes::ParseFloat(node, "width", 100.0f); - auto height = PAGXAttributes::ParseFloat(node, "height", 100.0f); - ellipse->setSize({width, height}); - ellipse->setReversed(PAGXAttributes::ParseBool(node, "reversed", false)); - return ellipse; -} - -std::shared_ptr PAGXParser::parsePolystar(const std::shared_ptr& node) { - auto polystar = std::make_shared(); - auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); - auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); - polystar->setCenter({centerX, centerY}); - auto typeStr = PAGXAttributes::ParseString(node, "type", "star"); - polystar->setPolystarType(PAGXAttributes::ParsePolystarType(typeStr)); - polystar->setPointCount(PAGXAttributes::ParseFloat(node, "points", 5.0f)); - polystar->setOuterRadius(PAGXAttributes::ParseFloat(node, "outerRadius", 100.0f)); - polystar->setInnerRadius(PAGXAttributes::ParseFloat(node, "innerRadius", 50.0f)); - polystar->setRotation(PAGXAttributes::ParseFloat(node, "rotation", 0.0f)); - polystar->setOuterRoundness(PAGXAttributes::ParseFloat(node, "outerRoundness", 0.0f)); - polystar->setInnerRoundness(PAGXAttributes::ParseFloat(node, "innerRoundness", 0.0f)); - polystar->setReversed(PAGXAttributes::ParseBool(node, "reversed", false)); - return polystar; -} - -std::shared_ptr PAGXParser::parsePath(const std::shared_ptr& node) { - auto shapePath = std::make_shared(); - auto d = PAGXAttributes::ParseString(node, "d", ""); - if (!d.empty()) { - auto path = SVGPathParser::FromSVGString(d); - if (path) { - shapePath->setPath(*path); - } - } - shapePath->setReversed(PAGXAttributes::ParseBool(node, "reversed", false)); - return shapePath; -} - -std::shared_ptr PAGXParser::parseTextSpan(const std::shared_ptr& node) { - auto textSpan = std::make_shared(); - auto x = PAGXAttributes::ParseFloat(node, "x", 0.0f); - auto y = PAGXAttributes::ParseFloat(node, "y", 0.0f); - - auto textContent = PAGXAttributes::GetTextContent(node); - auto fontFamily = PAGXAttributes::ParseString(node, "font", ""); - auto fontSize = PAGXAttributes::ParseFloat(node, "fontSize", 12.0f); - - // Try to create typeface from font name - auto typeface = fontFamily.empty() ? nullptr : Typeface::MakeFromName(fontFamily, ""); - - Font font(typeface, fontSize); - auto textBlob = TextLayouter::Layout(textContent, font); - textSpan->setTextBlob(textBlob); - - // Parse text anchor and adjust position accordingly - auto textAnchorStr = PAGXAttributes::ParseString(node, "textAnchor", "start"); - if (textBlob && textAnchorStr != "start") { - auto bounds = textBlob->getTightBounds(); - if (textAnchorStr == "middle") { - x -= bounds.width() * 0.5f; - } else if (textAnchorStr == "end") { - x -= bounds.width(); - } - } - - textSpan->setPosition({x, y}); - - return textSpan; -} - -std::shared_ptr PAGXParser::parseFill(const std::shared_ptr& node) { - auto fill = std::make_shared(); - - // Try to get color from attribute - auto colorStr = PAGXAttributes::ParseString(node, "color", ""); - if (!colorStr.empty()) { - if (colorStr[0] == '#' && colorStr.length() > 1 && !std::isxdigit(colorStr[1])) { - // Reference to color source: #gradientId - fill->setColorSource(resolveColorReference(colorStr)); - } else { - // Direct color value - fill->setColorSource(SolidColor::Make(PAGXAttributes::ParseColor(colorStr))); - } - } else { - // Try inline color source - auto inlineColor = parseInlineColorSource(node); - if (inlineColor) { - fill->setColorSource(inlineColor); - } - } - - fill->setAlpha(PAGXAttributes::ParseFloat(node, "alpha", 1.0f)); - auto blendModeStr = PAGXAttributes::ParseString(node, "blendMode", "normal"); - fill->setBlendMode(PAGXAttributes::ParseBlendMode(blendModeStr)); - auto fillRuleStr = PAGXAttributes::ParseString(node, "fillRule", "winding"); - fill->setFillRule(fillRuleStr == "evenOdd" ? FillRule::EvenOdd : FillRule::Winding); - - return fill; -} - -std::shared_ptr PAGXParser::parseStroke(const std::shared_ptr& node) { - auto stroke = std::make_shared(); - - auto colorStr = PAGXAttributes::ParseString(node, "color", ""); - if (!colorStr.empty()) { - if (colorStr[0] == '#' && colorStr.length() > 1 && !std::isxdigit(colorStr[1])) { - stroke->setColorSource(resolveColorReference(colorStr)); - } else { - stroke->setColorSource(SolidColor::Make(PAGXAttributes::ParseColor(colorStr))); - } - } else { - auto inlineColor = parseInlineColorSource(node); - if (inlineColor) { - stroke->setColorSource(inlineColor); - } - } - - stroke->setStrokeWidth(PAGXAttributes::ParseFloat(node, "width", 1.0f)); - stroke->setAlpha(PAGXAttributes::ParseFloat(node, "alpha", 1.0f)); - auto blendModeStr = PAGXAttributes::ParseString(node, "blendMode", "normal"); - stroke->setBlendMode(PAGXAttributes::ParseBlendMode(blendModeStr)); - - auto capStr = PAGXAttributes::ParseString(node, "cap", "butt"); - stroke->setLineCap(PAGXAttributes::ParseLineCap(capStr)); - - auto joinStr = PAGXAttributes::ParseString(node, "join", "miter"); - stroke->setLineJoin(PAGXAttributes::ParseLineJoin(joinStr)); - - stroke->setMiterLimit(PAGXAttributes::ParseFloat(node, "miterLimit", 4.0f)); - - auto dashesStr = PAGXAttributes::ParseString(node, "dashes", ""); - if (!dashesStr.empty()) { - stroke->setDashes(PAGXAttributes::ParseDashes(dashesStr)); - } - stroke->setDashOffset(PAGXAttributes::ParseFloat(node, "dashOffset", 0.0f)); - - auto alignStr = PAGXAttributes::ParseString(node, "align", "center"); - stroke->setStrokeAlign(PAGXAttributes::ParseStrokeAlign(alignStr)); - - return stroke; -} - -std::shared_ptr PAGXParser::parseTrimPath(const std::shared_ptr& node) { - auto trim = std::make_shared(); - trim->setStart(PAGXAttributes::ParseFloat(node, "start", 0.0f)); - trim->setEnd(PAGXAttributes::ParseFloat(node, "end", 1.0f)); - trim->setOffset(PAGXAttributes::ParseFloat(node, "offset", 0.0f)); - auto typeStr = PAGXAttributes::ParseString(node, "type", "separate"); - trim->setTrimType(PAGXAttributes::ParseTrimPathType(typeStr)); - return trim; -} - -std::shared_ptr PAGXParser::parseRoundCorner(const std::shared_ptr& node) { - auto round = std::make_shared(); - round->setRadius(PAGXAttributes::ParseFloat(node, "radius", 10.0f)); - return round; -} - -std::shared_ptr PAGXParser::parseMergePath(const std::shared_ptr& node) { - auto merge = std::make_shared(); - auto opStr = PAGXAttributes::ParseString(node, "op", "append"); - merge->setMode(PAGXAttributes::ParseMergePathOp(opStr)); - return merge; -} - -std::shared_ptr PAGXParser::parseRepeater(const std::shared_ptr& node) { - auto repeater = std::make_shared(); - repeater->setCopies(PAGXAttributes::ParseFloat(node, "copies", 3.0f)); - repeater->setOffset(PAGXAttributes::ParseFloat(node, "offset", 0.0f)); - - auto orderStr = PAGXAttributes::ParseString(node, "order", "belowOriginal"); - repeater->setOrder(orderStr == "aboveOriginal" ? RepeaterOrder::AboveOriginal - : RepeaterOrder::BelowOriginal); - - auto anchorStr = PAGXAttributes::ParseString(node, "anchor", "0,0"); - repeater->setAnchorPoint(PAGXAttributes::ParsePoint(anchorStr)); - - auto positionStr = PAGXAttributes::ParseString(node, "position", "100,100"); - repeater->setPosition(PAGXAttributes::ParsePoint(positionStr, {100.0f, 100.0f})); - - repeater->setRotation(PAGXAttributes::ParseFloat(node, "rotation", 0.0f)); - - auto scaleStr = PAGXAttributes::ParseString(node, "scale", "1,1"); - repeater->setScale(PAGXAttributes::ParsePoint(scaleStr, {1.0f, 1.0f})); - - repeater->setStartAlpha(PAGXAttributes::ParseFloat(node, "startAlpha", 1.0f)); - repeater->setEndAlpha(PAGXAttributes::ParseFloat(node, "endAlpha", 1.0f)); - return repeater; -} - -std::shared_ptr PAGXParser::parseColorSource(const std::shared_ptr& node) { - const auto& name = node->name; - if (name == "SolidColor") { - auto colorStr = PAGXAttributes::ParseString(node, "color", "#000000"); - return SolidColor::Make(PAGXAttributes::ParseColor(colorStr)); - } - if (name == "LinearGradient") { - return parseLinearGradient(node); - } - if (name == "RadialGradient") { - return parseRadialGradient(node); - } - if (name == "ConicGradient") { - return parseConicGradient(node); - } - if (name == "DiamondGradient") { - return parseDiamondGradient(node); - } - if (name == "ImagePattern") { - return parseImagePattern(node); - } - return nullptr; -} - -std::shared_ptr PAGXParser::parseInlineColorSource( - const std::shared_ptr& parentNode) { - auto child = parentNode->firstChild; - while (child) { - if (child->type == DOMNodeType::Element) { - auto source = parseColorSource(child); - if (source) { - return source; - } - } - child = child->nextSibling; - } - return nullptr; -} - -std::vector> PAGXParser::parseColorStops( - const std::shared_ptr& node) { - std::vector> stops; - auto child = node->firstChild; - while (child) { - if (child->name == "ColorStop") { - auto offset = PAGXAttributes::ParseFloat(child, "offset", 0.0f); - auto colorStr = PAGXAttributes::ParseString(child, "color", "#000000"); - stops.emplace_back(offset, PAGXAttributes::ParseColor(colorStr)); - } - child = child->nextSibling; - } - return stops; -} - -std::shared_ptr PAGXParser::parseLinearGradient( - const std::shared_ptr& node) { - auto startX = PAGXAttributes::ParseFloat(node, "startX", 0.0f); - auto startY = PAGXAttributes::ParseFloat(node, "startY", 0.0f); - auto endX = PAGXAttributes::ParseFloat(node, "endX", 0.0f); - auto endY = PAGXAttributes::ParseFloat(node, "endY", 0.0f); - - auto stops = parseColorStops(node); - std::vector colors; - std::vector positions; - colors.reserve(stops.size()); - positions.reserve(stops.size()); - for (const auto& [offset, color] : stops) { - positions.push_back(offset); - colors.push_back(color); - } - - auto gradient = Gradient::MakeLinear({startX, startY}, {endX, endY}, colors, positions); - auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); - if (!matrixStr.empty()) { - gradient->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); - } - return gradient; -} - -std::shared_ptr PAGXParser::parseRadialGradient( - const std::shared_ptr& node) { - auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); - auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); - auto radius = PAGXAttributes::ParseFloat(node, "radius", 0.0f); - - auto stops = parseColorStops(node); - std::vector colors; - std::vector positions; - colors.reserve(stops.size()); - positions.reserve(stops.size()); - for (const auto& [offset, color] : stops) { - positions.push_back(offset); - colors.push_back(color); - } - - auto gradient = Gradient::MakeRadial({centerX, centerY}, radius, colors, positions); - auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); - if (!matrixStr.empty()) { - gradient->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); - } - return gradient; -} - -std::shared_ptr PAGXParser::parseConicGradient( - const std::shared_ptr& node) { - auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); - auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); - auto startAngle = PAGXAttributes::ParseFloat(node, "startAngle", 0.0f); - auto endAngle = PAGXAttributes::ParseFloat(node, "endAngle", 360.0f); - - auto stops = parseColorStops(node); - std::vector colors; - std::vector positions; - colors.reserve(stops.size()); - positions.reserve(stops.size()); - for (const auto& [offset, color] : stops) { - positions.push_back(offset); - colors.push_back(color); - } - - auto gradient = Gradient::MakeConic({centerX, centerY}, startAngle, endAngle, colors, positions); - auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); - if (!matrixStr.empty()) { - gradient->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); - } - return gradient; -} - -std::shared_ptr PAGXParser::parseDiamondGradient( - const std::shared_ptr& node) { - auto centerX = PAGXAttributes::ParseFloat(node, "centerX", 0.0f); - auto centerY = PAGXAttributes::ParseFloat(node, "centerY", 0.0f); - auto halfDiagonal = PAGXAttributes::ParseFloat(node, "halfDiagonal", 0.0f); - - auto stops = parseColorStops(node); - std::vector colors; - std::vector positions; - colors.reserve(stops.size()); - positions.reserve(stops.size()); - for (const auto& [offset, color] : stops) { - positions.push_back(offset); - colors.push_back(color); - } - - auto gradient = Gradient::MakeDiamond({centerX, centerY}, halfDiagonal, colors, positions); - auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); - if (!matrixStr.empty()) { - gradient->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); - } - return gradient; -} - -std::shared_ptr PAGXParser::parseImagePattern(const std::shared_ptr& node) { - auto [hasImage, imageRef] = node->findAttribute("image"); - if (!hasImage || imageRef.empty()) { - return nullptr; - } - - std::shared_ptr image = nullptr; - if (imageRef[0] == '#') { - auto it = images.find(imageRef.substr(1)); - if (it != images.end()) { - image = it->second; - } - } - - if (!image) { - return nullptr; - } - - auto tileModeXStr = PAGXAttributes::ParseString(node, "tileModeX", "clamp"); - auto tileModeYStr = PAGXAttributes::ParseString(node, "tileModeY", "clamp"); - - auto pattern = - ImagePattern::Make(image, PAGXAttributes::ParseTileMode(tileModeXStr), - PAGXAttributes::ParseTileMode(tileModeYStr), SamplingOptions()); - auto matrixStr = PAGXAttributes::ParseString(node, "matrix", ""); - if (!matrixStr.empty()) { - pattern->setMatrix(PAGXAttributes::ParseMatrix(matrixStr)); - } - return pattern; -} - -std::vector> PAGXParser::parseFilters( - const std::shared_ptr& node) { - std::vector> filters; - auto child = node->firstChild; - while (child) { - if (child->name == "BlurFilter") { - auto blurX = PAGXAttributes::ParseFloat(child, "blurrinessX", 0.0f); - auto blurY = PAGXAttributes::ParseFloat(child, "blurrinessY", 0.0f); - auto tileModeStr = PAGXAttributes::ParseString(child, "tileMode", "decal"); - auto filter = BlurFilter::Make(blurX, blurY, PAGXAttributes::ParseTileMode(tileModeStr)); - if (filter) { - filters.push_back(filter); - } - } else if (child->name == "DropShadowFilter") { - auto offsetX = PAGXAttributes::ParseFloat(child, "offsetX", 0.0f); - auto offsetY = PAGXAttributes::ParseFloat(child, "offsetY", 0.0f); - auto blurX = PAGXAttributes::ParseFloat(child, "blurrinessX", 0.0f); - auto blurY = PAGXAttributes::ParseFloat(child, "blurrinessY", 0.0f); - auto colorStr = PAGXAttributes::ParseString(child, "color", "#000000"); - auto shadowOnly = PAGXAttributes::ParseBool(child, "shadowOnly", false); - auto filter = DropShadowFilter::Make(offsetX, offsetY, blurX, blurY, - PAGXAttributes::ParseColor(colorStr), shadowOnly); - if (filter) { - filters.push_back(filter); - } - } - child = child->nextSibling; - } - return filters; -} - -std::vector> PAGXParser::parseStyles( - const std::shared_ptr& node) { - std::vector> styles; - auto child = node->firstChild; - while (child) { - if (child->name == "DropShadowStyle") { - auto offsetX = PAGXAttributes::ParseFloat(child, "offsetX", 0.0f); - auto offsetY = PAGXAttributes::ParseFloat(child, "offsetY", 0.0f); - auto blurX = PAGXAttributes::ParseFloat(child, "blurrinessX", 0.0f); - auto blurY = PAGXAttributes::ParseFloat(child, "blurrinessY", 0.0f); - auto colorStr = PAGXAttributes::ParseString(child, "color", "#000000"); - auto showBehind = PAGXAttributes::ParseBool(child, "showBehindLayer", true); - auto style = DropShadowStyle::Make(offsetX, offsetY, blurX, blurY, - PAGXAttributes::ParseColor(colorStr), showBehind); - if (style) { - styles.push_back(style); - } - } else if (child->name == "InnerShadowStyle") { - auto offsetX = PAGXAttributes::ParseFloat(child, "offsetX", 0.0f); - auto offsetY = PAGXAttributes::ParseFloat(child, "offsetY", 0.0f); - auto blurX = PAGXAttributes::ParseFloat(child, "blurrinessX", 0.0f); - auto blurY = PAGXAttributes::ParseFloat(child, "blurrinessY", 0.0f); - auto colorStr = PAGXAttributes::ParseString(child, "color", "#000000"); - auto style = InnerShadowStyle::Make(offsetX, offsetY, blurX, blurY, - PAGXAttributes::ParseColor(colorStr)); - if (style) { - styles.push_back(style); - } - } - child = child->nextSibling; - } - return styles; -} - -std::shared_ptr PAGXParser::resolveColorReference(const std::string& value) { - if (value.empty() || value[0] != '#') { - return nullptr; - } - auto id = value.substr(1); - auto it = colorSources.find(id); - if (it != colorSources.end()) { - return it->second; - } - return nullptr; -} - -std::shared_ptr PAGXParser::resolveImageReference(const std::string& ref) { - if (ref.empty()) { - return nullptr; - } - // Check for data URI - if (ref.find("data:") == 0) { - // Parse data URI format: data:[][;base64], - auto commaPos = ref.find(','); - if (commaPos == std::string::npos) { - return nullptr; - } - auto header = ref.substr(0, commaPos); - auto base64Data = ref.substr(commaPos + 1); - // Check if it's base64 encoded - if (header.find(";base64") == std::string::npos) { - return nullptr; - } - auto data = pagx::Base64Decode(base64Data); - if (data == nullptr) { - return nullptr; - } - return Image::MakeFromEncoded(data); - } - // Relative path - resolve against basePath - std::string fullPath = basePath_; - if (!fullPath.empty() && fullPath.back() != '/') { - fullPath += '/'; - } - fullPath += ref; - return Image::MakeFromFile(fullPath); -} - -} // namespace pagx diff --git a/pagx/src/layers/PAGXParser.h b/pagx/src/layers/PAGXParser.h deleted file mode 100644 index d9d4fca97f..0000000000 --- a/pagx/src/layers/PAGXParser.h +++ /dev/null @@ -1,137 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include -#include "tgfx/core/Image.h" -#include "tgfx/layers/Layer.h" -#include "tgfx/layers/VectorLayer.h" -#include "tgfx/layers/filters/LayerFilter.h" -#include "tgfx/layers/layerstyles/LayerStyle.h" -#include "tgfx/layers/vectors/ColorSource.h" -#include "tgfx/layers/vectors/Ellipse.h" -#include "tgfx/layers/vectors/FillStyle.h" -#include "tgfx/layers/vectors/Gradient.h" -#include "tgfx/layers/vectors/ImagePattern.h" -#include "tgfx/layers/vectors/MergePath.h" -#include "tgfx/layers/vectors/Polystar.h" -#include "tgfx/layers/vectors/Rectangle.h" -#include "tgfx/layers/vectors/Repeater.h" -#include "tgfx/layers/vectors/RoundCorner.h" -#include "tgfx/layers/vectors/ShapePath.h" -#include "tgfx/layers/vectors/StrokeStyle.h" -#include "tgfx/layers/vectors/TextSpan.h" -#include "tgfx/layers/vectors/TrimPath.h" -#include "tgfx/layers/vectors/VectorElement.h" -#include "tgfx/layers/vectors/VectorGroup.h" -#include "tgfx/svg/xml/XMLDOM.h" - -namespace pagx { - -using namespace tgfx; - -class PAGXParser { - public: - PAGXParser(std::shared_ptr rootNode, const std::string& basePath); - - bool parse(); - - float width() const { - return width_; - } - - float height() const { - return height_; - } - - std::shared_ptr getRoot() const { - return rootLayer; - } - - private: - void parseResources(const std::shared_ptr& node); - - std::shared_ptr parseLayer(const std::shared_ptr& node); - - std::vector> parseContents(const std::shared_ptr& node); - - std::shared_ptr parseVectorElement(const std::shared_ptr& node); - - std::shared_ptr parseGroup(const std::shared_ptr& node); - - std::shared_ptr parseRectangle(const std::shared_ptr& node); - - std::shared_ptr parseEllipse(const std::shared_ptr& node); - - std::shared_ptr parsePolystar(const std::shared_ptr& node); - - std::shared_ptr parsePath(const std::shared_ptr& node); - - std::shared_ptr parseTextSpan(const std::shared_ptr& node); - - std::shared_ptr parseFill(const std::shared_ptr& node); - - std::shared_ptr parseStroke(const std::shared_ptr& node); - - std::shared_ptr parseTrimPath(const std::shared_ptr& node); - - std::shared_ptr parseRoundCorner(const std::shared_ptr& node); - - std::shared_ptr parseMergePath(const std::shared_ptr& node); - - std::shared_ptr parseRepeater(const std::shared_ptr& node); - - std::shared_ptr parseColorSource(const std::shared_ptr& node); - - std::shared_ptr parseInlineColorSource(const std::shared_ptr& parentNode); - - std::shared_ptr parseLinearGradient(const std::shared_ptr& node); - - std::shared_ptr parseRadialGradient(const std::shared_ptr& node); - - std::shared_ptr parseConicGradient(const std::shared_ptr& node); - - std::shared_ptr parseDiamondGradient(const std::shared_ptr& node); - - std::shared_ptr parseImagePattern(const std::shared_ptr& node); - - std::vector> parseColorStops(const std::shared_ptr& node); - - std::vector> parseFilters(const std::shared_ptr& node); - - std::vector> parseStyles(const std::shared_ptr& node); - - std::shared_ptr resolveColorReference(const std::string& value); - - std::shared_ptr resolveImageReference(const std::string& ref); - - std::shared_ptr rootNode_ = nullptr; - std::string basePath_; - float width_ = 0.0f; - float height_ = 0.0f; - std::shared_ptr rootLayer = nullptr; - std::unordered_map> colorSources; - std::unordered_map> images; - std::unordered_map> layerIdMap; -}; - -} // namespace pagx diff --git a/pagx/src/layers/PAGXUtils.cpp b/pagx/src/layers/PAGXUtils.cpp deleted file mode 100644 index df804d2b6e..0000000000 --- a/pagx/src/layers/PAGXUtils.cpp +++ /dev/null @@ -1,80 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "PAGXUtils.h" -#include -#include - -namespace pagx { - -std::shared_ptr Base64Decode(const std::string& encodedString) { - static const std::array decodingTable = { - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, - 64, 64, 64, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 64, 64, 64, 64, 64, 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64}; - - size_t inLength = encodedString.size(); - if (inLength % 4 != 0) { - return nullptr; - } - - size_t outLength = inLength / 4 * 3; - if (encodedString[inLength - 1] == '=') { - outLength--; - } - if (encodedString[inLength - 2] == '=') { - outLength--; - } - - auto out = static_cast(malloc(outLength)); - auto outData = tgfx::Data::MakeAdopted(out, outLength, tgfx::Data::FreeProc); - - for (size_t i = 0, j = 0; i < inLength;) { - uint32_t a = encodedString[i] == '=' - ? 0 & i++ - : decodingTable[static_cast(encodedString[i++])]; - uint32_t b = encodedString[i] == '=' - ? 0 & i++ - : decodingTable[static_cast(encodedString[i++])]; - uint32_t c = encodedString[i] == '=' - ? 0 & i++ - : decodingTable[static_cast(encodedString[i++])]; - uint32_t d = encodedString[i] == '=' - ? 0 & i++ - : decodingTable[static_cast(encodedString[i++])]; - - uint32_t triple = (a << 18) + (b << 12) + (c << 6) + d; - - if (j < outLength) { - out[j++] = (triple >> 16) & 0xFF; - } - if (j < outLength) { - out[j++] = (triple >> 8) & 0xFF; - } - if (j < outLength) { - out[j++] = triple & 0xFF; - } - } - - return outData; -} - -} // namespace pagx diff --git a/pagx/src/layers/TextLayouter.cpp b/pagx/src/layers/TextLayouter.cpp deleted file mode 100644 index e5ee88a1af..0000000000 --- a/pagx/src/layers/TextLayouter.cpp +++ /dev/null @@ -1,170 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "pagx/layers/TextLayouter.h" -#include -#include "tgfx/core/TextBlobBuilder.h" -#include "tgfx/core/Typeface.h" -#include "tgfx/core/UTF.h" - -namespace pagx { - -using namespace tgfx; - -struct GlyphInfo { - Unichar unichar = 0; - GlyphID glyphID = 0; - std::shared_ptr typeface = nullptr; -}; - -static std::mutex& FallbackMutex = *new std::mutex; -static std::vector> FallbackTypefaces = {}; - -void TextLayouter::SetFallbackTypefaces(std::vector> typefaces) { - std::lock_guard lock(FallbackMutex); - FallbackTypefaces = std::move(typefaces); -} - -std::vector> TextLayouter::GetFallbackTypefaces() { - std::lock_guard lock(FallbackMutex); - return FallbackTypefaces; -} - -static std::vector ShapeText(const std::string& text, - const std::shared_ptr& typeface, - const std::vector>& fallbacks) { - if (text.empty()) { - return {}; - } - - std::vector glyphs = {}; - glyphs.reserve(text.size()); - - const char* head = text.data(); - const char* tail = head + text.size(); - - while (head < tail) { - auto unichar = UTF::NextUTF8(&head, tail); - - GlyphID glyphID = typeface ? typeface->getGlyphID(unichar) : 0; - std::shared_ptr matchedTypeface = typeface; - - if (glyphID == 0) { - for (const auto& fallback : fallbacks) { - if (fallback == nullptr) { - continue; - } - glyphID = fallback->getGlyphID(unichar); - if (glyphID > 0) { - matchedTypeface = fallback; - break; - } - } - } - - glyphs.push_back({unichar, glyphID, matchedTypeface}); - } - - return glyphs; -} - -std::shared_ptr TextLayouter::Layout(const std::string& text, const Font& font) { - if (text.empty()) { - return nullptr; - } - - auto typeface = font.getTypeface(); - auto fallbacks = GetFallbackTypefaces(); - - // If primary typeface is empty or invalid, try to use first fallback or system default - if (typeface == nullptr || typeface->fontFamily().empty()) { - if (!fallbacks.empty() && fallbacks[0] != nullptr) { - typeface = fallbacks[0]; - } else { - // Try system default fonts (platform-specific) - static const char* defaultFonts[] = {"Helvetica", "Arial", "sans-serif", nullptr}; - for (int i = 0; defaultFonts[i] != nullptr && typeface == nullptr; i++) { - typeface = Typeface::MakeFromName(defaultFonts[i], ""); - } - if (typeface == nullptr) { - return nullptr; - } - } - } - - auto glyphs = ShapeText(text, typeface, fallbacks); - if (glyphs.empty()) { - return nullptr; - } - - std::vector positions = {}; - positions.reserve(glyphs.size()); - - float xOffset = 0; - float emptyAdvance = font.getSize() / 2.0f; - - for (const auto& glyph : glyphs) { - positions.push_back({xOffset, 0}); - - if (glyph.glyphID > 0 && glyph.typeface != nullptr) { - Font glyphFont = font; - glyphFont.setTypeface(glyph.typeface); - xOffset += glyphFont.getAdvance(glyph.glyphID); - } else { - xOffset += emptyAdvance; - } - } - - // Group glyphs by typeface - std::unordered_map> typefaceToIndices = {}; - for (size_t i = 0; i < glyphs.size(); i++) { - const auto& glyph = glyphs[i]; - if (glyph.glyphID == 0 || glyph.typeface == nullptr) { - continue; - } - typefaceToIndices[glyph.typeface->uniqueID()].push_back(i); - } - - if (typefaceToIndices.empty()) { - return nullptr; - } - - TextBlobBuilder builder; - for (const auto& [typefaceID, indices] : typefaceToIndices) { - if (indices.empty()) { - continue; - } - - auto typeface = glyphs[indices[0]].typeface; - Font runFont = font; - runFont.setTypeface(typeface); - - const auto& buffer = builder.allocRunPos(runFont, indices.size()); - auto* pointPositions = reinterpret_cast(buffer.positions); - - for (size_t i = 0; i < indices.size(); i++) { - auto idx = indices[i]; - buffer.glyphs[i] = glyphs[idx].glyphID; - pointPositions[i] = positions[idx]; - } - } - - return builder.build(); -} - -} // namespace pagx diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp new file mode 100644 index 0000000000..e88ec5bde4 --- /dev/null +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -0,0 +1,1168 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/PAGXSVGParser.h" +#include +#include +#include +#include +#include +#include "SVGParserInternal.h" + +namespace pagx { + +std::shared_ptr PAGXSVGParser::Parse(const std::string& filePath, + const Options& options) { + std::ifstream file(filePath, std::ios::binary); + if (!file.is_open()) { + return nullptr; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + + auto doc = Parse(reinterpret_cast(content.data()), content.size(), options); + if (doc) { + auto lastSlash = filePath.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + doc->setBasePath(filePath.substr(0, lastSlash + 1)); + } + } + return doc; +} + +std::shared_ptr PAGXSVGParser::Parse(const uint8_t* data, size_t length, + const Options& options) { + SVGParserImpl parser(options); + return parser.parse(data, length); +} + +std::shared_ptr PAGXSVGParser::ParseString(const std::string& svgContent, + const Options& options) { + return Parse(reinterpret_cast(svgContent.data()), svgContent.size(), options); +} + +// ============== SVGParserImpl ============== + +SVGParserImpl::SVGParserImpl(const PAGXSVGParser::Options& options) : _options(options) { +} + +// Simple XML parser for SVG +class SVGXMLTokenizer { + public: + SVGXMLTokenizer(const char* data, size_t length) : _data(data), _length(length), _pos(0) { + } + + bool isEnd() const { + return _pos >= _length; + } + + void skipWhitespace() { + while (_pos < _length && std::isspace(_data[_pos])) { + ++_pos; + } + } + + bool match(char c) { + if (_pos < _length && _data[_pos] == c) { + ++_pos; + return true; + } + return false; + } + + bool matchString(const char* str) { + size_t len = std::strlen(str); + if (_pos + len <= _length && std::strncmp(_data + _pos, str, len) == 0) { + _pos += len; + return true; + } + return false; + } + + std::string readUntil(char delimiter) { + std::string result; + while (_pos < _length && _data[_pos] != delimiter) { + result += _data[_pos++]; + } + return result; + } + + std::string readUntilAny(const char* delimiters) { + std::string result; + while (_pos < _length) { + bool isDelim = false; + for (const char* d = delimiters; *d; ++d) { + if (_data[_pos] == *d) { + isDelim = true; + break; + } + } + if (isDelim) { + break; + } + result += _data[_pos++]; + } + return result; + } + + std::string readName() { + std::string result; + while (_pos < _length && + (std::isalnum(_data[_pos]) || _data[_pos] == '_' || _data[_pos] == '-' || + _data[_pos] == ':')) { + result += _data[_pos++]; + } + return result; + } + + std::string readQuotedString() { + char quote = _data[_pos++]; + std::string result; + while (_pos < _length && _data[_pos] != quote) { + if (_data[_pos] == '&') { + result += readEscapeSequence(); + } else { + result += _data[_pos++]; + } + } + if (_pos < _length) { + ++_pos; + } + return result; + } + + std::string readEscapeSequence() { + std::string seq; + ++_pos; + while (_pos < _length && _data[_pos] != ';') { + seq += _data[_pos++]; + } + if (_pos < _length) { + ++_pos; + } + if (seq == "lt") + return "<"; + if (seq == "gt") + return ">"; + if (seq == "amp") + return "&"; + if (seq == "quot") + return "\""; + if (seq == "apos") + return "'"; + if (!seq.empty() && seq[0] == '#') { + int code = 0; + if (seq.length() > 1 && seq[1] == 'x') { + code = std::stoi(seq.substr(2), nullptr, 16); + } else { + code = std::stoi(seq.substr(1)); + } + if (code > 0 && code < 128) { + return std::string(1, static_cast(code)); + } + } + return "&" + seq + ";"; + } + + std::string readTextContent() { + std::string result; + while (_pos < _length && _data[_pos] != '<') { + if (_data[_pos] == '&') { + result += readEscapeSequence(); + } else { + result += _data[_pos++]; + } + } + return result; + } + + void skipComment() { + while (_pos + 2 < _length) { + if (_data[_pos] == '-' && _data[_pos + 1] == '-' && _data[_pos + 2] == '>') { + _pos += 3; + return; + } + ++_pos; + } + } + + void skipCDATA() { + while (_pos + 2 < _length) { + if (_data[_pos] == ']' && _data[_pos + 1] == ']' && _data[_pos + 2] == '>') { + _pos += 3; + return; + } + ++_pos; + } + } + + void skipProcessingInstruction() { + while (_pos + 1 < _length) { + if (_data[_pos] == '?' && _data[_pos + 1] == '>') { + _pos += 2; + return; + } + ++_pos; + } + } + + private: + const char* _data = nullptr; + size_t _length = 0; + size_t _pos = 0; +}; + +static std::unique_ptr ParseSVGXMLElement(SVGXMLTokenizer& tokenizer); + +static void ParseSVGXMLAttributes(SVGXMLTokenizer& tokenizer, SVGXMLNode* node) { + while (true) { + tokenizer.skipWhitespace(); + if (tokenizer.isEnd()) { + break; + } + + std::string name = tokenizer.readName(); + if (name.empty()) { + break; + } + + tokenizer.skipWhitespace(); + if (!tokenizer.match('=')) { + break; + } + + tokenizer.skipWhitespace(); + std::string value = tokenizer.readQuotedString(); + node->attributes[name] = value; + } +} + +static std::unique_ptr ParseSVGXMLElement(SVGXMLTokenizer& tokenizer) { + tokenizer.skipWhitespace(); + + if (tokenizer.isEnd() || !tokenizer.match('<')) { + return nullptr; + } + + while (true) { + if (tokenizer.matchString("!--")) { + tokenizer.skipComment(); + tokenizer.skipWhitespace(); + if (!tokenizer.match('<')) { + return nullptr; + } + } else if (tokenizer.matchString("![CDATA[")) { + tokenizer.skipCDATA(); + tokenizer.skipWhitespace(); + if (!tokenizer.match('<')) { + return nullptr; + } + } else if (tokenizer.matchString("?")) { + tokenizer.skipProcessingInstruction(); + tokenizer.skipWhitespace(); + if (!tokenizer.match('<')) { + return nullptr; + } + } else if (tokenizer.matchString("!DOCTYPE")) { + tokenizer.readUntil('>'); + tokenizer.match('>'); + tokenizer.skipWhitespace(); + if (!tokenizer.match('<')) { + return nullptr; + } + } else { + break; + } + } + + if (tokenizer.match('/')) { + return nullptr; + } + + std::string tagName = tokenizer.readName(); + if (tagName.empty()) { + return nullptr; + } + + auto node = std::make_unique(); + node->tagName = tagName; + + ParseSVGXMLAttributes(tokenizer, node.get()); + + tokenizer.skipWhitespace(); + + if (tokenizer.match('/')) { + tokenizer.match('>'); + return node; + } + + if (!tokenizer.match('>')) { + return node; + } + + while (true) { + std::string text = tokenizer.readTextContent(); + if (!text.empty()) { + // Trim whitespace + size_t start = text.find_first_not_of(" \t\n\r"); + size_t end = text.find_last_not_of(" \t\n\r"); + if (start != std::string::npos && end != std::string::npos) { + node->textContent += text.substr(start, end - start + 1); + } + } + + if (tokenizer.isEnd()) { + break; + } + + auto child = ParseSVGXMLElement(tokenizer); + if (!child) { + tokenizer.skipWhitespace(); + tokenizer.readUntil('>'); + tokenizer.match('>'); + break; + } + + node->children.push_back(std::move(child)); + } + + return node; +} + +std::unique_ptr SVGParserImpl::parseXML(const char* data, size_t length) { + SVGXMLTokenizer tokenizer(data, length); + return ParseSVGXMLElement(tokenizer); +} + +std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { + if (!data || length == 0) { + return nullptr; + } + + auto xmlRoot = parseXML(reinterpret_cast(data), length); + if (!xmlRoot || xmlRoot->tagName != "svg") { + return nullptr; + } + + // Parse viewBox and dimensions + auto viewBox = parseViewBox(xmlRoot->getAttribute("viewBox")); + float width = parseLength(xmlRoot->getAttribute("width"), 0); + float height = parseLength(xmlRoot->getAttribute("height"), 0); + + if (viewBox.size() >= 4) { + _viewBoxWidth = viewBox[2]; + _viewBoxHeight = viewBox[3]; + if (width == 0) { + width = _viewBoxWidth; + } + if (height == 0) { + height = _viewBoxHeight; + } + } else { + _viewBoxWidth = width; + _viewBoxHeight = height; + } + + if (width <= 0 || height <= 0) { + return nullptr; + } + + _document = PAGXDocument::Make(width, height); + + // First pass: collect defs + for (auto& child : xmlRoot->children) { + if (child->tagName == "defs") { + parseDefs(child.get()); + } + } + + // Second pass: convert elements + auto root = _document->root(); + for (auto& child : xmlRoot->children) { + if (child->tagName == "defs") { + continue; + } + auto pagxNode = convertElement(child.get()); + if (pagxNode) { + root->appendChild(std::move(pagxNode)); + } + } + + return _document; +} + +void SVGParserImpl::parseDefs(SVGXMLNode* defsNode) { + for (auto& child : defsNode->children) { + std::string id = child->getAttribute("id"); + if (!id.empty()) { + _defs[id] = child.get(); + } + // Also handle nested defs + if (child->tagName == "defs") { + parseDefs(child.get()); + } + } +} + +std::unique_ptr SVGParserImpl::convertElement(SVGXMLNode* element) { + const auto& tag = element->tagName; + + std::unique_ptr node = nullptr; + + if (tag == "g" || tag == "svg") { + node = convertG(element); + } else if (tag == "rect") { + node = convertRect(element); + } else if (tag == "circle") { + node = convertCircle(element); + } else if (tag == "ellipse") { + node = convertEllipse(element); + } else if (tag == "line") { + node = convertLine(element); + } else if (tag == "polyline") { + node = convertPolyline(element); + } else if (tag == "polygon") { + node = convertPolygon(element); + } else if (tag == "path") { + node = convertPath(element); + } else if (tag == "text") { + node = convertText(element); + } else if (tag == "image") { + node = convertImage(element); + } else if (tag == "use") { + node = convertUse(element); + } else if (tag == "linearGradient") { + node = convertLinearGradient(element); + } else if (tag == "radialGradient") { + node = convertRadialGradient(element); + } else if (tag == "pattern") { + node = convertPattern(element); + } else if (tag == "mask") { + node = convertMask(element); + } else if (tag == "clipPath") { + node = convertClipPath(element); + } else if (tag == "filter") { + node = convertFilter(element); + } else if (_options.preserveUnknownElements) { + node = PAGXNode::Make(PAGXNodeType::Unknown); + node->setAttribute("_svgTag", tag); + } + + return node; +} + +std::unique_ptr SVGParserImpl::convertG(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Group); + parseCommonAttributes(element, node.get()); + parseFillStroke(element, node.get()); + + for (auto& child : element->children) { + auto childNode = convertElement(child.get()); + if (childNode) { + node->appendChild(std::move(childNode)); + } + } + + return node; +} + +std::unique_ptr SVGParserImpl::convertRect(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Rectangle); + parseCommonAttributes(element, node.get()); + + float x = parseLength(element->getAttribute("x"), _viewBoxWidth); + float y = parseLength(element->getAttribute("y"), _viewBoxHeight); + float width = parseLength(element->getAttribute("width"), _viewBoxWidth); + float height = parseLength(element->getAttribute("height"), _viewBoxHeight); + float rx = parseLength(element->getAttribute("rx"), _viewBoxWidth); + float ry = parseLength(element->getAttribute("ry"), _viewBoxHeight); + + node->setFloatAttribute("x", x); + node->setFloatAttribute("y", y); + node->setFloatAttribute("width", width); + node->setFloatAttribute("height", height); + if (rx > 0) { + node->setFloatAttribute("rx", rx); + } + if (ry > 0) { + node->setFloatAttribute("ry", ry); + } + + parseFillStroke(element, node.get()); + return node; +} + +std::unique_ptr SVGParserImpl::convertCircle(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Ellipse); + parseCommonAttributes(element, node.get()); + + float cx = parseLength(element->getAttribute("cx"), _viewBoxWidth); + float cy = parseLength(element->getAttribute("cy"), _viewBoxHeight); + float r = parseLength(element->getAttribute("r"), _viewBoxWidth); + + node->setFloatAttribute("cx", cx); + node->setFloatAttribute("cy", cy); + node->setFloatAttribute("rx", r); + node->setFloatAttribute("ry", r); + + parseFillStroke(element, node.get()); + return node; +} + +std::unique_ptr SVGParserImpl::convertEllipse(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Ellipse); + parseCommonAttributes(element, node.get()); + + float cx = parseLength(element->getAttribute("cx"), _viewBoxWidth); + float cy = parseLength(element->getAttribute("cy"), _viewBoxHeight); + float rx = parseLength(element->getAttribute("rx"), _viewBoxWidth); + float ry = parseLength(element->getAttribute("ry"), _viewBoxHeight); + + node->setFloatAttribute("cx", cx); + node->setFloatAttribute("cy", cy); + node->setFloatAttribute("rx", rx); + node->setFloatAttribute("ry", ry); + + parseFillStroke(element, node.get()); + return node; +} + +std::unique_ptr SVGParserImpl::convertLine(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Path); + parseCommonAttributes(element, node.get()); + + float x1 = parseLength(element->getAttribute("x1"), _viewBoxWidth); + float y1 = parseLength(element->getAttribute("y1"), _viewBoxHeight); + float x2 = parseLength(element->getAttribute("x2"), _viewBoxWidth); + float y2 = parseLength(element->getAttribute("y2"), _viewBoxHeight); + + PathData path; + path.moveTo(x1, y1); + path.lineTo(x2, y2); + node->setPathAttribute("d", path); + + parseFillStroke(element, node.get()); + return node; +} + +std::unique_ptr SVGParserImpl::convertPolyline(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Path); + parseCommonAttributes(element, node.get()); + + PathData path = parsePoints(element->getAttribute("points"), false); + node->setPathAttribute("d", path); + + parseFillStroke(element, node.get()); + return node; +} + +std::unique_ptr SVGParserImpl::convertPolygon(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Path); + parseCommonAttributes(element, node.get()); + + PathData path = parsePoints(element->getAttribute("points"), true); + node->setPathAttribute("d", path); + + parseFillStroke(element, node.get()); + return node; +} + +std::unique_ptr SVGParserImpl::convertPath(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Path); + parseCommonAttributes(element, node.get()); + + std::string d = element->getAttribute("d"); + node->setAttribute("d", d); + + parseFillStroke(element, node.get()); + return node; +} + +std::unique_ptr SVGParserImpl::convertText(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Text); + parseCommonAttributes(element, node.get()); + + float x = parseLength(element->getAttribute("x"), _viewBoxWidth); + float y = parseLength(element->getAttribute("y"), _viewBoxHeight); + node->setFloatAttribute("x", x); + node->setFloatAttribute("y", y); + + if (!element->textContent.empty()) { + node->setAttribute("text", element->textContent); + } + + std::string fontFamily = element->getAttribute("font-family"); + if (!fontFamily.empty()) { + node->setAttribute("fontFamily", fontFamily); + } + + std::string fontSize = element->getAttribute("font-size"); + if (!fontSize.empty()) { + node->setFloatAttribute("fontSize", parseLength(fontSize, _viewBoxHeight)); + } + + parseFillStroke(element, node.get()); + return node; +} + +std::unique_ptr SVGParserImpl::convertImage(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Image); + parseCommonAttributes(element, node.get()); + + float x = parseLength(element->getAttribute("x"), _viewBoxWidth); + float y = parseLength(element->getAttribute("y"), _viewBoxHeight); + float width = parseLength(element->getAttribute("width"), _viewBoxWidth); + float height = parseLength(element->getAttribute("height"), _viewBoxHeight); + + node->setFloatAttribute("x", x); + node->setFloatAttribute("y", y); + node->setFloatAttribute("width", width); + node->setFloatAttribute("height", height); + + std::string href = element->getAttribute("xlink:href"); + if (href.empty()) { + href = element->getAttribute("href"); + } + if (!href.empty()) { + node->setAttribute("href", href); + } + + return node; +} + +std::unique_ptr SVGParserImpl::convertUse(SVGXMLNode* element) { + std::string href = element->getAttribute("xlink:href"); + if (href.empty()) { + href = element->getAttribute("href"); + } + + std::string refId = resolveUrl(href); + auto it = _defs.find(refId); + if (it == _defs.end()) { + return nullptr; + } + + if (_options.expandUseReferences) { + auto node = convertElement(it->second); + if (node) { + float x = parseLength(element->getAttribute("x"), _viewBoxWidth); + float y = parseLength(element->getAttribute("y"), _viewBoxHeight); + if (x != 0 || y != 0) { + Matrix m = node->getMatrixAttribute("transform"); + m.preTranslate(x, y); + node->setMatrixAttribute("transform", m); + } + } + return node; + } + + auto node = PAGXNode::Make(PAGXNodeType::Group); + parseCommonAttributes(element, node.get()); + node->setAttribute("_useRef", refId); + return node; +} + +std::unique_ptr SVGParserImpl::convertLinearGradient(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::LinearGradient); + + std::string id = element->getAttribute("id"); + if (!id.empty()) { + node->setId(id); + } + + node->setFloatAttribute("x1", parseLength(element->getAttribute("x1", "0%"), 1.0f)); + node->setFloatAttribute("y1", parseLength(element->getAttribute("y1", "0%"), 1.0f)); + node->setFloatAttribute("x2", parseLength(element->getAttribute("x2", "100%"), 1.0f)); + node->setFloatAttribute("y2", parseLength(element->getAttribute("y2", "0%"), 1.0f)); + + // Parse stops + int stopIndex = 0; + for (auto& child : element->children) { + if (child->tagName == "stop") { + float offset = parseLength(child->getAttribute("offset", "0"), 1.0f); + Color color = parseColor(child->getAttribute("stop-color", "#000000")); + float opacity = parseLength(child->getAttribute("stop-opacity", "1"), 1.0f); + color = color.withAlpha(opacity); + + std::string prefix = "stop" + std::to_string(stopIndex) + "_"; + node->setFloatAttribute(prefix + "offset", offset); + node->setColorAttribute(prefix + "color", color); + ++stopIndex; + } + } + node->setIntAttribute("stopCount", stopIndex); + + return node; +} + +std::unique_ptr SVGParserImpl::convertRadialGradient(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::RadialGradient); + + std::string id = element->getAttribute("id"); + if (!id.empty()) { + node->setId(id); + } + + node->setFloatAttribute("cx", parseLength(element->getAttribute("cx", "50%"), 1.0f)); + node->setFloatAttribute("cy", parseLength(element->getAttribute("cy", "50%"), 1.0f)); + node->setFloatAttribute("r", parseLength(element->getAttribute("r", "50%"), 1.0f)); + node->setFloatAttribute("fx", parseLength(element->getAttribute("fx"), 1.0f)); + node->setFloatAttribute("fy", parseLength(element->getAttribute("fy"), 1.0f)); + + // Parse stops (same as linear gradient) + int stopIndex = 0; + for (auto& child : element->children) { + if (child->tagName == "stop") { + float offset = parseLength(child->getAttribute("offset", "0"), 1.0f); + Color color = parseColor(child->getAttribute("stop-color", "#000000")); + float opacity = parseLength(child->getAttribute("stop-opacity", "1"), 1.0f); + color = color.withAlpha(opacity); + + std::string prefix = "stop" + std::to_string(stopIndex) + "_"; + node->setFloatAttribute(prefix + "offset", offset); + node->setColorAttribute(prefix + "color", color); + ++stopIndex; + } + } + node->setIntAttribute("stopCount", stopIndex); + + return node; +} + +std::unique_ptr SVGParserImpl::convertPattern(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::ImagePattern); + std::string id = element->getAttribute("id"); + if (!id.empty()) { + node->setId(id); + } + // Pattern conversion is complex, store raw attributes for now + for (auto& attr : element->attributes) { + node->setAttribute(attr.first, attr.second); + } + return node; +} + +std::unique_ptr SVGParserImpl::convertMask(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::Mask); + std::string id = element->getAttribute("id"); + if (!id.empty()) { + node->setId(id); + } + for (auto& child : element->children) { + auto childNode = convertElement(child.get()); + if (childNode) { + node->appendChild(std::move(childNode)); + } + } + return node; +} + +std::unique_ptr SVGParserImpl::convertClipPath(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::ClipPath); + std::string id = element->getAttribute("id"); + if (!id.empty()) { + node->setId(id); + } + for (auto& child : element->children) { + auto childNode = convertElement(child.get()); + if (childNode) { + node->appendChild(std::move(childNode)); + } + } + return node; +} + +std::unique_ptr SVGParserImpl::convertFilter(SVGXMLNode* element) { + auto node = PAGXNode::Make(PAGXNodeType::BlurFilter); + std::string id = element->getAttribute("id"); + if (!id.empty()) { + node->setId(id); + } + // Look for feGaussianBlur + for (auto& child : element->children) { + if (child->tagName == "feGaussianBlur") { + std::string stdDev = child->getAttribute("stdDeviation"); + if (!stdDev.empty()) { + node->setFloatAttribute("blurRadius", std::stof(stdDev)); + } + } + } + return node; +} + +void SVGParserImpl::parseCommonAttributes(SVGXMLNode* element, PAGXNode* node) { + std::string id = element->getAttribute("id"); + if (!id.empty()) { + node->setId(id); + } + + std::string transform = element->getAttribute("transform"); + if (!transform.empty()) { + Matrix m = parseTransform(transform); + if (!m.isIdentity()) { + node->setMatrixAttribute("transform", m); + } + } + + std::string opacity = element->getAttribute("opacity"); + if (!opacity.empty()) { + node->setFloatAttribute("opacity", std::stof(opacity)); + } + + std::string display = element->getAttribute("display"); + if (display == "none") { + node->setBoolAttribute("visible", false); + } + + std::string visibility = element->getAttribute("visibility"); + if (visibility == "hidden") { + node->setBoolAttribute("visible", false); + } +} + +void SVGParserImpl::parseFillStroke(SVGXMLNode* element, PAGXNode* node) { + std::string fill = element->getAttribute("fill"); + if (!fill.empty() && fill != "none") { + auto fillNode = PAGXNode::Make(PAGXNodeType::Fill); + if (fill.find("url(") == 0) { + std::string refId = resolveUrl(fill); + fillNode->setAttribute("colorSourceRef", refId); + } else { + Color color = parseColor(fill); + std::string fillOpacity = element->getAttribute("fill-opacity"); + if (!fillOpacity.empty()) { + color = color.withAlpha(std::stof(fillOpacity)); + } + fillNode->setColorAttribute("color", color); + } + node->appendChild(std::move(fillNode)); + } else if (fill.empty()) { + // SVG default is black fill + auto fillNode = PAGXNode::Make(PAGXNodeType::Fill); + fillNode->setColorAttribute("color", Color::Black()); + node->appendChild(std::move(fillNode)); + } + + std::string stroke = element->getAttribute("stroke"); + if (!stroke.empty() && stroke != "none") { + auto strokeNode = PAGXNode::Make(PAGXNodeType::Stroke); + if (stroke.find("url(") == 0) { + std::string refId = resolveUrl(stroke); + strokeNode->setAttribute("colorSourceRef", refId); + } else { + Color color = parseColor(stroke); + std::string strokeOpacity = element->getAttribute("stroke-opacity"); + if (!strokeOpacity.empty()) { + color = color.withAlpha(std::stof(strokeOpacity)); + } + strokeNode->setColorAttribute("color", color); + } + + std::string strokeWidth = element->getAttribute("stroke-width"); + if (!strokeWidth.empty()) { + strokeNode->setFloatAttribute("width", parseLength(strokeWidth, _viewBoxWidth)); + } + + std::string strokeLinecap = element->getAttribute("stroke-linecap"); + if (!strokeLinecap.empty()) { + strokeNode->setAttribute("lineCap", strokeLinecap); + } + + std::string strokeLinejoin = element->getAttribute("stroke-linejoin"); + if (!strokeLinejoin.empty()) { + strokeNode->setAttribute("lineJoin", strokeLinejoin); + } + + std::string strokeMiterlimit = element->getAttribute("stroke-miterlimit"); + if (!strokeMiterlimit.empty()) { + strokeNode->setFloatAttribute("miterLimit", std::stof(strokeMiterlimit)); + } + + node->appendChild(std::move(strokeNode)); + } +} + +Matrix SVGParserImpl::parseTransform(const std::string& value) { + Matrix result = Matrix::Identity(); + if (value.empty()) { + return result; + } + + const char* ptr = value.c_str(); + const char* end = ptr + value.length(); + + auto skipWS = [&]() { + while (ptr < end && (std::isspace(*ptr) || *ptr == ',')) { + ++ptr; + } + }; + + auto readNumber = [&]() -> float { + skipWS(); + const char* start = ptr; + if (*ptr == '-' || *ptr == '+') { + ++ptr; + } + while (ptr < end && (std::isdigit(*ptr) || *ptr == '.')) { + ++ptr; + } + if (ptr < end && (*ptr == 'e' || *ptr == 'E')) { + ++ptr; + if (*ptr == '-' || *ptr == '+') { + ++ptr; + } + while (ptr < end && std::isdigit(*ptr)) { + ++ptr; + } + } + return std::stof(std::string(start, ptr)); + }; + + while (ptr < end) { + skipWS(); + if (ptr >= end) { + break; + } + + std::string func; + while (ptr < end && std::isalpha(*ptr)) { + func += *ptr++; + } + + skipWS(); + if (*ptr != '(') { + break; + } + ++ptr; + + Matrix m = Matrix::Identity(); + + if (func == "translate") { + float tx = readNumber(); + skipWS(); + float ty = 0; + if (ptr < end && *ptr != ')') { + ty = readNumber(); + } + m = Matrix::Translate(tx, ty); + } else if (func == "scale") { + float sx = readNumber(); + skipWS(); + float sy = sx; + if (ptr < end && *ptr != ')') { + sy = readNumber(); + } + m = Matrix::Scale(sx, sy); + } else if (func == "rotate") { + float angle = readNumber(); + skipWS(); + if (ptr < end && *ptr != ')') { + float cx = readNumber(); + float cy = readNumber(); + m = Matrix::Translate(cx, cy) * Matrix::Rotate(angle) * Matrix::Translate(-cx, -cy); + } else { + m = Matrix::Rotate(angle); + } + } else if (func == "skewX") { + float angle = readNumber(); + float radians = angle * 3.14159265358979323846f / 180.0f; + m.skewX = std::tan(radians); + } else if (func == "skewY") { + float angle = readNumber(); + float radians = angle * 3.14159265358979323846f / 180.0f; + m.skewY = std::tan(radians); + } else if (func == "matrix") { + m.scaleX = readNumber(); + m.skewY = readNumber(); + m.skewX = readNumber(); + m.scaleY = readNumber(); + m.transX = readNumber(); + m.transY = readNumber(); + } + + skipWS(); + if (*ptr == ')') { + ++ptr; + } + + result = result * m; + } + + return result; +} + +Color SVGParserImpl::parseColor(const std::string& value) { + if (value.empty() || value == "none") { + return Color::Transparent(); + } + + if (value[0] == '#') { + uint32_t hex = 0; + if (value.length() == 4) { + // #RGB -> #RRGGBB + char r = value[1]; + char g = value[2]; + char b = value[3]; + std::string expanded = std::string() + r + r + g + g + b + b; + hex = 0xFF000000 | std::stoul(expanded, nullptr, 16); + } else if (value.length() == 7) { + hex = 0xFF000000 | std::stoul(value.substr(1), nullptr, 16); + } else if (value.length() == 9) { + hex = std::stoul(value.substr(1), nullptr, 16); + } + return Color::FromHex(hex); + } + + if (value.find("rgb") == 0) { + size_t start = value.find('('); + size_t end = value.find(')'); + if (start != std::string::npos && end != std::string::npos) { + std::string inner = value.substr(start + 1, end - start - 1); + std::istringstream iss(inner); + float r = 0, g = 0, b = 0, a = 1.0f; + char comma; + iss >> r >> comma >> g >> comma >> b; + if (value.find("rgba") == 0) { + iss >> comma >> a; + } + return Color::FromRGBA(r / 255.0f, g / 255.0f, b / 255.0f, a); + } + } + + // Named colors (subset) + static const std::unordered_map namedColors = { + {"black", 0xFF000000}, {"white", 0xFFFFFFFF}, {"red", 0xFFFF0000}, + {"green", 0xFF008000}, {"blue", 0xFF0000FF}, {"yellow", 0xFFFFFF00}, + {"cyan", 0xFF00FFFF}, {"magenta", 0xFFFF00FF}, {"gray", 0xFF808080}, + {"grey", 0xFF808080}, {"silver", 0xFFC0C0C0}, {"maroon", 0xFF800000}, + {"olive", 0xFF808000}, {"lime", 0xFF00FF00}, {"aqua", 0xFF00FFFF}, + {"teal", 0xFF008080}, {"navy", 0xFF000080}, {"fuchsia", 0xFFFF00FF}, + {"purple", 0xFF800080}, {"orange", 0xFFFFA500}, {"transparent", 0x00000000}, + }; + + auto it = namedColors.find(value); + if (it != namedColors.end()) { + return Color::FromHex(it->second); + } + + return Color::Black(); +} + +float SVGParserImpl::parseLength(const std::string& value, float containerSize) { + if (value.empty()) { + return 0; + } + + size_t idx = 0; + float num = std::stof(value, &idx); + + std::string unit = value.substr(idx); + if (unit == "%") { + return num / 100.0f * containerSize; + } + if (unit == "px" || unit.empty()) { + return num; + } + if (unit == "pt") { + return num * 1.333333f; + } + if (unit == "em" || unit == "rem") { + return num * 16.0f; // Assume 16px base font + } + if (unit == "in") { + return num * 96.0f; + } + if (unit == "cm") { + return num * 37.795275591f; + } + if (unit == "mm") { + return num * 3.7795275591f; + } + + return num; +} + +std::vector SVGParserImpl::parseViewBox(const std::string& value) { + std::vector result; + if (value.empty()) { + return result; + } + + std::istringstream iss(value); + float num = 0; + while (iss >> num) { + result.push_back(num); + char c; + iss >> c; // Skip separator + } + + return result; +} + +PathData SVGParserImpl::parsePoints(const std::string& value, bool closed) { + PathData path; + if (value.empty()) { + return path; + } + + std::vector points; + std::istringstream iss(value); + float num = 0; + while (iss >> num) { + points.push_back(num); + char c; + if (iss.peek() == ',' || iss.peek() == ' ') { + iss >> c; + } + } + + if (points.size() >= 2) { + path.moveTo(points[0], points[1]); + for (size_t i = 2; i + 1 < points.size(); i += 2) { + path.lineTo(points[i], points[i + 1]); + } + if (closed) { + path.close(); + } + } + + return path; +} + +std::string SVGParserImpl::resolveUrl(const std::string& url) { + if (url.empty()) { + return ""; + } + // Handle url(#id) format + if (url.find("url(") == 0) { + size_t start = url.find('#'); + size_t end = url.find(')'); + if (start != std::string::npos && end != std::string::npos) { + return url.substr(start + 1, end - start - 1); + } + } + // Handle #id format + if (url[0] == '#') { + return url.substr(1); + } + return url; +} + +} // namespace pagx diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp deleted file mode 100644 index e9e5530103..0000000000 --- a/pagx/src/svg/SVGImporter.cpp +++ /dev/null @@ -1,68 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "pagx/svg/SVGImporter.h" -#include -#include "SVGToPAGXConverter.h" -#include "tgfx/core/Data.h" -#include "tgfx/core/Stream.h" - -namespace pagx { - -std::string SVGImporter::ImportFromFile(const std::string& svgFilePath) { - auto data = tgfx::Data::MakeFromFile(svgFilePath); - if (!data) { - return ""; - } - auto stream = tgfx::Stream::MakeFromData(std::move(data)); - if (!stream) { - return ""; - } - return ImportFromStream(*stream); -} - -std::string SVGImporter::ImportFromStream(tgfx::Stream& svgStream) { - auto svgDOM = tgfx::SVGDOM::Make(svgStream); - if (!svgDOM) { - return ""; - } - return ImportFromDOM(svgDOM); -} - -std::string SVGImporter::ImportFromDOM(const std::shared_ptr& svgDOM) { - if (!svgDOM) { - return ""; - } - SVGToPAGXConverter converter(svgDOM); - return converter.convert(); -} - -bool SVGImporter::SaveToFile(const std::string& pagxContent, const std::string& outputPath) { - if (pagxContent.empty() || outputPath.empty()) { - return false; - } - std::ofstream file(outputPath); - if (!file.is_open()) { - return false; - } - file << pagxContent; - file.close(); - return true; -} - -} // namespace pagx diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h new file mode 100644 index 0000000000..e8a2414c36 --- /dev/null +++ b/pagx/src/svg/SVGParserInternal.h @@ -0,0 +1,98 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include "pagx/PAGXDocument.h" +#include "pagx/PAGXSVGParser.h" + +namespace pagx { + +/** + * Internal SVG XML node representation. + */ +struct SVGXMLNode { + std::string tagName = {}; + std::unordered_map attributes = {}; + std::vector> children = {}; + std::string textContent = {}; + + std::string getAttribute(const std::string& name, const std::string& defaultValue = "") const { + auto it = attributes.find(name); + return it != attributes.end() ? it->second : defaultValue; + } + + bool hasAttribute(const std::string& name) const { + return attributes.find(name) != attributes.end(); + } +}; + +/** + * Internal SVG parser implementation. + */ +class SVGParserImpl { + public: + SVGParserImpl(const PAGXSVGParser::Options& options); + + std::shared_ptr parse(const uint8_t* data, size_t length); + + private: + std::unique_ptr parseXML(const char* data, size_t length); + void parseSVGRoot(SVGXMLNode* svgNode); + void parseDefs(SVGXMLNode* defsNode); + std::unique_ptr convertElement(SVGXMLNode* element); + std::unique_ptr convertG(SVGXMLNode* element); + std::unique_ptr convertRect(SVGXMLNode* element); + std::unique_ptr convertCircle(SVGXMLNode* element); + std::unique_ptr convertEllipse(SVGXMLNode* element); + std::unique_ptr convertLine(SVGXMLNode* element); + std::unique_ptr convertPolyline(SVGXMLNode* element); + std::unique_ptr convertPolygon(SVGXMLNode* element); + std::unique_ptr convertPath(SVGXMLNode* element); + std::unique_ptr convertText(SVGXMLNode* element); + std::unique_ptr convertImage(SVGXMLNode* element); + std::unique_ptr convertUse(SVGXMLNode* element); + std::unique_ptr convertLinearGradient(SVGXMLNode* element); + std::unique_ptr convertRadialGradient(SVGXMLNode* element); + std::unique_ptr convertPattern(SVGXMLNode* element); + std::unique_ptr convertMask(SVGXMLNode* element); + std::unique_ptr convertClipPath(SVGXMLNode* element); + std::unique_ptr convertFilter(SVGXMLNode* element); + + void parseCommonAttributes(SVGXMLNode* element, PAGXNode* node); + void parseFillStroke(SVGXMLNode* element, PAGXNode* node); + Matrix parseTransform(const std::string& value); + Color parseColor(const std::string& value); + float parseLength(const std::string& value, float containerSize = 0); + std::vector parseViewBox(const std::string& value); + PathData parsePoints(const std::string& value, bool closed); + + std::string resolveUrl(const std::string& url); + + PAGXSVGParser::Options _options = {}; + std::shared_ptr _document = nullptr; + std::unordered_map _defs = {}; + float _viewBoxWidth = 0; + float _viewBoxHeight = 0; +}; + +} // namespace pagx diff --git a/pagx/src/svg/SVGToPAGXConverter.cpp b/pagx/src/svg/SVGToPAGXConverter.cpp deleted file mode 100644 index 389ee4165b..0000000000 --- a/pagx/src/svg/SVGToPAGXConverter.cpp +++ /dev/null @@ -1,1897 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "SVGToPAGXConverter.h" -#include -#include -#include "tgfx/svg/SVGPathParser.h" -#include "tgfx/svg/node/SVGImage.h" -#include "tgfx/svg/node/SVGLinearGradient.h" -#include "tgfx/svg/node/SVGPattern.h" -#include "tgfx/svg/node/SVGRadialGradient.h" -#include "tgfx/svg/node/SVGStop.h" -#include "tgfx/svg/node/SVGUse.h" - -namespace pagx { - -// Helper to get value from SVGProperty -template -static std::optional GetPropertyValue(const SVGProperty& prop) { - return prop.get(); -} - -template -static std::optional GetPropertyValue(const SVGProperty& prop) { - return prop.get(); -} - -static std::string CleanFontFamily(const std::string& family) { - std::string result = family; - if (result.size() >= 2) { - if ((result.front() == '"' && result.back() == '"') || - (result.front() == '\'' && result.back() == '\'')) { - result = result.substr(1, result.size() - 2); - } - } - return result; -} - -SVGToPAGXConverter::SVGToPAGXConverter(const std::shared_ptr& svgDOM) : _svgDOM(svgDOM) { -} - -std::string SVGToPAGXConverter::convert() { - if (!_svgDOM || !_svgDOM->getRoot()) { - return ""; - } - - auto root = _svgDOM->getRoot(); - auto containerSize = _svgDOM->getContainerSize(); - _width = containerSize.width; - _height = containerSize.height; - - if (_width <= 0 || _height <= 0) { - auto viewBox = root->getViewBox(); - if (viewBox.has_value()) { - _width = viewBox->width(); - _height = viewBox->height(); - } - } - - if (_width <= 0) { - _width = 100; - } - if (_height <= 0) { - _height = 100; - } - - collectGradients(); - collectPatterns(); - collectMasks(); - collectUsedMasks(root.get()); - countColorSourceUsages(); - - writeHeader(); - writeResources(); - writeMaskLayers(1); - writeLayers(); - - _output << "\n"; - return _output.str(); -} - -void SVGToPAGXConverter::collectUsedMasks(const SVGNode* node) { - if (!node) { - return; - } - - auto maskId = getMaskId(node); - if (!maskId.empty()) { - _usedMasks.insert(maskId); - } - - // Recursively check children if node is a container - if (node->hasChildren()) { - auto container = static_cast(node); - for (const auto& child : container->getChildren()) { - collectUsedMasks(child.get()); - } - } -} - -void SVGToPAGXConverter::writeHeader() { - _output << "\n"; - _output << "(_width) << "\" height=\"" - << static_cast(_height) << "\">\n"; -} - -void SVGToPAGXConverter::collectGradients() { - // Collect gradients from nodeIDMapper directly instead of traversing the whole tree - for (const auto& [id, svgNode] : _svgDOM->nodeIDMapper()) { - auto tag = svgNode->tag(); - if (tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient) { - _gradients[id] = svgNode.get(); - } - } -} - -void SVGToPAGXConverter::collectPatterns() { - // Collect patterns from nodeIDMapper - for (const auto& [id, svgNode] : _svgDOM->nodeIDMapper()) { - if (svgNode->tag() != SVGTag::Pattern) { - continue; - } - - auto pattern = static_cast(svgNode.get()); - auto container = static_cast(pattern); - - // Check if pattern uses objectBoundingBox units - bool isObjectBoundingBox = - pattern->getPatternUnits().type() == SVGObjectBoundingBoxUnits::Type::ObjectBoundingBox; - - // Pattern may contain elements that reference elements - for (const auto& child : container->getChildren()) { - if (child->tag() != SVGTag::Use) { - continue; - } - - auto use = static_cast(child.get()); - auto href = use->getHref(); - auto imageId = href.iri(); - if (imageId.empty()) { - continue; - } - - // Find the referenced image - auto imageIt = _svgDOM->nodeIDMapper().find(imageId); - if (imageIt == _svgDOM->nodeIDMapper().end() || imageIt->second->tag() != SVGTag::Image) { - continue; - } - - auto image = static_cast(imageIt->second.get()); - auto imageHref = image->getHref(); - auto imageData = imageHref.iri(); - - // Store image if not already stored - if (_images.find(imageId) == _images.end()) { - _images[imageId] = imageData; - } - - // Get image dimensions - float imageWidth = image->getWidth().value(); - float imageHeight = image->getHeight().value(); - - // Get pattern dimensions - auto patternWidth = pattern->getWidth(); - auto patternHeight = pattern->getHeight(); - - PatternInfo patternInfo = {}; - patternInfo.patternId = id; - patternInfo.imageId = imageId; - patternInfo.imageData = imageData; - patternInfo.imageWidth = imageWidth; - patternInfo.imageHeight = imageHeight; - patternInfo.isObjectBoundingBox = isObjectBoundingBox; - - // Store pattern dimensions - if (patternWidth.has_value() && patternHeight.has_value()) { - patternInfo.patternWidth = patternWidth->value(); - patternInfo.patternHeight = patternHeight->value(); - } - - _patterns[id] = patternInfo; - } - } -} - -void SVGToPAGXConverter::collectMasks() { - // Collect masks from nodeIDMapper - for (const auto& [id, svgNode] : _svgDOM->nodeIDMapper()) { - if (svgNode->tag() != SVGTag::Mask) { - continue; - } - - auto mask = static_cast(svgNode.get()); - MaskInfo maskInfo = {}; - maskInfo.maskId = id; - maskInfo.maskNode = mask; - maskInfo.isLuminance = mask->getMaskType().type() == SVGMaskType::Type::Luminance; - - _masks[id] = maskInfo; - } -} - -void SVGToPAGXConverter::countColorSourceUsages() { - auto root = _svgDOM->getRoot(); - for (const auto& child : root->getChildren()) { - countColorSourceUsagesFromNode(child.get()); - } -} - -void SVGToPAGXConverter::countColorSourceUsagesFromNode(const SVGNode* node) { - if (!node) { - return; - } - - auto tag = node->tag(); - - // Skip non-renderable elements - if (tag == SVGTag::Defs || tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient || - tag == SVGTag::Stop || tag == SVGTag::ClipPath || tag == SVGTag::Mask || - tag == SVGTag::Filter || tag == SVGTag::Pattern) { - return; - } - - // Check fill attribute for color source references - auto fillProp = node->getFill(); - auto fillOpt = GetPropertyValue(fillProp); - if (fillOpt.has_value() && fillOpt->type() == SVGPaint::Type::IRI) { - auto iri = fillOpt->iri().iri(); - auto& usage = _colorSourceUsages[iri]; - usage.count++; - if (_gradients.find(iri) != _gradients.end()) { - usage.type = "gradient"; - } else if (_patterns.find(iri) != _patterns.end()) { - usage.type = "pattern"; - } - } - - // Check stroke attribute for color source references - auto strokeProp = node->getStroke(); - auto strokeOpt = GetPropertyValue(strokeProp); - if (strokeOpt.has_value() && strokeOpt->type() == SVGPaint::Type::IRI) { - auto iri = strokeOpt->iri().iri(); - auto& usage = _colorSourceUsages[iri]; - usage.count++; - if (_gradients.find(iri) != _gradients.end()) { - usage.type = "gradient"; - } else if (_patterns.find(iri) != _patterns.end()) { - usage.type = "pattern"; - } - } - - // Recurse into containers - if (tag == SVGTag::G || tag == SVGTag::Svg) { - auto container = static_cast(node); - for (const auto& child : container->getChildren()) { - countColorSourceUsagesFromNode(child.get()); - } - } -} - -void SVGToPAGXConverter::writeResources() { - // Collect resources that need to be in Resources section (referenced more than once) - std::vector sharedGradients = {}; - std::vector sharedPatterns = {}; - - for (const auto& [id, usage] : _colorSourceUsages) { - if (usage.count > 1) { - if (usage.type == "gradient") { - sharedGradients.push_back(id); - } else if (usage.type == "pattern") { - // For patterns with objectBoundingBox, always inline since they depend on shape size - auto patternIt = _patterns.find(id); - if (patternIt != _patterns.end() && !patternIt->second.isObjectBoundingBox) { - sharedPatterns.push_back(id); - } - } - } - } - - // Check if we have any shared resources or images - bool hasSharedResources = !sharedGradients.empty() || !sharedPatterns.empty() || !_images.empty(); - if (!hasSharedResources) { - return; - } - - _output << "\n"; - indent(1); - _output << "\n"; - - // Output shared gradients - for (const auto& id : sharedGradients) { - auto it = _gradients.find(id); - if (it == _gradients.end()) { - continue; - } - auto node = it->second; - auto tag = node->tag(); - if (tag == SVGTag::LinearGradient) { - auto gradient = static_cast(node); - indent(2); - _output << "getX1(), _width) << "\""; - _output << " startY=\"" << lengthToFloat(gradient->getY1(), _height) << "\""; - _output << " endX=\"" << lengthToFloat(gradient->getX2(), _width) << "\""; - _output << " endY=\"" << lengthToFloat(gradient->getY2(), _height) << "\""; - _output << ">\n"; - - auto container = static_cast(node); - for (const auto& child : container->getChildren()) { - if (child->tag() == SVGTag::Stop) { - auto stop = static_cast(child.get()); - indent(3); - auto offset = stop->getOffset(); - float offsetValue = offset.value(); - if (offset.unit() == SVGLength::Unit::Percentage) { - offsetValue = offsetValue / 100.0f; - } - _output << "getStopColor(); - auto stopColorOpt = GetPropertyValue(stopColorProp); - if (stopColorOpt.has_value()) { - auto color = stopColorOpt->color(); - auto opacityProp = stop->getStopOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - float alpha = opacityOpt.has_value() ? opacityOpt.value() : 1.0f; - Color c = color; - c.alpha = alpha; - _output << " color=\"" << colorToHex(c) << "\""; - } - _output << "/>\n"; - } - } - - indent(2); - _output << "\n"; - } else if (tag == SVGTag::RadialGradient) { - auto gradient = static_cast(node); - indent(2); - _output << "getCx(), _width) << "\""; - _output << " centerY=\"" << lengthToFloat(gradient->getCy(), _height) << "\""; - auto r = gradient->getR(); - _output << " radius=\"" << lengthToFloat(r, std::max(_width, _height)) << "\""; - _output << ">\n"; - - auto container = static_cast(node); - for (const auto& child : container->getChildren()) { - if (child->tag() == SVGTag::Stop) { - auto stop = static_cast(child.get()); - indent(3); - auto offset = stop->getOffset(); - float offsetValue = offset.value(); - if (offset.unit() == SVGLength::Unit::Percentage) { - offsetValue = offsetValue / 100.0f; - } - _output << "getStopColor(); - auto stopColorOpt = GetPropertyValue(stopColorProp); - if (stopColorOpt.has_value()) { - auto color = stopColorOpt->color(); - auto opacityProp = stop->getStopOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - float alpha = opacityOpt.has_value() ? opacityOpt.value() : 1.0f; - Color c = color; - c.alpha = alpha; - _output << " color=\"" << colorToHex(c) << "\""; - } - _output << "/>\n"; - } - } - - indent(2); - _output << "\n"; - } - } - - // Output Image resources (always needed for patterns) - for (const auto& [imageId, imageData] : _images) { - indent(2); - _output << "\n"; - } - - // Output shared ImagePattern resources (non-objectBoundingBox patterns referenced multiple times) - for (const auto& patternId : sharedPatterns) { - auto it = _patterns.find(patternId); - if (it == _patterns.end()) { - continue; - } - const auto& patternInfo = it->second; - indent(2); - _output << "\n"; - } - - indent(1); - _output << "\n"; -} - -void SVGToPAGXConverter::writeLayers() { - auto root = _svgDOM->getRoot(); - _output << "\n"; - - convertChildren(root->getChildren(), 1); -} - -void SVGToPAGXConverter::convertNode(const SVGNode* node, int depth, bool /*needScopeIsolation*/) { - if (!node) { - return; - } - - auto tag = node->tag(); - - if (tag == SVGTag::Defs || tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient || - tag == SVGTag::Stop || tag == SVGTag::ClipPath || tag == SVGTag::Mask || - tag == SVGTag::Filter) { - return; - } - - auto visibilityProp = node->getVisibility(); - auto visibilityOpt = GetPropertyValue(visibilityProp); - if (visibilityOpt.has_value() && visibilityOpt.value().type() == SVGVisibility::Type::Hidden) { - return; - } - - switch (tag) { - case SVGTag::G: - case SVGTag::Svg: - convertContainer(static_cast(node), depth); - break; - case SVGTag::Rect: - convertRect(static_cast(node), depth); - break; - case SVGTag::Circle: - convertCircle(static_cast(node), depth); - break; - case SVGTag::Ellipse: - convertEllipse(static_cast(node), depth); - break; - case SVGTag::Path: - convertPath(static_cast(node), depth); - break; - case SVGTag::Polygon: - case SVGTag::Polyline: - convertPoly(static_cast(node), depth); - break; - case SVGTag::Line: - convertLine(static_cast(node), depth); - break; - case SVGTag::Text: - convertText(static_cast(node), depth); - break; - default: - break; - } -} - -void SVGToPAGXConverter::convertContainer(const SVGContainer* container, int depth) { - if (!container || !container->hasChildren()) { - return; - } - - int renderableCount = countRenderableChildren(container); - - // If container has only one renderable child, don't create a wrapper Layer for the container - // Just output the child directly - if (renderableCount == 1) { - convertChildren(container->getChildren(), depth); - return; - } - - // Container with multiple children needs a wrapper Layer - indent(depth); - _output << "(container); - auto transform = transformable->getTransform(); - if (!transform.isIdentity()) { - _output << " matrix=\"" << matrixToString(transform) << "\""; - } - - auto opacityProp = container->getOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - if (opacityOpt.has_value() && opacityOpt.value() < 1.0f) { - _output << " alpha=\"" << opacityOpt.value() << "\""; - } - - _output << ">\n"; - - convertChildren(container->getChildren(), depth + 1); - - indent(depth); - _output << "\n"; -} - -void SVGToPAGXConverter::convertRect(const SVGRect* rect, int depth) { - if (!rect) { - return; - } - - float x = std::stof(lengthToFloat(rect->getX(), _width)); - float y = std::stof(lengthToFloat(rect->getY(), _height)); - float width = std::stof(lengthToFloat(rect->getWidth(), _width)); - float height = std::stof(lengthToFloat(rect->getHeight(), _height)); - - if (width <= 0 || height <= 0) { - return; - } - - float centerX = x + width / 2.0f; - float centerY = y + height / 2.0f; - - float rx = 0; - float ry = 0; - auto rxOpt = rect->getRx(); - auto ryOpt = rect->getRy(); - if (rxOpt.has_value()) { - rx = std::stof(lengthToFloat(*rxOpt, _width)); - } - if (ryOpt.has_value()) { - ry = std::stof(lengthToFloat(*ryOpt, _height)); - } - if (rx > 0 && ry <= 0) { - ry = rx; - } - if (ry > 0 && rx <= 0) { - rx = ry; - } - - auto transformable = static_cast(rect); - auto transform = transformable->getTransform(); - bool hasTransform = !transform.isIdentity(); - - auto opacityProp = rect->getOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; - - // Check for mask - std::string maskId = getMaskId(rect); - if (!maskId.empty()) { - _usedMasks.insert(maskId); - } - - // Each shape element always gets its own Layer - indent(depth); - _output << "second.isLuminance) { - _output << " maskType=\"luminance\""; - } - } - _output << ">\n"; - indent(depth + 1); - _output << "\n"; - - // Group is needed when there's a transform or opacity to apply - int contentDepth = depth + 2; - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - contentDepth = depth + 3; - } - - indent(contentDepth); - _output << " 0 || ry > 0) { - float roundness = std::min(rx, ry); - _output << " roundness=\"" << roundness << "\""; - } - _output << "/>\n"; - - writeFillStyle(rect, contentDepth, x, y, width, height); - writeStrokeStyle(rect, contentDepth, x, y, width, height); - - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - } - - indent(depth + 1); - _output << "\n"; - indent(depth); - _output << "\n"; -} - -void SVGToPAGXConverter::convertCircle(const SVGCircle* circle, int depth) { - if (!circle) { - return; - } - - float cx = std::stof(lengthToFloat(circle->getCx(), _width)); - float cy = std::stof(lengthToFloat(circle->getCy(), _height)); - float r = std::stof(lengthToFloat(circle->getR(), std::max(_width, _height))); - - if (r <= 0) { - return; - } - - auto transformable = static_cast(circle); - auto transform = transformable->getTransform(); - bool hasTransform = !transform.isIdentity(); - - auto opacityProp = circle->getOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; - - std::string maskId = getMaskId(circle); - if (!maskId.empty()) { - _usedMasks.insert(maskId); - } - - indent(depth); - _output << "second.isLuminance) { - _output << " maskType=\"luminance\""; - } - } - _output << ">\n"; - indent(depth + 1); - _output << "\n"; - - int contentDepth = depth + 2; - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - contentDepth = depth + 3; - } - - indent(contentDepth); - _output << "\n"; - - writeFillStyle(circle, contentDepth, cx - r, cy - r, r * 2, r * 2); - writeStrokeStyle(circle, contentDepth, cx - r, cy - r, r * 2, r * 2); - - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - } - - indent(depth + 1); - _output << "\n"; - indent(depth); - _output << "\n"; -} - -void SVGToPAGXConverter::convertEllipse(const SVGEllipse* ellipse, int depth) { - if (!ellipse) { - return; - } - - float cx = std::stof(lengthToFloat(ellipse->getCx(), _width)); - float cy = std::stof(lengthToFloat(ellipse->getCy(), _height)); - - float rx = 0; - float ry = 0; - auto rxOpt = ellipse->getRx(); - auto ryOpt = ellipse->getRy(); - if (rxOpt.has_value()) { - rx = std::stof(lengthToFloat(*rxOpt, _width)); - } - if (ryOpt.has_value()) { - ry = std::stof(lengthToFloat(*ryOpt, _height)); - } - - if (rx <= 0 || ry <= 0) { - return; - } - - auto transformable = static_cast(ellipse); - auto transform = transformable->getTransform(); - bool hasTransform = !transform.isIdentity(); - - auto opacityProp = ellipse->getOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; - - std::string maskId = getMaskId(ellipse); - if (!maskId.empty()) { - _usedMasks.insert(maskId); - } - - indent(depth); - _output << "second.isLuminance) { - _output << " maskType=\"luminance\""; - } - } - _output << ">\n"; - indent(depth + 1); - _output << "\n"; - - int contentDepth = depth + 2; - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - contentDepth = depth + 3; - } - - indent(contentDepth); - _output << "\n"; - - writeFillStyle(ellipse, contentDepth, cx - rx, cy - ry, rx * 2, ry * 2); - writeStrokeStyle(ellipse, contentDepth, cx - rx, cy - ry, rx * 2, ry * 2); - - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - } - - indent(depth + 1); - _output << "\n"; - indent(depth); - _output << "\n"; -} - -void SVGToPAGXConverter::convertPath(const SVGPath* path, int depth) { - if (!path) { - return; - } - - auto shapePath = path->getShapePath(); - if (shapePath.isEmpty()) { - return; - } - - auto transformable = static_cast(path); - auto transform = transformable->getTransform(); - bool hasTransform = !transform.isIdentity(); - - auto opacityProp = path->getOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; - - std::string maskId = getMaskId(path); - if (!maskId.empty()) { - _usedMasks.insert(maskId); - } - - indent(depth); - _output << "second.isLuminance) { - _output << " maskType=\"luminance\""; - } - } - _output << ">\n"; - indent(depth + 1); - _output << "\n"; - - int contentDepth = depth + 2; - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - contentDepth = depth + 3; - } - - indent(contentDepth); - _output << "\n"; - - auto pathBounds = shapePath.getBounds(); - writeFillStyle(path, contentDepth, pathBounds.left, pathBounds.top, pathBounds.width(), - pathBounds.height()); - writeStrokeStyle(path, contentDepth, pathBounds.left, pathBounds.top, pathBounds.width(), - pathBounds.height()); - - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - } - - indent(depth + 1); - _output << "\n"; - indent(depth); - _output << "\n"; -} - -void SVGToPAGXConverter::convertLine(const SVGLine* line, int depth) { - if (!line) { - return; - } - - auto x1 = line->getX1().value(); - auto y1 = line->getY1().value(); - auto x2 = line->getX2().value(); - auto y2 = line->getY2().value(); - - Path linePath; - linePath.moveTo(x1, y1); - linePath.lineTo(x2, y2); - - if (linePath.isEmpty()) { - return; - } - - auto transformable = static_cast(line); - auto transform = transformable->getTransform(); - bool hasTransform = !transform.isIdentity(); - - auto opacityProp = line->getOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; - - std::string maskId = getMaskId(line); - if (!maskId.empty()) { - _usedMasks.insert(maskId); - } - - indent(depth); - _output << "second.isLuminance) { - _output << " maskType=\"luminance\""; - } - } - _output << ">\n"; - indent(depth + 1); - _output << "\n"; - - int contentDepth = depth + 2; - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - contentDepth = depth + 3; - } - - indent(contentDepth); - _output << "\n"; - - auto lineBounds = linePath.getBounds(); - writeFillStyle(line, contentDepth, lineBounds.left, lineBounds.top, lineBounds.width(), - lineBounds.height()); - writeStrokeStyle(line, contentDepth, lineBounds.left, lineBounds.top, lineBounds.width(), - lineBounds.height()); - - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - } - - indent(depth + 1); - _output << "\n"; - indent(depth); - _output << "\n"; -} - -void SVGToPAGXConverter::convertPoly(const SVGPoly* poly, int depth) { - if (!poly) { - return; - } - - const auto& points = poly->getPoints(); - if (points.empty()) { - return; - } - - Path polyPath; - polyPath.moveTo(points[0]); - for (size_t i = 1; i < points.size(); ++i) { - polyPath.lineTo(points[i]); - } - if (poly->tag() == SVGTag::Polygon) { - polyPath.close(); - } - - if (polyPath.isEmpty()) { - return; - } - - auto transformable = static_cast(poly); - auto transform = transformable->getTransform(); - bool hasTransform = !transform.isIdentity(); - - auto opacityProp = poly->getOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; - - std::string maskId = getMaskId(poly); - if (!maskId.empty()) { - _usedMasks.insert(maskId); - } - - indent(depth); - _output << "second.isLuminance) { - _output << " maskType=\"luminance\""; - } - } - _output << ">\n"; - indent(depth + 1); - _output << "\n"; - - int contentDepth = depth + 2; - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - contentDepth = depth + 3; - } - - indent(contentDepth); - _output << "\n"; - - auto polyBounds = polyPath.getBounds(); - writeFillStyle(poly, contentDepth, polyBounds.left, polyBounds.top, polyBounds.width(), - polyBounds.height()); - writeStrokeStyle(poly, contentDepth, polyBounds.left, polyBounds.top, polyBounds.width(), - polyBounds.height()); - - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - } - - indent(depth + 1); - _output << "\n"; - indent(depth); - _output << "\n"; -} - -void SVGToPAGXConverter::convertText(const SVGText* text, int depth) { - if (!text) { - return; - } - - const auto& textChildren = text->getTextChildren(); - if (textChildren.empty()) { - return; - } - - auto transformable = static_cast(text); - auto transform = transformable->getTransform(); - bool hasTransform = !transform.isIdentity(); - - auto opacityProp = text->getOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; - - // Get text-level x, y position - auto xList = text->getX(); - auto yList = text->getY(); - float baseX = xList.empty() ? 0.0f : std::stof(lengthToFloat(xList[0], _width)); - float baseY = yList.empty() ? 0.0f : std::stof(lengthToFloat(yList[0], _height)); - - // Get font properties from text element - auto fontFamilyProp = text->getFontFamily(); - auto fontFamilyOpt = GetPropertyValue(fontFamilyProp); - std::string fontFamily = - fontFamilyOpt.has_value() ? CleanFontFamily(fontFamilyOpt->family()) : ""; - - auto fontSizeProp = text->getFontSize(); - auto fontSizeOpt = GetPropertyValue(fontSizeProp); - float fontSize = fontSizeOpt.has_value() ? std::stof(lengthToFloat(fontSizeOpt->size(), _height)) - : 12.0f; - - // Get text-anchor property - auto textAnchorProp = text->getTextAnchor(); - auto textAnchorOpt = GetPropertyValue(textAnchorProp); - std::string textAnchor = "start"; - if (textAnchorOpt.has_value()) { - switch (textAnchorOpt->type()) { - case SVGTextAnchor::Type::Middle: - textAnchor = "middle"; - break; - case SVGTextAnchor::Type::End: - textAnchor = "end"; - break; - default: - textAnchor = "start"; - break; - } - } - - std::string maskId = getMaskId(text); - if (!maskId.empty()) { - _usedMasks.insert(maskId); - } - - indent(depth); - _output << "second.isLuminance) { - _output << " maskType=\"luminance\""; - } - } - _output << ">\n"; - - indent(depth + 1); - _output << "\n"; - - // Process each text child (TextLiteral or TSpan) - for (const auto& child : textChildren) { - auto tag = child->tag(); - if (tag == SVGTag::TextLiteral) { - auto literal = static_cast(child.get()); - const std::string& textContent = literal->getText(); - if (textContent.empty()) { - continue; - } - - indent(depth + 2); - _output << "\n"; - - indent(depth + 3); - _output << "\n"; - - writeFillStyle(text, depth + 3, 0, 0, 0, 0); - writeStrokeStyle(text, depth + 3, 0, 0, 0, 0); - - indent(depth + 2); - _output << "\n"; - } else if (tag == SVGTag::TSpan) { - auto tspan = static_cast(child.get()); - const auto& tspanChildren = tspan->getTextChildren(); - - // Get tspan-specific position (if specified) - auto tspanX = tspan->getX(); - auto tspanY = tspan->getY(); - float spanX = tspanX.empty() ? baseX : std::stof(lengthToFloat(tspanX[0], _width)); - float spanY = tspanY.empty() ? baseY : std::stof(lengthToFloat(tspanY[0], _height)); - - // Get tspan-specific font properties (inherit from text if not specified) - auto tspanFontFamilyProp = tspan->getFontFamily(); - auto tspanFontFamilyOpt = GetPropertyValue(tspanFontFamilyProp); - std::string spanFontFamily = - tspanFontFamilyOpt.has_value() ? CleanFontFamily(tspanFontFamilyOpt->family()) : fontFamily; - - auto tspanFontSizeProp = tspan->getFontSize(); - auto tspanFontSizeOpt = GetPropertyValue(tspanFontSizeProp); - float spanFontSize = tspanFontSizeOpt.has_value() - ? std::stof(lengthToFloat(tspanFontSizeOpt->size(), _height)) - : fontSize; - - // Get tspan-specific text-anchor (inherit from text if not specified) - auto tspanTextAnchorProp = tspan->getTextAnchor(); - auto tspanTextAnchorOpt = GetPropertyValue(tspanTextAnchorProp); - std::string spanTextAnchor = textAnchor; - if (tspanTextAnchorOpt.has_value()) { - switch (tspanTextAnchorOpt->type()) { - case SVGTextAnchor::Type::Middle: - spanTextAnchor = "middle"; - break; - case SVGTextAnchor::Type::End: - spanTextAnchor = "end"; - break; - default: - spanTextAnchor = "start"; - break; - } - } - - for (const auto& tspanChild : tspanChildren) { - if (tspanChild->tag() != SVGTag::TextLiteral) { - continue; - } - auto literal = static_cast(tspanChild.get()); - const std::string& textContent = literal->getText(); - if (textContent.empty()) { - continue; - } - - indent(depth + 2); - _output << "\n"; - - indent(depth + 3); - _output << "\n"; - - // Use tspan's fill/stroke if specified, otherwise inherit from text - writeFillStyle(tspan, depth + 3, 0, 0, 0, 0); - writeStrokeStyle(tspan, depth + 3, 0, 0, 0, 0); - - indent(depth + 2); - _output << "\n"; - } - } - } - - indent(depth + 1); - _output << "\n"; - indent(depth); - _output << "\n"; -} - -void SVGToPAGXConverter::writeFillStyle(const SVGNode* node, int depth, float shapeX, float shapeY, - float shapeWidth, float shapeHeight) { - auto fillProp = node->getFill(); - auto fillOpt = GetPropertyValue(fillProp); - - // Only output Fill if fill attribute is explicitly set - if (!fillOpt.has_value()) { - return; - } - - auto fillValue = fillOpt.value(); - if (fillValue.type() == SVGPaint::Type::None) { - return; - } - - auto fillRuleProp = node->getFillRule(); - auto fillRuleOpt = GetPropertyValue(fillRuleProp); - bool hasEvenOddFillRule = - fillRuleOpt.has_value() && fillRuleOpt.value().type() == SVGFillRule::Type::EvenOdd; - - if (fillValue.type() == SVGPaint::Type::IRI) { - auto iri = fillValue.iri().iri(); - if (iri.empty()) { - return; - } - - // Check if this color source should be inlined - auto usageIt = _colorSourceUsages.find(iri); - bool shouldInline = - (usageIt == _colorSourceUsages.end()) || (usageIt->second.count == 1) || - (_patterns.find(iri) != _patterns.end() && _patterns.at(iri).isObjectBoundingBox); - - if (shouldInline) { - // Check if it's a gradient - auto gradientIt = _gradients.find(iri); - if (gradientIt != _gradients.end()) { - indent(depth); - _output << "\n"; - writeInlineGradient(iri, depth + 1); - indent(depth); - _output << "
\n"; - return; - } - - // Check if it's a pattern - auto patternIt = _patterns.find(iri); - if (patternIt != _patterns.end()) { - indent(depth); - _output << "\n"; - writeInlineImagePattern(patternIt->second, depth + 1, shapeX, shapeY, shapeWidth, - shapeHeight); - indent(depth); - _output << "
\n"; - return; - } - } - - // Reference to shared resource - indent(depth); - _output << "\n"; - return; - } - - // Solid color - Color fillColor = Color::Black(); - if (fillValue.type() == SVGPaint::Type::Color) { - fillColor = fillValue.color().color(); - auto fillOpacityProp = node->getFillOpacity(); - auto fillOpacityOpt = GetPropertyValue(fillOpacityProp); - if (fillOpacityOpt.has_value()) { - fillColor.alpha = fillOpacityOpt.value(); - } - } - - indent(depth); - _output << "\n"; -} - -void SVGToPAGXConverter::writeStrokeStyle(const SVGNode* node, int depth, float shapeX, - float shapeY, float shapeWidth, float shapeHeight) { - auto strokeProp = node->getStroke(); - auto strokeOpt = GetPropertyValue(strokeProp); - if (!strokeOpt.has_value()) { - return; - } - - auto strokeValue = strokeOpt.value(); - if (strokeValue.type() == SVGPaint::Type::None) { - return; - } - - // Collect stroke attributes - auto strokeWidthProp = node->getStrokeWidth(); - auto strokeWidthOpt = GetPropertyValue(strokeWidthProp); - float width = 1.0f; - if (strokeWidthOpt.has_value()) { - width = std::stof(lengthToFloat(strokeWidthOpt.value(), _width)); - } - - std::string capAttr = {}; - auto lineCapProp = node->getStrokeLineCap(); - auto lineCapOpt = GetPropertyValue(lineCapProp); - if (lineCapOpt.has_value()) { - auto cap = lineCapOpt.value(); - if (cap == SVGLineCap::Round) { - capAttr = " cap=\"round\""; - } else if (cap == SVGLineCap::Square) { - capAttr = " cap=\"square\""; - } - } - - std::string joinAttr = {}; - auto lineJoinProp = node->getStrokeLineJoin(); - auto lineJoinOpt = GetPropertyValue(lineJoinProp); - if (lineJoinOpt.has_value()) { - auto joinType = lineJoinOpt.value().type(); - if (joinType == SVGLineJoin::Type::Round) { - joinAttr = " join=\"round\""; - } else if (joinType == SVGLineJoin::Type::Bevel) { - joinAttr = " join=\"bevel\""; - } - } - - std::string miterAttr = {}; - auto miterLimitProp = node->getStrokeMiterLimit(); - auto miterLimitOpt = GetPropertyValue(miterLimitProp); - if (miterLimitOpt.has_value() && miterLimitOpt.value() != 4.0f) { - std::ostringstream ss; - ss << " miterLimit=\"" << miterLimitOpt.value() << "\""; - miterAttr = ss.str(); - } - - std::string dashesAttr = {}; - auto dashArrayProp = node->getStrokeDashArray(); - auto dashArrayOpt = GetPropertyValue(dashArrayProp); - if (dashArrayOpt.has_value()) { - auto& dashes = dashArrayOpt.value().dashArray(); - if (!dashes.empty()) { - std::ostringstream ss; - ss << " dashes=\""; - for (size_t i = 0; i < dashes.size(); ++i) { - if (i > 0) { - ss << ","; - } - ss << lengthToFloat(dashes[i], _width); - } - ss << "\""; - dashesAttr = ss.str(); - } - } - - std::string dashOffsetAttr = {}; - auto dashOffsetProp = node->getStrokeDashOffset(); - auto dashOffsetOpt = GetPropertyValue(dashOffsetProp); - if (dashOffsetOpt.has_value()) { - float offset = std::stof(lengthToFloat(dashOffsetOpt.value(), _width)); - if (offset != 0.0f) { - std::ostringstream ss; - ss << " dashOffset=\"" << offset << "\""; - dashOffsetAttr = ss.str(); - } - } - - // Handle IRI reference (gradient or pattern) - if (strokeValue.type() == SVGPaint::Type::IRI) { - auto iri = strokeValue.iri().iri(); - if (iri.empty()) { - return; - } - - // Check if this color source should be inlined - auto usageIt = _colorSourceUsages.find(iri); - bool shouldInline = - (usageIt == _colorSourceUsages.end()) || (usageIt->second.count == 1) || - (_patterns.find(iri) != _patterns.end() && _patterns.at(iri).isObjectBoundingBox); - - if (shouldInline) { - // Check if it's a gradient - auto gradientIt = _gradients.find(iri); - if (gradientIt != _gradients.end()) { - indent(depth); - _output << "\n"; - writeInlineGradient(iri, depth + 1); - indent(depth); - _output << "\n"; - return; - } - - // Check if it's a pattern - auto patternIt = _patterns.find(iri); - if (patternIt != _patterns.end()) { - indent(depth); - _output << "\n"; - writeInlineImagePattern(patternIt->second, depth + 1, shapeX, shapeY, shapeWidth, - shapeHeight); - indent(depth); - _output << "\n"; - return; - } - } - - // Reference to shared resource - indent(depth); - _output << "\n"; - return; - } - - // Solid color - indent(depth); - _output << "getStrokeOpacity(); - auto strokeOpacityOpt = GetPropertyValue(strokeOpacityProp); - if (strokeOpacityOpt.has_value()) { - color.alpha = strokeOpacityOpt.value(); - } - _output << " color=\"" << colorToHex(color) << "\""; - } - _output << " width=\"" << width << "\"" << capAttr << joinAttr << miterAttr << dashesAttr - << dashOffsetAttr << "/>\n"; -} - -void SVGToPAGXConverter::writeInlineGradient(const std::string& gradientId, int depth) { - auto it = _gradients.find(gradientId); - if (it == _gradients.end()) { - return; - } - - auto node = it->second; - auto tag = node->tag(); - - if (tag == SVGTag::LinearGradient) { - auto gradient = static_cast(node); - indent(depth); - _output << "getX1(), _width) << "\""; - _output << " startY=\"" << lengthToFloat(gradient->getY1(), _height) << "\""; - _output << " endX=\"" << lengthToFloat(gradient->getX2(), _width) << "\""; - _output << " endY=\"" << lengthToFloat(gradient->getY2(), _height) << "\""; - _output << ">\n"; - - auto container = static_cast(node); - for (const auto& child : container->getChildren()) { - if (child->tag() == SVGTag::Stop) { - auto stop = static_cast(child.get()); - indent(depth + 1); - auto offset = stop->getOffset(); - float offsetValue = offset.value(); - if (offset.unit() == SVGLength::Unit::Percentage) { - offsetValue = offsetValue / 100.0f; - } - _output << "getStopColor(); - auto stopColorOpt = GetPropertyValue(stopColorProp); - if (stopColorOpt.has_value()) { - auto color = stopColorOpt->color(); - auto opacityProp = stop->getStopOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - float alpha = opacityOpt.has_value() ? opacityOpt.value() : 1.0f; - Color c = color; - c.alpha = alpha; - _output << " color=\"" << colorToHex(c) << "\""; - } - _output << "/>\n"; - } - } - - indent(depth); - _output << "\n"; - } else if (tag == SVGTag::RadialGradient) { - auto gradient = static_cast(node); - indent(depth); - _output << "getCx(), _width) << "\""; - _output << " centerY=\"" << lengthToFloat(gradient->getCy(), _height) << "\""; - auto r = gradient->getR(); - _output << " radius=\"" << lengthToFloat(r, std::max(_width, _height)) << "\""; - _output << ">\n"; - - auto container = static_cast(node); - for (const auto& child : container->getChildren()) { - if (child->tag() == SVGTag::Stop) { - auto stop = static_cast(child.get()); - indent(depth + 1); - auto offset = stop->getOffset(); - float offsetValue = offset.value(); - if (offset.unit() == SVGLength::Unit::Percentage) { - offsetValue = offsetValue / 100.0f; - } - _output << "getStopColor(); - auto stopColorOpt = GetPropertyValue(stopColorProp); - if (stopColorOpt.has_value()) { - auto color = stopColorOpt->color(); - auto opacityProp = stop->getStopOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - float alpha = opacityOpt.has_value() ? opacityOpt.value() : 1.0f; - Color c = color; - c.alpha = alpha; - _output << " color=\"" << colorToHex(c) << "\""; - } - _output << "/>\n"; - } - } - - indent(depth); - _output << "\n"; - } -} - -void SVGToPAGXConverter::writeInlineImagePattern(const PatternInfo& patternInfo, int depth, - float shapeX, float shapeY, float shapeWidth, - float shapeHeight) { - indent(depth); - _output << " 0 && - patternInfo.patternHeight > 0 && shapeWidth > 0 && shapeHeight > 0) { - float tileWidth = shapeWidth * patternInfo.patternWidth; - float tileHeight = shapeHeight * patternInfo.patternHeight; - - if (patternInfo.imageWidth > 0 && patternInfo.imageHeight > 0 && tileWidth > 0 && - tileHeight > 0) { - float scaleX = tileWidth / patternInfo.imageWidth; - float scaleY = tileHeight / patternInfo.imageHeight; - // Translate to align pattern with shape's top-left corner - float tx = shapeX; - float ty = shapeY; - _output << " matrix=\"" << scaleX << ",0,0," << scaleY << "," << tx << "," << ty << "\""; - } - } - - _output << "/>\n"; -} - -std::string SVGToPAGXConverter::colorToHex(const Color& color) const { - auto toHex = [](float value) -> int { return static_cast(std::round(value * 255)); }; - - std::ostringstream ss; - ss << "#" << std::uppercase << std::hex << std::setfill('0'); - ss << std::setw(2) << toHex(color.red); - ss << std::setw(2) << toHex(color.green); - ss << std::setw(2) << toHex(color.blue); - - if (color.alpha < 1.0f) { - ss << std::setw(2) << toHex(color.alpha); - } - - return ss.str(); -} - -std::string SVGToPAGXConverter::matrixToString(const Matrix& matrix) const { - std::ostringstream ss; - ss << matrix.getScaleX() << "," << matrix.getSkewY() << "," << matrix.getSkewX() << "," - << matrix.getScaleY() << "," << matrix.getTranslateX() << "," << matrix.getTranslateY(); - return ss.str(); -} - -std::string SVGToPAGXConverter::lengthToFloat(const SVGLength& length, float containerSize) const { - float value = length.value(); - auto unit = length.unit(); - - switch (unit) { - case SVGLength::Unit::Percentage: - value = value * containerSize / 100.0f; - break; - case SVGLength::Unit::PX: - case SVGLength::Unit::Number: - case SVGLength::Unit::Unknown: - default: - break; - } - - std::ostringstream ss; - ss << value; - return ss.str(); -} - -void SVGToPAGXConverter::indent(int depth) { - for (int i = 0; i < depth; ++i) { - _output << " "; - } -} - -int SVGToPAGXConverter::countRenderableChildren(const SVGContainer* container) const { - int count = 0; - for (const auto& child : container->getChildren()) { - auto tag = child->tag(); - if (tag == SVGTag::Defs || tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient || - tag == SVGTag::Stop || tag == SVGTag::ClipPath || tag == SVGTag::Mask || - tag == SVGTag::Filter || tag == SVGTag::G || tag == SVGTag::Svg) { - continue; - } - auto visibilityProp = child->getVisibility(); - auto visibilityOpt = GetPropertyValue(visibilityProp); - if (visibilityOpt.has_value() && visibilityOpt.value().type() == SVGVisibility::Type::Hidden) { - continue; - } - count++; - } - return count; -} - -bool SVGToPAGXConverter::hasOnlyFill(const SVGNode* node) const { - auto fillProp = node->getFill(); - auto fillOpt = GetPropertyValue(fillProp); - bool hasFill = fillOpt.has_value() && fillOpt->type() != SVGPaint::Type::None; - - auto strokeProp = node->getStroke(); - auto strokeOpt = GetPropertyValue(strokeProp); - bool hasStroke = strokeOpt.has_value() && strokeOpt->type() != SVGPaint::Type::None; - - return hasFill && !hasStroke; -} - -bool SVGToPAGXConverter::hasOnlyStroke(const SVGNode* node) const { - auto fillProp = node->getFill(); - auto fillOpt = GetPropertyValue(fillProp); - bool hasFill = fillOpt.has_value() && fillOpt->type() != SVGPaint::Type::None; - - auto strokeProp = node->getStroke(); - auto strokeOpt = GetPropertyValue(strokeProp); - bool hasStroke = strokeOpt.has_value() && strokeOpt->type() != SVGPaint::Type::None; - - return !hasFill && hasStroke; -} - -bool SVGToPAGXConverter::areRectsEqual(const SVGRect* a, const SVGRect* b) const { - if (!a || !b) { - return false; - } - - auto ax = lengthToFloat(a->getX(), _width); - auto ay = lengthToFloat(a->getY(), _height); - auto aw = lengthToFloat(a->getWidth(), _width); - auto ah = lengthToFloat(a->getHeight(), _height); - - auto bx = lengthToFloat(b->getX(), _width); - auto by = lengthToFloat(b->getY(), _height); - auto bw = lengthToFloat(b->getWidth(), _width); - auto bh = lengthToFloat(b->getHeight(), _height); - - if (ax != bx || ay != by || aw != bw || ah != bh) { - return false; - } - - float arx = 0, ary = 0, brx = 0, bry = 0; - if (a->getRx().has_value()) { - arx = std::stof(lengthToFloat(*a->getRx(), _width)); - } - if (a->getRy().has_value()) { - ary = std::stof(lengthToFloat(*a->getRy(), _height)); - } - if (b->getRx().has_value()) { - brx = std::stof(lengthToFloat(*b->getRx(), _width)); - } - if (b->getRy().has_value()) { - bry = std::stof(lengthToFloat(*b->getRy(), _height)); - } - - return arx == brx && ary == bry; -} - -void SVGToPAGXConverter::convertChildren(const std::vector>& children, - int depth) { - size_t i = 0; - while (i < children.size()) { - auto& child = children[i]; - auto tag = child->tag(); - - // Skip non-renderable elements - if (tag == SVGTag::Defs || tag == SVGTag::LinearGradient || tag == SVGTag::RadialGradient || - tag == SVGTag::Stop || tag == SVGTag::ClipPath || tag == SVGTag::Mask || - tag == SVGTag::Filter || tag == SVGTag::Pattern) { - i++; - continue; - } - - auto visibilityProp = child->getVisibility(); - auto visibilityOpt = GetPropertyValue(visibilityProp); - if (visibilityOpt.has_value() && visibilityOpt.value().type() == SVGVisibility::Type::Hidden) { - i++; - continue; - } - - // Check for consecutive identical rects (fill-only followed by stroke-only) - if (tag == SVGTag::Rect && hasOnlyFill(child.get()) && i + 1 < children.size()) { - auto& next = children[i + 1]; - if (next->tag() == SVGTag::Rect && hasOnlyStroke(next.get())) { - auto fillRect = static_cast(child.get()); - auto strokeRect = static_cast(next.get()); - if (areRectsEqual(fillRect, strokeRect)) { - convertRectWithStroke(fillRect, strokeRect, depth); - i += 2; - continue; - } - } - } - - // Normal processing - if (tag == SVGTag::G || tag == SVGTag::Svg) { - convertContainer(static_cast(child.get()), depth); - } else { - convertNode(child.get(), depth, false); - } - i++; - } -} - -void SVGToPAGXConverter::convertRectWithStroke(const SVGRect* fillRect, const SVGRect* strokeRect, - int depth) { - float x = std::stof(lengthToFloat(fillRect->getX(), _width)); - float y = std::stof(lengthToFloat(fillRect->getY(), _height)); - float width = std::stof(lengthToFloat(fillRect->getWidth(), _width)); - float height = std::stof(lengthToFloat(fillRect->getHeight(), _height)); - - if (width <= 0 || height <= 0) { - return; - } - - float centerX = x + width / 2.0f; - float centerY = y + height / 2.0f; - - float rx = 0; - float ry = 0; - auto rxOpt = fillRect->getRx(); - auto ryOpt = fillRect->getRy(); - if (rxOpt.has_value()) { - rx = std::stof(lengthToFloat(*rxOpt, _width)); - } - if (ryOpt.has_value()) { - ry = std::stof(lengthToFloat(*ryOpt, _height)); - } - if (rx > 0 && ry <= 0) { - ry = rx; - } - if (ry > 0 && rx <= 0) { - rx = ry; - } - - auto transformable = static_cast(fillRect); - auto transform = transformable->getTransform(); - bool hasTransform = !transform.isIdentity(); - - auto opacityProp = fillRect->getOpacity(); - auto opacityOpt = GetPropertyValue(opacityProp); - bool hasOpacity = opacityOpt.has_value() && opacityOpt.value() < 1.0f; - - indent(depth); - _output << "\n"; - indent(depth + 1); - _output << "\n"; - - int contentDepth = depth + 2; - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - contentDepth = depth + 3; - } - - indent(contentDepth); - _output << " 0 || ry > 0) { - float roundness = std::min(rx, ry); - _output << " roundness=\"" << roundness << "\""; - } - _output << "/>\n"; - - writeFillStyle(fillRect, contentDepth, x, y, width, height); - writeStrokeStyle(strokeRect, contentDepth, x, y, width, height); - - if (hasTransform || hasOpacity) { - indent(depth + 2); - _output << "\n"; - } - - indent(depth + 1); - _output << "\n"; - indent(depth); - _output << "\n"; -} - -std::string SVGToPAGXConverter::getMaskId(const SVGNode* node) const { - if (!node) { - return ""; - } - - auto maskProp = node->getMask(); - auto maskOpt = GetPropertyValue(maskProp); - if (!maskOpt.has_value() || maskOpt->type() != SVGFuncIRI::Type::IRI) { - return ""; - } - - auto maskIri = maskOpt->iri().iri(); - if (maskIri.empty()) { - return ""; - } - - // Check if this mask exists in our collected masks - if (_masks.find(maskIri) == _masks.end()) { - return ""; - } - - return maskIri; -} - -void SVGToPAGXConverter::writeMaskLayers(int depth) { - // Write mask layers that are referenced by content layers - for (const auto& [maskId, maskInfo] : _masks) { - // Only output masks that will be used - if (_usedMasks.find(maskId) == _usedMasks.end()) { - continue; - } - - auto maskNode = static_cast(maskInfo.maskNode); - auto container = static_cast(maskNode); - if (!container->hasChildren()) { - continue; - } - - indent(depth); - _output << "\n"; - indent(depth + 1); - _output << "\n"; - - // Convert mask children - we need to output shapes directly without wrapping in Layer - for (const auto& child : container->getChildren()) { - auto tag = child->tag(); - if (tag == SVGTag::Rect) { - auto rect = static_cast(child.get()); - float x = std::stof(lengthToFloat(rect->getX(), _width)); - float y = std::stof(lengthToFloat(rect->getY(), _height)); - float width = std::stof(lengthToFloat(rect->getWidth(), _width)); - float height = std::stof(lengthToFloat(rect->getHeight(), _height)); - float centerX = x + width / 2.0f; - float centerY = y + height / 2.0f; - - indent(depth + 2); - _output << "\n"; - writeFillStyle(rect, depth + 2, x, y, width, height); - } else if (tag == SVGTag::Circle) { - auto circle = static_cast(child.get()); - float cx = std::stof(lengthToFloat(circle->getCx(), _width)); - float cy = std::stof(lengthToFloat(circle->getCy(), _height)); - float r = std::stof(lengthToFloat(circle->getR(), std::max(_width, _height))); - - indent(depth + 2); - _output << "\n"; - writeFillStyle(circle, depth + 2, cx - r, cy - r, r * 2, r * 2); - } else if (tag == SVGTag::Ellipse) { - auto ellipse = static_cast(child.get()); - float cx = std::stof(lengthToFloat(ellipse->getCx(), _width)); - float cy = std::stof(lengthToFloat(ellipse->getCy(), _height)); - auto rxOpt = ellipse->getRx(); - auto ryOpt = ellipse->getRy(); - float rx = rxOpt.has_value() ? std::stof(lengthToFloat(*rxOpt, _width)) : 0; - float ry = ryOpt.has_value() ? std::stof(lengthToFloat(*ryOpt, _height)) : 0; - - indent(depth + 2); - _output << "\n"; - writeFillStyle(ellipse, depth + 2, cx - rx, cy - ry, rx * 2, ry * 2); - } else if (tag == SVGTag::Path) { - auto path = static_cast(child.get()); - auto shapePath = path->getShapePath(); - if (!shapePath.isEmpty()) { - indent(depth + 2); - _output << "\n"; - auto bounds = shapePath.getBounds(); - writeFillStyle(path, depth + 2, bounds.left, bounds.top, bounds.width(), bounds.height()); - } - } - } - - indent(depth + 1); - _output << "\n"; - indent(depth); - _output << "\n"; - } -} - -} // namespace pagx diff --git a/pagx/src/svg/SVGToPAGXConverter.h b/pagx/src/svg/SVGToPAGXConverter.h deleted file mode 100644 index 7fd657b541..0000000000 --- a/pagx/src/svg/SVGToPAGXConverter.h +++ /dev/null @@ -1,137 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include -#include -#include -#include "tgfx/core/Color.h" -#include "tgfx/core/Matrix.h" -#include "tgfx/svg/SVGDOM.h" -#include "tgfx/svg/node/SVGCircle.h" -#include "tgfx/svg/node/SVGContainer.h" -#include "tgfx/svg/node/SVGEllipse.h" -#include "tgfx/svg/node/SVGGradient.h" -#include "tgfx/svg/node/SVGLine.h" -#include "tgfx/svg/node/SVGMask.h" -#include "tgfx/svg/node/SVGNode.h" -#include "tgfx/svg/node/SVGPath.h" -#include "tgfx/svg/node/SVGPoly.h" -#include "tgfx/svg/node/SVGRect.h" -#include "tgfx/svg/node/SVGRoot.h" -#include "tgfx/svg/node/SVGText.h" - -namespace pagx { - -using namespace tgfx; - -struct PatternInfo { - std::string patternId = {}; - std::string imageId = {}; - std::string imageData = {}; - float imageWidth = 0.0f; - float imageHeight = 0.0f; - float patternWidth = 0.0f; // in objectBoundingBox units (0-1) - float patternHeight = 0.0f; // in objectBoundingBox units (0-1) - bool isObjectBoundingBox = true; -}; - -struct ColorSourceUsage { - int count = 0; - std::string type = {}; // "gradient" or "pattern" -}; - -struct MaskInfo { - std::string maskId = {}; - const SVGNode* maskNode = nullptr; - bool isLuminance = false; -}; - -class SVGToPAGXConverter { - public: - explicit SVGToPAGXConverter(const std::shared_ptr& svgDOM); - - std::string convert(); - - private: - void writeHeader(); - void writeResources(); - void writeLayers(); - - void convertNode(const SVGNode* node, int depth, bool needScopeIsolation); - void convertContainer(const SVGContainer* container, int depth); - void convertRect(const SVGRect* rect, int depth); - void convertCircle(const SVGCircle* circle, int depth); - void convertEllipse(const SVGEllipse* ellipse, int depth); - void convertPath(const SVGPath* path, int depth); - void convertLine(const SVGLine* line, int depth); - void convertPoly(const SVGPoly* poly, int depth); - void convertText(const SVGText* text, int depth); - - void writeFillStyle(const SVGNode* node, int depth, float shapeX = 0, float shapeY = 0, - float shapeWidth = 0, float shapeHeight = 0); - void writeStrokeStyle(const SVGNode* node, int depth, float shapeX = 0, float shapeY = 0, - float shapeWidth = 0, float shapeHeight = 0); - - void writeInlineGradient(const std::string& gradientId, int depth); - void writeInlineImagePattern(const PatternInfo& patternInfo, int depth, float shapeX, - float shapeY, float shapeWidth, float shapeHeight); - - std::string colorToHex(const Color& color) const; - std::string colorToString(const SVGPaint& paint) const; - std::string matrixToString(const Matrix& matrix) const; - std::string lengthToFloat(const SVGLength& length, float containerSize) const; - - void indent(int depth); - void writeAttribute(const std::string& name, const std::string& value); - void writeAttribute(const std::string& name, float value); - - void collectGradients(); - void collectPatterns(); - void collectMasks(); - void collectUsedMasks(const SVGNode* node); - void countColorSourceUsages(); - void countColorSourceUsagesFromNode(const SVGNode* node); - int countRenderableChildren(const SVGContainer* container) const; - - bool hasOnlyFill(const SVGNode* node) const; - bool hasOnlyStroke(const SVGNode* node) const; - bool areRectsEqual(const SVGRect* a, const SVGRect* b) const; - void convertChildren(const std::vector>& children, int depth); - void convertRectWithStroke(const SVGRect* fillRect, const SVGRect* strokeRect, int depth); - - std::string getMaskId(const SVGNode* node) const; - void writeMaskLayers(int depth); - - std::shared_ptr _svgDOM = nullptr; - std::ostringstream _output = {}; - float _width = 0.0f; - float _height = 0.0f; - std::map _gradients = {}; - std::map _patterns = {}; - std::map _images = {}; - std::map _colorSourceUsages = {}; - std::map _masks = {}; - std::set _usedMasks = {}; -}; - -} // namespace pagx diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp new file mode 100644 index 0000000000..5d170eeb0a --- /dev/null +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -0,0 +1,669 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/LayerBuilder.h" +#include "pagx/PAGXSVGParser.h" +#include "tgfx/core/Image.h" +#include "tgfx/core/Path.h" +#include "tgfx/layers/Layer.h" +#include "tgfx/layers/VectorLayer.h" +#include "tgfx/layers/filters/BlurFilter.h" +#include "tgfx/layers/filters/DropShadowFilter.h" +#include "tgfx/layers/layerstyles/DropShadowStyle.h" +#include "tgfx/layers/layerstyles/InnerShadowStyle.h" +#include "tgfx/layers/vectors/Ellipse.h" +#include "tgfx/layers/vectors/FillStyle.h" +#include "tgfx/layers/vectors/Gradient.h" +#include "tgfx/layers/vectors/VectorGroup.h" +#include "tgfx/layers/vectors/ImagePattern.h" +#include "tgfx/layers/vectors/MergePath.h" +#include "tgfx/layers/vectors/Polystar.h" +#include "tgfx/layers/vectors/Rectangle.h" +#include "tgfx/layers/vectors/Repeater.h" +#include "tgfx/layers/vectors/RoundCorner.h" +#include "tgfx/layers/vectors/ShapePath.h" +#include "tgfx/layers/vectors/SolidColor.h" +#include "tgfx/layers/vectors/StrokeStyle.h" +#include "tgfx/layers/vectors/TextSpan.h" +#include "tgfx/layers/vectors/TrimPath.h" + +namespace pagx { + +// Type converters from pagx to tgfx +static tgfx::Point ToTGFX(const Point& p) { + return tgfx::Point::Make(p.x, p.y); +} + +static tgfx::Color ToTGFX(const Color& c) { + return {c.red, c.green, c.blue, c.alpha}; +} + +static tgfx::Matrix ToTGFX(const Matrix& m) { + return tgfx::Matrix::MakeAll(m.scaleX, m.skewX, m.transX, m.skewY, m.scaleY, m.transY); +} + +static tgfx::Path ToTGFX(const PathData& pathData) { + tgfx::Path path; + pathData.forEach([&](PathVerb verb, const float* pts) { + switch (verb) { + case PathVerb::Move: + path.moveTo(pts[0], pts[1]); + break; + case PathVerb::Line: + path.lineTo(pts[0], pts[1]); + break; + case PathVerb::Quad: + path.quadTo(pts[0], pts[1], pts[2], pts[3]); + break; + case PathVerb::Cubic: + path.cubicTo(pts[0], pts[1], pts[2], pts[3], pts[4], pts[5]); + break; + case PathVerb::Close: + path.close(); + break; + } + }); + return path; +} + +// Internal builder class +class LayerBuilderImpl { + public: + LayerBuilderImpl(const LayerBuilder::Options& options) : _options(options) { + } + + PAGXContent build(const PAGXDocument& document) { + PAGXContent content; + content.width = document.width(); + content.height = document.height(); + + auto root = document.root(); + if (!root) { + return content; + } + + // Build resources map + auto resources = document.resources(); + if (resources) { + for (size_t i = 0; i < resources->childCount(); ++i) { + auto resource = resources->childAt(i); + if (!resource->id().empty()) { + _resourceNodes[resource->id()] = resource; + } + } + } + + // Build layer tree + auto layer = tgfx::Layer::Make(); + for (size_t i = 0; i < root->childCount(); ++i) { + auto childLayer = convertNode(root->childAt(i)); + if (childLayer) { + layer->addChild(childLayer); + } + } + + content.root = layer; + return content; + } + + private: + std::shared_ptr convertNode(const PAGXNode* node) { + if (!node) { + return nullptr; + } + + std::shared_ptr layer = nullptr; + + switch (node->type()) { + case PAGXNodeType::Layer: + case PAGXNodeType::Group: + layer = convertGroup(node); + break; + case PAGXNodeType::Rectangle: + case PAGXNodeType::Ellipse: + case PAGXNodeType::Polystar: + case PAGXNodeType::Path: + case PAGXNodeType::Text: + layer = convertVectorLayer(node); + break; + case PAGXNodeType::Image: + layer = convertImage(node); + break; + default: + break; + } + + if (layer) { + applyCommonAttributes(node, layer.get()); + } + + return layer; + } + + std::shared_ptr convertGroup(const PAGXNode* node) { + auto layer = tgfx::Layer::Make(); + for (size_t i = 0; i < node->childCount(); ++i) { + auto childLayer = convertNode(node->childAt(i)); + if (childLayer) { + layer->addChild(childLayer); + } + } + return layer; + } + + std::shared_ptr convertVectorLayer(const PAGXNode* node) { + auto layer = tgfx::VectorLayer::Make(); + + // Convert vector element + auto element = convertVectorElement(node); + if (element) { + layer->setContent(element); + } + + return layer; + } + + std::shared_ptr convertVectorElement(const PAGXNode* node) { + std::shared_ptr element = nullptr; + + switch (node->type()) { + case PAGXNodeType::Rectangle: + element = convertRectangle(node); + break; + case PAGXNodeType::Ellipse: + element = convertEllipse(node); + break; + case PAGXNodeType::Polystar: + element = convertPolystar(node); + break; + case PAGXNodeType::Path: + element = convertPath(node); + break; + case PAGXNodeType::Text: + element = convertText(node); + break; + case PAGXNodeType::Group: + element = convertVectorGroup(node); + break; + default: + break; + } + + if (element) { + // Add fill and stroke styles from children + for (size_t i = 0; i < node->childCount(); ++i) { + auto child = node->childAt(i); + if (child->type() == PAGXNodeType::Fill) { + auto fill = convertFill(child); + if (fill) { + element->addChild(fill); + } + } else if (child->type() == PAGXNodeType::Stroke) { + auto stroke = convertStroke(child); + if (stroke) { + element->addChild(stroke); + } + } else if (child->type() == PAGXNodeType::TrimPath) { + auto trim = convertTrimPath(child); + if (trim) { + element->addChild(trim); + } + } else if (child->type() == PAGXNodeType::RoundCorner) { + auto round = convertRoundCorner(child); + if (round) { + element->addChild(round); + } + } else if (child->type() == PAGXNodeType::MergePath) { + auto merge = convertMergePath(child); + if (merge) { + element->addChild(merge); + } + } else if (child->type() == PAGXNodeType::Repeater) { + auto repeater = convertRepeater(child); + if (repeater) { + element->addChild(repeater); + } + } + } + } + + return element; + } + + std::shared_ptr convertRectangle(const PAGXNode* node) { + float x = node->getFloatAttribute("x", 0); + float y = node->getFloatAttribute("y", 0); + float width = node->getFloatAttribute("width", 0); + float height = node->getFloatAttribute("height", 0); + float rx = node->getFloatAttribute("rx", 0); + float ry = node->getFloatAttribute("ry", 0); + + auto rect = tgfx::Rectangle::Make(); + rect->setX(x); + rect->setY(y); + rect->setWidth(width); + rect->setHeight(height); + rect->setRoundedCornerX(rx); + rect->setRoundedCornerY(ry); + return rect; + } + + std::shared_ptr convertEllipse(const PAGXNode* node) { + float cx = node->getFloatAttribute("cx", 0); + float cy = node->getFloatAttribute("cy", 0); + float rx = node->getFloatAttribute("rx", 0); + float ry = node->getFloatAttribute("ry", 0); + + auto ellipse = tgfx::Ellipse::Make(); + ellipse->setCenterX(cx); + ellipse->setCenterY(cy); + ellipse->setRadiusX(rx); + ellipse->setRadiusY(ry); + return ellipse; + } + + std::shared_ptr convertPolystar(const PAGXNode* node) { + auto polystar = tgfx::Polystar::Make(); + polystar->setCenterX(node->getFloatAttribute("centerX", 0)); + polystar->setCenterY(node->getFloatAttribute("centerY", 0)); + polystar->setPoints(node->getFloatAttribute("points", 5)); + polystar->setInnerRadius(node->getFloatAttribute("innerRadius", 0)); + polystar->setOuterRadius(node->getFloatAttribute("outerRadius", 0)); + polystar->setInnerRoundness(node->getFloatAttribute("innerRoundness", 0)); + polystar->setOuterRoundness(node->getFloatAttribute("outerRoundness", 0)); + polystar->setRotation(node->getFloatAttribute("rotation", 0)); + + std::string type = node->getAttribute("type", "Star"); + if (type == "Polygon") { + polystar->setType(tgfx::PolystarType::Polygon); + } else { + polystar->setType(tgfx::PolystarType::Star); + } + + return polystar; + } + + std::shared_ptr convertPath(const PAGXNode* node) { + auto pathData = node->getPathAttribute("d"); + auto tgfxPath = ToTGFX(pathData); + auto shapePath = tgfx::ShapePath::Make(); + shapePath->setPath(tgfxPath); + return shapePath; + } + + std::shared_ptr convertText(const PAGXNode* node) { + std::string text = node->getAttribute("text"); + float fontSize = node->getFloatAttribute("fontSize", 16); + std::string fontFamily = node->getAttribute("fontFamily"); + + auto textSpan = tgfx::TextSpan::Make(); + textSpan->setText(text); + + // Create font from typeface + std::shared_ptr typeface = nullptr; + if (!fontFamily.empty() && !_options.fallbackTypefaces.empty()) { + for (auto& tf : _options.fallbackTypefaces) { + if (tf && tf->fontFamily() == fontFamily) { + typeface = tf; + break; + } + } + } + if (!typeface && !_options.fallbackTypefaces.empty()) { + typeface = _options.fallbackTypefaces[0]; + } + + if (typeface) { + textSpan->setTypeface(typeface); + } + textSpan->setFontSize(fontSize); + + float x = node->getFloatAttribute("x", 0); + float y = node->getFloatAttribute("y", 0); + textSpan->setX(x); + textSpan->setY(y); + + return textSpan; + } + + std::shared_ptr convertVectorGroup(const PAGXNode* node) { + auto group = tgfx::VectorGroup::Make(); + for (size_t i = 0; i < node->childCount(); ++i) { + auto child = node->childAt(i); + if (child->type() != PAGXNodeType::Fill && child->type() != PAGXNodeType::Stroke) { + auto childElement = convertVectorElement(child); + if (childElement) { + group->addChild(childElement); + } + } + } + return group; + } + + std::shared_ptr convertFill(const PAGXNode* node) { + auto fill = tgfx::FillStyle::Make(); + + // Try to get color source from reference + std::string colorSourceRef = node->getAttribute("colorSourceRef"); + if (!colorSourceRef.empty()) { + auto colorSource = convertColorSourceFromRef(colorSourceRef); + if (colorSource) { + fill->setColorSource(colorSource); + } + } else { + // Use solid color + auto color = node->getColorAttribute("color", Color::Black()); + auto solidColor = tgfx::SolidColor::Make(ToTGFX(color)); + fill->setColorSource(solidColor); + } + + return fill; + } + + std::shared_ptr convertStroke(const PAGXNode* node) { + auto stroke = tgfx::StrokeStyle::Make(); + + // Color source + std::string colorSourceRef = node->getAttribute("colorSourceRef"); + if (!colorSourceRef.empty()) { + auto colorSource = convertColorSourceFromRef(colorSourceRef); + if (colorSource) { + stroke->setColorSource(colorSource); + } + } else { + auto color = node->getColorAttribute("color", Color::Black()); + auto solidColor = tgfx::SolidColor::Make(ToTGFX(color)); + stroke->setColorSource(solidColor); + } + + // Stroke properties + float width = node->getFloatAttribute("width", 1); + stroke->setStrokeWidth(width); + + std::string lineCap = node->getAttribute("lineCap", "butt"); + if (lineCap == "round") { + stroke->setLineCap(tgfx::LineCap::Round); + } else if (lineCap == "square") { + stroke->setLineCap(tgfx::LineCap::Square); + } else { + stroke->setLineCap(tgfx::LineCap::Butt); + } + + std::string lineJoin = node->getAttribute("lineJoin", "miter"); + if (lineJoin == "round") { + stroke->setLineJoin(tgfx::LineJoin::Round); + } else if (lineJoin == "bevel") { + stroke->setLineJoin(tgfx::LineJoin::Bevel); + } else { + stroke->setLineJoin(tgfx::LineJoin::Miter); + } + + float miterLimit = node->getFloatAttribute("miterLimit", 4); + stroke->setMiterLimit(miterLimit); + + return stroke; + } + + std::shared_ptr convertColorSourceFromRef(const std::string& refId) { + auto it = _resourceNodes.find(refId); + if (it == _resourceNodes.end()) { + return nullptr; + } + return convertColorSource(it->second); + } + + std::shared_ptr convertColorSource(const PAGXNode* node) { + if (!node) { + return nullptr; + } + + switch (node->type()) { + case PAGXNodeType::SolidColor: { + auto color = node->getColorAttribute("color", Color::Black()); + return tgfx::SolidColor::Make(ToTGFX(color)); + } + case PAGXNodeType::LinearGradient: { + return convertLinearGradient(node); + } + case PAGXNodeType::RadialGradient: { + return convertRadialGradient(node); + } + default: + return nullptr; + } + } + + std::shared_ptr convertLinearGradient(const PAGXNode* node) { + float x1 = node->getFloatAttribute("x1", 0); + float y1 = node->getFloatAttribute("y1", 0); + float x2 = node->getFloatAttribute("x2", 1); + float y2 = node->getFloatAttribute("y2", 0); + + int stopCount = node->getIntAttribute("stopCount", 0); + std::vector colors; + std::vector positions; + + for (int i = 0; i < stopCount; ++i) { + std::string prefix = "stop" + std::to_string(i) + "_"; + float offset = node->getFloatAttribute(prefix + "offset", 0); + auto color = node->getColorAttribute(prefix + "color", Color::Black()); + colors.push_back(ToTGFX(color)); + positions.push_back(offset); + } + + if (colors.empty()) { + colors = {tgfx::Color::Black(), tgfx::Color::White()}; + positions = {0.0f, 1.0f}; + } + + auto startPoint = tgfx::Point::Make(x1, y1); + auto endPoint = tgfx::Point::Make(x2, y2); + + return tgfx::Gradient::MakeLinear(startPoint, endPoint, colors, positions); + } + + std::shared_ptr convertRadialGradient(const PAGXNode* node) { + float cx = node->getFloatAttribute("cx", 0.5f); + float cy = node->getFloatAttribute("cy", 0.5f); + float r = node->getFloatAttribute("r", 0.5f); + + int stopCount = node->getIntAttribute("stopCount", 0); + std::vector colors; + std::vector positions; + + for (int i = 0; i < stopCount; ++i) { + std::string prefix = "stop" + std::to_string(i) + "_"; + float offset = node->getFloatAttribute(prefix + "offset", 0); + auto color = node->getColorAttribute(prefix + "color", Color::Black()); + colors.push_back(ToTGFX(color)); + positions.push_back(offset); + } + + if (colors.empty()) { + colors = {tgfx::Color::Black(), tgfx::Color::White()}; + positions = {0.0f, 1.0f}; + } + + auto center = tgfx::Point::Make(cx, cy); + return tgfx::Gradient::MakeRadial(center, r, colors, positions); + } + + std::shared_ptr convertTrimPath(const PAGXNode* node) { + auto trim = tgfx::TrimPath::Make(); + trim->setStart(node->getFloatAttribute("start", 0)); + trim->setEnd(node->getFloatAttribute("end", 1)); + trim->setOffset(node->getFloatAttribute("offset", 0)); + return trim; + } + + std::shared_ptr convertRoundCorner(const PAGXNode* node) { + auto round = tgfx::RoundCorner::Make(); + round->setRadius(node->getFloatAttribute("radius", 0)); + return round; + } + + std::shared_ptr convertMergePath(const PAGXNode* node) { + auto merge = tgfx::MergePath::Make(); + std::string mode = node->getAttribute("mode", "Merge"); + // Set merge mode based on string + return merge; + } + + std::shared_ptr convertRepeater(const PAGXNode* node) { + auto repeater = tgfx::Repeater::Make(); + repeater->setCopies(node->getFloatAttribute("copies", 1)); + repeater->setOffset(node->getFloatAttribute("offset", 0)); + return repeater; + } + + std::shared_ptr convertImage(const PAGXNode* node) { + std::string href = node->getAttribute("href"); + if (href.empty()) { + return nullptr; + } + + std::string imagePath = href; + if (!_options.basePath.empty() && href[0] != '/' && href.find("://") == std::string::npos) { + imagePath = _options.basePath + href; + } + + auto image = tgfx::Image::MakeFromFile(imagePath); + if (!image) { + return nullptr; + } + + auto layer = tgfx::Layer::Make(); + // Note: Image rendering would typically use ImageLayer or similar + // For now, we just create a placeholder layer + + return layer; + } + + void applyCommonAttributes(const PAGXNode* node, tgfx::Layer* layer) { + // Transform + if (node->hasAttribute("transform")) { + auto matrix = node->getMatrixAttribute("transform"); + layer->setMatrix(ToTGFX(matrix)); + } + + // Opacity + if (node->hasAttribute("opacity")) { + layer->setAlpha(node->getFloatAttribute("opacity", 1.0f)); + } + + // Visibility + if (node->hasAttribute("visible")) { + layer->setVisible(node->getBoolAttribute("visible", true)); + } + + // Filters + for (size_t i = 0; i < node->childCount(); ++i) { + auto child = node->childAt(i); + if (child->type() == PAGXNodeType::BlurFilter) { + float radius = child->getFloatAttribute("blurRadius", 0); + auto filter = tgfx::BlurFilter::Make(radius, radius); + layer->setFilter(filter); + } else if (child->type() == PAGXNodeType::DropShadowFilter) { + float dx = child->getFloatAttribute("dx", 0); + float dy = child->getFloatAttribute("dy", 0); + float blur = child->getFloatAttribute("blurRadius", 0); + auto color = child->getColorAttribute("color", Color::Black()); + auto filter = tgfx::DropShadowFilter::Make(dx, dy, blur, blur, ToTGFX(color)); + layer->setFilter(filter); + } + } + + // Layer styles + std::vector> styles; + for (size_t i = 0; i < node->childCount(); ++i) { + auto child = node->childAt(i); + if (child->type() == PAGXNodeType::DropShadowStyle) { + float dx = child->getFloatAttribute("dx", 0); + float dy = child->getFloatAttribute("dy", 0); + float blur = child->getFloatAttribute("blurRadius", 0); + auto color = child->getColorAttribute("color", Color::Black()); + auto style = tgfx::DropShadowStyle::Make(dx, dy, blur, blur, ToTGFX(color)); + styles.push_back(style); + } else if (child->type() == PAGXNodeType::InnerShadowStyle) { + float dx = child->getFloatAttribute("dx", 0); + float dy = child->getFloatAttribute("dy", 0); + float blur = child->getFloatAttribute("blurRadius", 0); + auto color = child->getColorAttribute("color", Color::Black()); + auto style = tgfx::InnerShadowStyle::Make(dx, dy, blur, blur, ToTGFX(color)); + styles.push_back(style); + } + } + if (!styles.empty()) { + layer->setLayerStyles(styles); + } + } + + LayerBuilder::Options _options = {}; + std::unordered_map _resourceNodes = {}; +}; + +// Public API implementation + +PAGXContent LayerBuilder::Build(const PAGXDocument& document, const Options& options) { + LayerBuilderImpl builder(options); + return builder.build(document); +} + +PAGXContent LayerBuilder::FromFile(const std::string& filePath, const Options& options) { + auto document = PAGXDocument::FromFile(filePath); + if (!document) { + return {}; + } + + auto opts = options; + if (opts.basePath.empty()) { + auto lastSlash = filePath.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + opts.basePath = filePath.substr(0, lastSlash + 1); + } + } + + return Build(*document, opts); +} + +PAGXContent LayerBuilder::FromData(const uint8_t* data, size_t length, const Options& options) { + auto document = PAGXDocument::FromXML(data, length); + if (!document) { + return {}; + } + return Build(*document, options); +} + +PAGXContent LayerBuilder::FromSVGFile(const std::string& filePath, const Options& options) { + auto document = PAGXSVGParser::Parse(filePath); + if (!document) { + return {}; + } + + auto opts = options; + if (opts.basePath.empty()) { + auto lastSlash = filePath.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + opts.basePath = filePath.substr(0, lastSlash + 1); + } + } + + return Build(*document, opts); +} + +} // namespace pagx diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 839ef02e0b..fc0e3cd78a 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -17,47 +17,18 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include -#include "pagx/layers/LayerBuilder.h" -#include "pagx/layers/TextLayouter.h" -#include "pagx/svg/SVGImporter.h" +#include "pagx/PAGXDocument.h" +#include "pagx/PAGXNode.h" +#include "pagx/PAGXSVGParser.h" +#include "pagx/PAGXTypes.h" +#include "pagx/PathData.h" #include "tgfx/core/Data.h" -#include "tgfx/core/Stream.h" -#include "tgfx/core/Typeface.h" -#include "tgfx/core/Surface.h" -#include "tgfx/svg/SVGDOM.h" -#include "tgfx/svg/TextShaper.h" -#include "utils/Baseline.h" -#include "utils/DevicePool.h" #include "utils/ProjectPath.h" #include "utils/TestUtils.h" namespace pag { -using namespace tgfx; - -static std::vector> GetFallbackTypefaces() { - static std::vector> typefaces; - static bool initialized = false; - if (!initialized) { - initialized = true; - auto regularTypeface = - Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf")); - if (regularTypeface) { - typefaces.push_back(regularTypeface); - } - auto emojiTypeface = - Typeface::MakeFromPath(ProjectPath::Absolute("resources/font/NotoColorEmoji.ttf")); - if (emojiTypeface) { - typefaces.push_back(emojiTypeface); - } - } - return typefaces; -} -static void SetupFallbackFonts() { - pagx::TextLayouter::SetFallbackTypefaces(GetFallbackTypefaces()); -} - -static void SaveFile(const std::shared_ptr& data, const std::string& key) { +static void SaveFile(const std::shared_ptr& data, const std::string& key) { if (!data) { return; } @@ -74,84 +45,215 @@ static void SaveFile(const std::shared_ptr& data, const std::string& key) } /** - * Test case: Convert all SVG files in apitest/SVG directory to PAGX format and render them + * Test case: PathData SVG string parsing and round-trip conversion + */ +PAG_TEST(PAGXTest, PathDataSVGRoundTrip) { + // Test basic path commands + std::string pathStr = "M10 20 L30 40 H50 V60 C70 80 90 100 110 120 S130 140 150 160 " + "Q170 180 190 200 T210 220 A10 20 30 1 0 230 240 Z"; + + auto pathData = pagx::PathData::FromSVGString(pathStr); + EXPECT_GT(pathData.verbs().size(), 0u); + EXPECT_GT(pathData.countPoints(), 0u); + + // Verify round-trip conversion + std::string outputStr = pathData.toSVGString(); + EXPECT_FALSE(outputStr.empty()); + + // Parse the output string and verify it produces the same structure + auto pathData2 = pagx::PathData::FromSVGString(outputStr); + EXPECT_EQ(pathData.verbs().size(), pathData2.verbs().size()); +} + +/** + * Test case: PathData forEach iteration + */ +PAG_TEST(PAGXTest, PathDataForEach) { + std::string pathStr = "M0 0 L100 0 L100 100 L0 100 Z"; + + auto pathData = pagx::PathData::FromSVGString(pathStr); + + int verbCount = 0; + pathData.forEach([&verbCount](pagx::PathVerb, const float*) { verbCount++; }); + + EXPECT_EQ(verbCount, 5); // M, L, L, L, Z +} + +/** + * Test case: PAGXNode basic operations + */ +PAG_TEST(PAGXTest, PAGXNodeBasic) { + auto node = pagx::PAGXNode::Make(pagx::PAGXNodeType::Group); + ASSERT_TRUE(node != nullptr); + EXPECT_EQ(node->type(), pagx::PAGXNodeType::Group); + EXPECT_STREQ(pagx::PAGXNodeTypeName(node->type()), "Group"); + EXPECT_EQ(node->childCount(), 0u); + + // Add child nodes + auto path1 = pagx::PAGXNode::Make(pagx::PAGXNodeType::Path); + auto path2 = pagx::PAGXNode::Make(pagx::PAGXNodeType::Path); + auto* path1Ptr = path1.get(); + node->appendChild(std::move(path1)); + node->appendChild(std::move(path2)); + EXPECT_EQ(node->childCount(), 2u); + + // Set attributes + path1Ptr->setAttribute("d", "M0 0 L100 100"); + path1Ptr->setAttribute("fill", "#FF0000"); + + EXPECT_TRUE(path1Ptr->hasAttribute("d")); + EXPECT_TRUE(path1Ptr->hasAttribute("fill")); + EXPECT_FALSE(path1Ptr->hasAttribute("stroke")); + EXPECT_EQ(path1Ptr->getAttribute("fill"), "#FF0000"); +} + +/** + * Test case: PAGXDocument creation and XML export + */ +PAG_TEST(PAGXTest, PAGXDocumentXMLExport) { + auto doc = pagx::PAGXDocument::Make(400, 300); + ASSERT_TRUE(doc != nullptr); + EXPECT_EQ(doc->width(), 400.0f); + EXPECT_EQ(doc->height(), 300.0f); + + auto root = doc->root(); + ASSERT_TRUE(root != nullptr); + EXPECT_EQ(root->type(), pagx::PAGXNodeType::Document); + + // Add a group with a path + auto group = pagx::PAGXNode::Make(pagx::PAGXNodeType::Group); + group->setId("testGroup"); + + auto path = pagx::PAGXNode::Make(pagx::PAGXNodeType::Path); + path->setAttribute("d", "M10 10 L90 10 L90 90 L10 90 Z"); + path->setAttribute("fill", "#0000FF"); + + group->appendChild(std::move(path)); + root->appendChild(std::move(group)); + + // Export to XML + std::string xml = doc->toXML(); + EXPECT_FALSE(xml.empty()); + EXPECT_NE(xml.find(" export -> parse) */ -PAG_TEST(PAGXTest, SVGToPAGXAll) { - SetupFallbackFonts(); +PAG_TEST(PAGXTest, PAGXDocumentRoundTrip) { + // Create a document + auto doc1 = pagx::PAGXDocument::Make(200, 150); + ASSERT_TRUE(doc1 != nullptr); + + auto root1 = doc1->root(); + auto rect = pagx::PAGXNode::Make(pagx::PAGXNodeType::Rectangle); + rect->setAttribute("x", "10"); + rect->setAttribute("y", "20"); + rect->setAttribute("width", "80"); + rect->setAttribute("height", "60"); + rect->setAttribute("fill", "#00FF00"); + root1->appendChild(std::move(rect)); + + // Export to XML + std::string xml = doc1->toXML(); + EXPECT_FALSE(xml.empty()); + // Parse the XML back + auto doc2 = pagx::PAGXDocument::FromXML(xml); + ASSERT_TRUE(doc2 != nullptr); + + // Verify the dimensions + EXPECT_FLOAT_EQ(doc2->width(), 200.0f); + EXPECT_FLOAT_EQ(doc2->height(), 150.0f); + + // Verify the structure + auto root2 = doc2->root(); + ASSERT_TRUE(root2 != nullptr); + EXPECT_GE(root2->childCount(), 1u); +} + +/** + * Test case: Convert SVG files to PAGX format + */ +PAG_TEST(PAGXTest, SVGToPAGXConversion) { std::string svgDir = ProjectPath::Absolute("resources/apitest/SVG"); - std::vector svgFiles = {}; + if (!std::filesystem::exists(svgDir)) { + return; // Skip if directory doesn't exist + } + + std::vector svgFiles = {}; for (const auto& entry : std::filesystem::directory_iterator(svgDir)) { if (entry.path().extension() == ".svg") { svgFiles.push_back(entry.path().string()); } } - std::sort(svgFiles.begin(), svgFiles.end()); - auto device = DevicePool::Make(); - ASSERT_TRUE(device != nullptr); - auto context = device->lockContext(); - ASSERT_TRUE(context != nullptr); - - // Create text shaper for SVG rendering - auto textShaper = TextShaper::Make(GetFallbackTypefaces()); + pagx::PAGXSVGParser::Options options; for (const auto& svgPath : svgFiles) { std::string baseName = std::filesystem::path(svgPath).stem().string(); - // Load original SVG with text shaper - auto svgStream = Stream::MakeFromFile(svgPath); - if (svgStream == nullptr) { - continue; - } - auto svgDOM = SVGDOM::Make(*svgStream, textShaper); - if (svgDOM == nullptr) { - continue; - } - - auto containerSize = svgDOM->getContainerSize(); - int width = static_cast(containerSize.width); - int height = static_cast(containerSize.height); - if (width <= 0 || height <= 0) { - continue; + // Parse SVG to PAGX document + auto doc = pagx::PAGXSVGParser::Parse(svgPath, options); + if (doc == nullptr) { + continue; // Some SVGs may fail to parse } - // Render original SVG - auto svgSurface = Surface::Make(context, width, height); - auto svgCanvas = svgSurface->getCanvas(); - svgDOM->render(svgCanvas); - EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); + EXPECT_GT(doc->width(), 0.0f); + EXPECT_GT(doc->height(), 0.0f); - // Convert to PAGX - auto pagxContent = pagx::SVGImporter::ImportFromFile(svgPath); - if (pagxContent.empty()) { - continue; - } + // Export to XML + std::string xml = doc->toXML(); + EXPECT_FALSE(xml.empty()); // Save PAGX file to output directory - auto pagxData = Data::MakeWithCopy(pagxContent.data(), pagxContent.size()); + auto pagxData = tgfx::Data::MakeWithCopy(xml.data(), xml.size()); SaveFile(pagxData, "PAGXTest/" + baseName + ".pagx"); + } +} - auto pagxStream = Stream::MakeFromData(pagxData); - if (pagxStream == nullptr) { - continue; - } +/** + * Test case: PAGXTypes basic operations + */ +PAG_TEST(PAGXTest, PAGXTypesBasic) { + // Test Point + pagx::Point p1 = {10.0f, 20.0f}; + EXPECT_FLOAT_EQ(p1.x, 10.0f); + EXPECT_FLOAT_EQ(p1.y, 20.0f); - auto content = pagx::LayerBuilder::FromStream(*pagxStream); - if (content.root == nullptr) { - continue; - } + // Test Rect + pagx::Rect r1 = {0.0f, 0.0f, 100.0f, 50.0f}; + EXPECT_FLOAT_EQ(r1.left, 0.0f); + EXPECT_FLOAT_EQ(r1.top, 0.0f); + EXPECT_FLOAT_EQ(r1.right, 100.0f); + EXPECT_FLOAT_EQ(r1.bottom, 50.0f); - // Render PAGX - auto pagxSurface = Surface::Make(context, width, height); - auto pagxCanvas = pagxSurface->getCanvas(); - content.root->draw(pagxCanvas); - EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName + "_pagx")); - } + // Test Color + pagx::Color c1 = {1.0f, 0.5f, 0.0f, 1.0f}; + EXPECT_FLOAT_EQ(c1.red, 1.0f); + EXPECT_FLOAT_EQ(c1.green, 0.5f); + EXPECT_FLOAT_EQ(c1.blue, 0.0f); + EXPECT_FLOAT_EQ(c1.alpha, 1.0f); - device->unlock(); + // Test Matrix (identity) + pagx::Matrix m1 = {}; + EXPECT_FLOAT_EQ(m1.scaleX, 1.0f); + EXPECT_FLOAT_EQ(m1.skewX, 0.0f); + EXPECT_FLOAT_EQ(m1.transX, 0.0f); + EXPECT_FLOAT_EQ(m1.skewY, 0.0f); + EXPECT_FLOAT_EQ(m1.scaleY, 1.0f); + EXPECT_FLOAT_EQ(m1.transY, 0.0f); } } // namespace pag From 1eb0f9a43a493ab0fc435a71cc5e220ba0b329b4 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 19:05:01 +0800 Subject: [PATCH 007/678] Refactor PAGX module to use strongly-typed node structures with inheritance and virtual functions. --- pagx/CMakeLists.txt | 3 +- pagx/include/pagx/LayerBuilder.h | 7 +- pagx/include/pagx/PAGXDocument.h | 132 ++-- pagx/include/pagx/PAGXNode.h | 1037 +++++++++++++++++++++----- pagx/include/pagx/PAGXTypes.h | 545 ++++++++++---- pagx/src/PAGXDocument.cpp | 108 ++- pagx/src/PAGXNode.cpp | 381 ++-------- pagx/src/PAGXTypes.cpp | 356 +++++++++ pagx/src/PAGXXMLParser.cpp | 1197 +++++++++++++++++++++++++----- pagx/src/PAGXXMLParser.h | 85 ++- pagx/src/PAGXXMLWriter.cpp | 869 +++++++++++++++++++--- pagx/src/PAGXXMLWriter.h | 8 +- pagx/src/PathData.cpp | 46 +- pagx/src/svg/PAGXSVGParser.cpp | 637 ++++++++-------- pagx/src/svg/SVGParserInternal.h | 52 +- pagx/src/tgfx/LayerBuilder.cpp | 636 +++++++--------- test/src/PAGXTest.cpp | 274 +++++-- 17 files changed, 4428 insertions(+), 1945 deletions(-) create mode 100644 pagx/src/PAGXTypes.cpp diff --git a/pagx/CMakeLists.txt b/pagx/CMakeLists.txt index 34388992c7..0fcf21ea5c 100644 --- a/pagx/CMakeLists.txt +++ b/pagx/CMakeLists.txt @@ -5,13 +5,14 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) option(PAGX_BUILD_SVG "Build PAGX SVG parser module" ON) -option(PAGX_BUILD_TGFX_ADAPTER "Build PAGX to tgfx adapter (LayerBuilder)" OFF) +option(PAGX_BUILD_TGFX_ADAPTER "Build PAGX to tgfx adapter (LayerBuilder)" ON) # ============== Core PAGX Library (Independent, no tgfx dependency) ============== # Core sources file(GLOB PAGX_CORE_SOURCES src/PAGXNode.cpp + src/PAGXTypes.cpp src/PAGXDocument.cpp src/PAGXXMLParser.cpp src/PAGXXMLWriter.cpp diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index d4b47c4d67..e8b0c64634 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -57,14 +57,15 @@ class LayerBuilder { /** * Fallback typefaces for text rendering. */ - std::vector> fallbackTypefaces = {}; + std::vector> fallbackTypefaces; /** * Base path for resolving relative resource paths. */ - std::string basePath = {}; + std::string basePath; - Options() = default; + Options() : fallbackTypefaces(), basePath() { + } }; /** diff --git a/pagx/include/pagx/PAGXDocument.h b/pagx/include/pagx/PAGXDocument.h index 747c150a58..4aff3dd7f4 100644 --- a/pagx/include/pagx/PAGXDocument.h +++ b/pagx/include/pagx/PAGXDocument.h @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2026 Tencent. All rights reserved. +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -21,146 +21,106 @@ #include #include #include +#include #include "pagx/PAGXNode.h" namespace pagx { +class PAGXXMLParser; + /** - * PAGXDocument is the central data structure for the PAGX format. - * It can be created from various sources (XML, SVG, PDF) and exported - * to various formats (XML, PAG binary). + * PAGXDocument is the root container for a PAGX document. + * It contains resources and layers, and provides methods for loading, saving, and manipulating + * the document. */ class PAGXDocument { public: /** - * Creates an empty PAGX document with the specified dimensions. - */ - static std::shared_ptr Make(float width, float height); - - /** - * Creates a PAGX document from a file. - * Supports .pagx (XML) and .svg files. + * Format version. */ - static std::shared_ptr FromFile(const std::string& filePath); + std::string version = "1.0"; /** - * Creates a PAGX document from XML content. + * Canvas width. */ - static std::shared_ptr FromXML(const std::string& xmlContent); + float width = 0; /** - * Creates a PAGX document from XML data. + * Canvas height. */ - static std::shared_ptr FromXML(const uint8_t* data, size_t length); + float height = 0; /** - * Returns the width of the document. + * Resources (images, gradients, compositions, etc.). + * These can be referenced by "#id" in the document. */ - float width() const { - return _width; - } + std::vector> resources = {}; /** - * Returns the height of the document. + * Top-level layers. */ - float height() const { - return _height; - } + std::vector> layers = {}; /** - * Sets the document dimensions. + * Base path for resolving relative resource paths. */ - void setSize(float width, float height); + std::string basePath = {}; /** - * Returns the PAGX version string. + * Creates an empty document with the specified size. */ - const std::string& version() const { - return _version; - } - - /** - * Returns the root node of the document tree. - */ - PAGXNode* root() const { - return _root.get(); - } - - /** - * Sets the root node of the document. - */ - void setRoot(std::unique_ptr root); - - /** - * Creates a new node with the specified type. - */ - std::unique_ptr createNode(PAGXNodeType type); - - // ============== Resource Management ============== - - /** - * Returns a resource node by its ID. - */ - PAGXNode* getResourceById(const std::string& id) const; + static std::shared_ptr Make(float width, float height); /** - * Adds a resource to the document. - * The resource must have a unique ID. + * Loads a document from a file. + * Returns nullptr if the file cannot be loaded. */ - void addResource(std::unique_ptr resource); + static std::shared_ptr FromFile(const std::string& filePath); /** - * Returns all resource IDs. + * Parses a document from XML content. + * Returns nullptr if parsing fails. */ - std::vector getResourceIds() const; + static std::shared_ptr FromXML(const std::string& xmlContent); /** - * Returns the resources node. + * Parses a document from XML data. + * Returns nullptr if parsing fails. */ - PAGXNode* resources() const { - return _resources.get(); - } - - // ============== Export ============== + static std::shared_ptr FromXML(const uint8_t* data, size_t length); /** - * Exports the document to PAGX XML format. + * Exports the document to XML format. */ std::string toXML() const; /** - * Saves the document to a file. + * Returns a deep clone of this document. */ - bool saveToFile(const std::string& filePath) const; - - // ============== Base Path ============== + std::shared_ptr clone() const; /** - * Returns the base path for resolving relative resource paths. + * Finds a resource by ID. + * Returns nullptr if not found. */ - const std::string& basePath() const { - return _basePath; - } + ResourceNode* findResource(const std::string& id) const; /** - * Sets the base path for resolving relative resource paths. + * Finds a layer by ID (searches recursively). + * Returns nullptr if not found. */ - void setBasePath(const std::string& path) { - _basePath = path; - } + LayerNode* findLayer(const std::string& id) const; private: + friend class PAGXXMLParser; PAGXDocument() = default; - float _width = 0.0f; - float _height = 0.0f; - std::string _version = "1.0"; - std::string _basePath = {}; - std::unique_ptr _root = nullptr; - std::unique_ptr _resources = nullptr; - std::unordered_map _resourceMap = {}; + mutable std::unordered_map resourceMap = {}; + mutable bool resourceMapDirty = true; - friend class PAGXXMLParser; + void rebuildResourceMap() const; + static LayerNode* findLayerRecursive(const std::vector>& layers, + const std::string& id); }; } // namespace pagx diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index 27bad2ecc6..1a734c3fbc 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2026 Tencent. All rights reserved. +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -20,276 +20,947 @@ #include #include -#include #include #include "pagx/PAGXTypes.h" #include "pagx/PathData.h" namespace pagx { +class PAGXNode; +class ColorSourceNode; +class VectorElementNode; +class LayerStyleNode; +class LayerFilterNode; +struct LayerNode; + /** - * Types of nodes in a PAGX document. + * Node types in PAGX document. */ -enum class PAGXNodeType { - // Document root - Document, - Resources, - - // Layers - Layer, +enum class NodeType { + // Color sources + SolidColor, + LinearGradient, + RadialGradient, + ConicGradient, + DiamondGradient, + ImagePattern, + ColorStop, - // Vector Elements - Group, + // Geometry elements Rectangle, Ellipse, Polystar, Path, - Text, + TextSpan, - // Styles + // Painters Fill, Stroke, + + // Shape modifiers TrimPath, RoundCorner, MergePath, + + // Text modifiers + TextModifier, + TextPath, + TextLayout, + RangeSelector, + + // Repeater Repeater, - // Color Sources - SolidColor, - LinearGradient, - RadialGradient, - ConicGradient, - DiamondGradient, - ImagePattern, + // Container + Group, - // Effects - BlurFilter, - DropShadowFilter, + // Layer styles DropShadowStyle, InnerShadowStyle, + BackgroundBlurStyle, - // Text - TextSpan, - - // Masks - Mask, - ClipPath, + // Layer filters + BlurFilter, + DropShadowFilter, + InnerShadowFilter, + BlendFilter, + ColorMatrixFilter, - // Image + // Resources Image, + Composition, - // Unknown/Custom - Unknown + // Layer + Layer }; /** * Returns the string name of a node type. */ -const char* PAGXNodeTypeName(PAGXNodeType type); +const char* NodeTypeName(NodeType type); /** - * Parses a node type from its string name. - */ -PAGXNodeType PAGXNodeTypeFromName(const std::string& name); - -/** - * PAGXNode represents a node in the PAGX document tree. - * Each node has a type, attributes, and optional children. + * Base class for all PAGX nodes. */ class PAGXNode { public: virtual ~PAGXNode() = default; /** - * Creates a new node with the specified type. + * Returns the type of this node. */ - static std::unique_ptr Make(PAGXNodeType type); + virtual NodeType type() const = 0; /** - * Returns the type of this node. + * Returns a deep clone of this node. */ - PAGXNodeType type() const { - return _type; + virtual std::unique_ptr clone() const = 0; + + protected: + PAGXNode() = default; +}; + +//============================================================================== +// Resource Node (base class - must be defined before ColorSourceNode) +//============================================================================== + +/** + * Base class for resource nodes. + */ +class ResourceNode : public PAGXNode { + public: + std::string id = {}; +}; + +//============================================================================== +// Color Source Nodes +//============================================================================== + +/** + * A color stop in a gradient. + */ +struct ColorStopNode : public PAGXNode { + float offset = 0; + Color color = {}; + + NodeType type() const override { + return NodeType::ColorStop; } - /** - * Returns the ID of this node. - */ - const std::string& id() const { - return _id; + std::unique_ptr clone() const override { + return std::make_unique(*this); } +}; - /** - * Sets the ID of this node. - */ - void setId(const std::string& id) { - _id = id; +/** + * Base class for color source nodes. + * Color sources can be stored as resources (with an id) or inline. + */ +class ColorSourceNode : public ResourceNode { +}; + +/** + * A solid color. + */ +struct SolidColorNode : public ColorSourceNode { + Color color = {}; + + NodeType type() const override { + return NodeType::SolidColor; } - // ============== Attribute Access ============== + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Returns true if the node has an attribute with the given name. - */ - bool hasAttribute(const std::string& name) const; +/** + * A linear gradient. + */ +struct LinearGradientNode : public ColorSourceNode { + float startX = 0; + float startY = 0; + float endX = 0; + float endY = 0; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::LinearGradient; + } - /** - * Returns the string value of an attribute. - */ - std::string getAttribute(const std::string& name, const std::string& defaultValue = "") const; + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Returns the float value of an attribute. - */ - float getFloatAttribute(const std::string& name, float defaultValue = 0.0f) const; +/** + * A radial gradient. + */ +struct RadialGradientNode : public ColorSourceNode { + float centerX = 0; + float centerY = 0; + float radius = 0; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::RadialGradient; + } - /** - * Returns the integer value of an attribute. - */ - int getIntAttribute(const std::string& name, int defaultValue = 0) const; + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Returns the boolean value of an attribute. - */ - bool getBoolAttribute(const std::string& name, bool defaultValue = true) const; +/** + * A conic (sweep) gradient. + */ +struct ConicGradientNode : public ColorSourceNode { + float centerX = 0; + float centerY = 0; + float startAngle = 0; + float endAngle = 360; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::ConicGradient; + } - /** - * Returns the color value of an attribute. - */ - Color getColorAttribute(const std::string& name, const Color& defaultValue = Color::Black()) const; + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Returns the matrix value of an attribute. - */ - Matrix getMatrixAttribute(const std::string& name) const; +/** + * A diamond gradient. + */ +struct DiamondGradientNode : public ColorSourceNode { + float centerX = 0; + float centerY = 0; + float halfDiagonal = 0; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::DiamondGradient; + } - /** - * Returns the path data value of an attribute. - */ - PathData getPathAttribute(const std::string& name) const; + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Sets a string attribute. - */ - void setAttribute(const std::string& name, const std::string& value); +/** + * An image pattern. + */ +struct ImagePatternNode : public ColorSourceNode { + std::string image = {}; + TileMode tileModeX = TileMode::Clamp; + TileMode tileModeY = TileMode::Clamp; + SamplingMode sampling = SamplingMode::Linear; + Matrix matrix = {}; + + NodeType type() const override { + return NodeType::ImagePattern; + } - /** - * Sets a float attribute. - */ - void setFloatAttribute(const std::string& name, float value); + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Sets an integer attribute. - */ - void setIntAttribute(const std::string& name, int value); +//============================================================================== +// Vector Element Nodes +//============================================================================== - /** - * Sets a boolean attribute. - */ - void setBoolAttribute(const std::string& name, bool value); +/** + * Base class for vector element nodes. + */ +class VectorElementNode : public PAGXNode {}; - /** - * Sets a color attribute. - */ - void setColorAttribute(const std::string& name, const Color& color); +/** + * A rectangle shape. + */ +struct RectangleNode : public VectorElementNode { + float centerX = 0; + float centerY = 0; + float width = 0; + float height = 0; + float roundness = 0; + bool reversed = false; + + NodeType type() const override { + return NodeType::Rectangle; + } - /** - * Sets a matrix attribute. - */ - void setMatrixAttribute(const std::string& name, const Matrix& matrix); + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Sets a path data attribute. - */ - void setPathAttribute(const std::string& name, const PathData& path); +/** + * An ellipse shape. + */ +struct EllipseNode : public VectorElementNode { + float centerX = 0; + float centerY = 0; + float width = 0; + float height = 0; + bool reversed = false; + + NodeType type() const override { + return NodeType::Ellipse; + } - /** - * Removes an attribute. - */ - void removeAttribute(const std::string& name); + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Returns all attribute names. - */ - std::vector getAttributeNames() const; +/** + * A polygon or star shape. + */ +struct PolystarNode : public VectorElementNode { + float centerX = 0; + float centerY = 0; + PolystarType polystarType = PolystarType::Star; + float points = 5; + float outerRadius = 100; + float innerRadius = 50; + float rotation = 0; + float outerRoundness = 0; + float innerRoundness = 0; + bool reversed = false; + + NodeType type() const override { + return NodeType::Polystar; + } - // ============== Children ============== + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Returns the child nodes. - */ - const std::vector>& children() const { - return _children; +/** + * A path shape. + */ +struct PathNode : public VectorElementNode { + PathData d = {}; + bool reversed = false; + + NodeType type() const override { + return NodeType::Path; } - /** - * Returns the number of children. - */ - size_t childCount() const { - return _children.size(); + std::unique_ptr clone() const override { + return std::make_unique(*this); } +}; - /** - * Returns the child at the specified index. - */ - PAGXNode* childAt(size_t index) const; +/** + * A text span. + */ +struct TextSpanNode : public VectorElementNode { + float x = 0; + float y = 0; + std::string font = {}; + float fontSize = 12; + int fontWeight = 400; + FontStyle fontStyle = FontStyle::Normal; + float tracking = 0; + float baselineShift = 0; + std::string text = {}; + + NodeType type() const override { + return NodeType::TextSpan; + } - /** - * Appends a child node. - */ - void appendChild(std::unique_ptr child); + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Inserts a child node at the specified index. - */ - void insertChild(size_t index, std::unique_ptr child); +//============================================================================== +// Painter Nodes +//============================================================================== - /** - * Removes and returns the child at the specified index. - */ - std::unique_ptr removeChild(size_t index); +/** + * A fill painter. + * The color can be a simple color string ("#FF0000"), a reference ("#gradientId"), + * or an inline color source node. + */ +struct FillNode : public VectorElementNode { + std::string color = {}; + std::unique_ptr colorSource = nullptr; + float alpha = 1; + BlendMode blendMode = BlendMode::Normal; + FillRule fillRule = FillRule::Winding; + Placement placement = Placement::Background; + + NodeType type() const override { + return NodeType::Fill; + } - /** - * Removes all children. - */ - void clearChildren(); + std::unique_ptr clone() const override { + auto node = std::make_unique(); + node->color = color; + if (colorSource) { + node->colorSource.reset(static_cast(colorSource->clone().release())); + } + node->alpha = alpha; + node->blendMode = blendMode; + node->fillRule = fillRule; + node->placement = placement; + return node; + } +}; - // ============== Parent ============== +/** + * A stroke painter. + */ +struct StrokeNode : public VectorElementNode { + std::string color = {}; + std::unique_ptr colorSource = nullptr; + float strokeWidth = 1; + float alpha = 1; + BlendMode blendMode = BlendMode::Normal; + LineCap cap = LineCap::Butt; + LineJoin join = LineJoin::Miter; + float miterLimit = 4; + std::vector dashes = {}; + float dashOffset = 0; + StrokeAlign align = StrokeAlign::Center; + Placement placement = Placement::Background; + + NodeType type() const override { + return NodeType::Stroke; + } - /** - * Returns the parent node (weak reference). - */ - PAGXNode* parent() const { - return _parent; + std::unique_ptr clone() const override { + auto node = std::make_unique(); + node->color = color; + if (colorSource) { + node->colorSource.reset(static_cast(colorSource->clone().release())); + } + node->strokeWidth = strokeWidth; + node->alpha = alpha; + node->blendMode = blendMode; + node->cap = cap; + node->join = join; + node->miterLimit = miterLimit; + node->dashes = dashes; + node->dashOffset = dashOffset; + node->align = align; + node->placement = placement; + return node; } +}; - // ============== Traversal ============== +//============================================================================== +// Shape Modifier Nodes +//============================================================================== - /** - * Finds the first descendant node with the given ID. - */ - PAGXNode* findById(const std::string& id); +/** + * Trim path modifier. + */ +struct TrimPathNode : public VectorElementNode { + float start = 0; + float end = 1; + float offset = 0; + TrimType trimType = TrimType::Separate; + + NodeType type() const override { + return NodeType::TrimPath; + } - /** - * Finds all descendant nodes of the given type. - */ - std::vector findByType(PAGXNodeType type); + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; - /** - * Creates a deep copy of this node and all its children. - */ - std::unique_ptr clone() const; +/** + * Round corner modifier. + */ +struct RoundCornerNode : public VectorElementNode { + float radius = 0; - protected: - explicit PAGXNode(PAGXNodeType type) : _type(type) { + NodeType type() const override { + return NodeType::RoundCorner; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Merge path modifier. + */ +struct MergePathNode : public VectorElementNode { + PathOp op = PathOp::Append; + + NodeType type() const override { + return NodeType::MergePath; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +//============================================================================== +// Text Modifier Nodes +//============================================================================== + +/** + * Range selector for text modifier. + */ +struct RangeSelectorNode : public PAGXNode { + float start = 0; + float end = 1; + float offset = 0; + SelectorUnit unit = SelectorUnit::Percentage; + SelectorShape shape = SelectorShape::Square; + float easeIn = 0; + float easeOut = 0; + SelectorMode mode = SelectorMode::Add; + float weight = 1; + bool randomize = false; + int seed = 0; + + NodeType type() const override { + return NodeType::RangeSelector; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Text modifier. + */ +struct TextModifierNode : public VectorElementNode { + Point anchor = {0.5f, 0.5f}; + Point position = {}; + float rotation = 0; + Point scale = {1, 1}; + float skew = 0; + float skewAxis = 0; + float alpha = 1; + std::string fillColor = {}; + std::string strokeColor = {}; + float strokeWidth = -1; + std::vector rangeSelectors = {}; + + NodeType type() const override { + return NodeType::TextModifier; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Text path modifier. + */ +struct TextPathNode : public VectorElementNode { + std::string path = {}; + TextPathAlign textPathAlign = TextPathAlign::Start; + float firstMargin = 0; + float lastMargin = 0; + bool perpendicularToPath = true; + bool reversed = false; + bool forceAlignment = false; + + NodeType type() const override { + return NodeType::TextPath; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Text layout modifier. + */ +struct TextLayoutNode : public VectorElementNode { + float width = 0; + float height = 0; + TextAlign textAlign = TextAlign::Left; + VerticalAlign verticalAlign = VerticalAlign::Top; + float lineHeight = 1.2f; + float indent = 0; + Overflow overflow = Overflow::Clip; + + NodeType type() const override { + return NodeType::TextLayout; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +//============================================================================== +// Repeater Node +//============================================================================== + +/** + * Repeater modifier. + */ +struct RepeaterNode : public VectorElementNode { + float copies = 3; + float offset = 0; + RepeaterOrder order = RepeaterOrder::BelowOriginal; + Point anchor = {}; + Point position = {100, 100}; + float rotation = 0; + Point scale = {1, 1}; + float startAlpha = 1; + float endAlpha = 1; + + NodeType type() const override { + return NodeType::Repeater; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +//============================================================================== +// Group Node +//============================================================================== + +/** + * Group container. + */ +struct GroupNode : public VectorElementNode { + std::string name = {}; + Point anchor = {}; + Point position = {}; + float rotation = 0; + Point scale = {1, 1}; + float skew = 0; + float skewAxis = 0; + float alpha = 1; + std::vector> elements = {}; + + NodeType type() const override { + return NodeType::Group; + } + + std::unique_ptr clone() const override { + auto node = std::make_unique(); + node->name = name; + node->anchor = anchor; + node->position = position; + node->rotation = rotation; + node->scale = scale; + node->skew = skew; + node->skewAxis = skewAxis; + node->alpha = alpha; + for (const auto& element : elements) { + node->elements.push_back( + std::unique_ptr(static_cast(element->clone().release()))); + } + return node; + } +}; + +//============================================================================== +// Layer Style Nodes +//============================================================================== + +/** + * Base class for layer style nodes. + */ +class LayerStyleNode : public PAGXNode { + public: + BlendMode blendMode = BlendMode::Normal; +}; + +/** + * Drop shadow style. + */ +struct DropShadowStyleNode : public LayerStyleNode { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool showBehindLayer = true; + + NodeType type() const override { + return NodeType::DropShadowStyle; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Inner shadow style. + */ +struct InnerShadowStyleNode : public LayerStyleNode { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + + NodeType type() const override { + return NodeType::InnerShadowStyle; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Background blur style. + */ +struct BackgroundBlurStyleNode : public LayerStyleNode { + float blurrinessX = 0; + float blurrinessY = 0; + TileMode tileMode = TileMode::Mirror; + + NodeType type() const override { + return NodeType::BackgroundBlurStyle; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +//============================================================================== +// Layer Filter Nodes +//============================================================================== + +/** + * Base class for layer filter nodes. + */ +class LayerFilterNode : public PAGXNode {}; + +/** + * Blur filter. + */ +struct BlurFilterNode : public LayerFilterNode { + float blurrinessX = 0; + float blurrinessY = 0; + TileMode tileMode = TileMode::Decal; + + NodeType type() const override { + return NodeType::BlurFilter; } - private: - PAGXNodeType _type = PAGXNodeType::Unknown; - std::string _id = {}; - std::unordered_map _attributes = {}; - std::vector> _children = {}; - PAGXNode* _parent = nullptr; + std::unique_ptr clone() const override { + return std::make_unique(*this); + } }; +/** + * Drop shadow filter. + */ +struct DropShadowFilterNode : public LayerFilterNode { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool shadowOnly = false; + + NodeType type() const override { + return NodeType::DropShadowFilter; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Inner shadow filter. + */ +struct InnerShadowFilterNode : public LayerFilterNode { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool shadowOnly = false; + + NodeType type() const override { + return NodeType::InnerShadowFilter; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Blend filter. + */ +struct BlendFilterNode : public LayerFilterNode { + Color color = {}; + BlendMode filterBlendMode = BlendMode::Normal; + + NodeType type() const override { + return NodeType::BlendFilter; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Color matrix filter. + */ +struct ColorMatrixFilterNode : public LayerFilterNode { + std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; + + NodeType type() const override { + return NodeType::ColorMatrixFilter; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +//============================================================================== +// Other Resource Nodes +//============================================================================== + +/** + * Image resource. + */ +struct ImageNode : public ResourceNode { + std::string source = {}; + + NodeType type() const override { + return NodeType::Image; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Composition resource. + */ +struct CompositionNode : public ResourceNode { + float width = 0; + float height = 0; + std::vector> layers = {}; + + NodeType type() const override { + return NodeType::Composition; + } + + std::unique_ptr clone() const override; +}; + +//============================================================================== +// Layer Node +//============================================================================== + +/** + * Layer node. + */ +struct LayerNode : public PAGXNode { + std::string id = {}; + std::string name = {}; + bool visible = true; + float alpha = 1; + BlendMode blendMode = BlendMode::Normal; + float x = 0; + float y = 0; + Matrix matrix = {}; + std::vector matrix3D = {}; + bool preserve3D = false; + bool antiAlias = true; + bool groupOpacity = false; + bool passThroughBackground = true; + bool excludeChildEffectsInLayerStyle = false; + Rect scrollRect = {}; + bool hasScrollRect = false; + std::string mask = {}; + MaskType maskType = MaskType::Alpha; + std::string composition = {}; + + std::vector> contents = {}; + std::vector> styles = {}; + std::vector> filters = {}; + std::vector> children = {}; + + NodeType type() const override { + return NodeType::Layer; + } + + std::unique_ptr clone() const override { + auto node = std::make_unique(); + node->id = id; + node->name = name; + node->visible = visible; + node->alpha = alpha; + node->blendMode = blendMode; + node->x = x; + node->y = y; + node->matrix = matrix; + node->matrix3D = matrix3D; + node->preserve3D = preserve3D; + node->antiAlias = antiAlias; + node->groupOpacity = groupOpacity; + node->passThroughBackground = passThroughBackground; + node->excludeChildEffectsInLayerStyle = excludeChildEffectsInLayerStyle; + node->scrollRect = scrollRect; + node->hasScrollRect = hasScrollRect; + node->mask = mask; + node->maskType = maskType; + node->composition = composition; + for (const auto& element : contents) { + node->contents.push_back( + std::unique_ptr(static_cast(element->clone().release()))); + } + for (const auto& style : styles) { + node->styles.push_back( + std::unique_ptr(static_cast(style->clone().release()))); + } + for (const auto& filter : filters) { + node->filters.push_back( + std::unique_ptr(static_cast(filter->clone().release()))); + } + for (const auto& child : children) { + node->children.push_back( + std::unique_ptr(static_cast(child->clone().release()))); + } + return node; + } +}; + +// Implementation of CompositionNode::clone +inline std::unique_ptr CompositionNode::clone() const { + auto node = std::make_unique(); + node->id = id; + node->width = width; + node->height = height; + for (const auto& layer : layers) { + node->layers.push_back( + std::unique_ptr(static_cast(layer->clone().release()))); + } + return node; +} + } // namespace pagx diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h index 7878c3b307..f915953c22 100644 --- a/pagx/include/pagx/PAGXTypes.h +++ b/pagx/include/pagx/PAGXTypes.h @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2026 Tencent. All rights reserved. +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -18,25 +18,18 @@ #pragma once -#include +#include #include +#include namespace pagx { /** - * Represents a 2D point with x and y coordinates. + * A point with x and y coordinates. */ struct Point { - float x = 0.0f; - float y = 0.0f; - - static Point Make(float x, float y) { - return {x, y}; - } - - static Point Zero() { - return {0.0f, 0.0f}; - } + float x = 0; + float y = 0; bool operator==(const Point& other) const { return x == other.x && y == other.y; @@ -45,133 +38,95 @@ struct Point { bool operator!=(const Point& other) const { return !(*this == other); } - - Point operator+(const Point& other) const { - return {x + other.x, y + other.y}; - } - - Point operator-(const Point& other) const { - return {x - other.x, y - other.y}; - } - - Point operator*(float scalar) const { - return {x * scalar, y * scalar}; - } }; /** - * Represents a rectangle with left, top, right, and bottom coordinates. + * A rectangle defined by position and size. */ struct Rect { - float left = 0.0f; - float top = 0.0f; - float right = 0.0f; - float bottom = 0.0f; + float x = 0; + float y = 0; + float width = 0; + float height = 0; - static Rect MakeEmpty() { - return {0.0f, 0.0f, 0.0f, 0.0f}; - } - - static Rect MakeWH(float width, float height) { - return {0.0f, 0.0f, width, height}; - } - - static Rect MakeXYWH(float x, float y, float width, float height) { - return {x, y, x + width, y + height}; + /** + * Returns a Rect from left, top, right, bottom coordinates. + */ + static Rect MakeLTRB(float l, float t, float r, float b) { + return {l, t, r - l, b - t}; } - static Rect MakeLTRB(float left, float top, float right, float bottom) { - return {left, top, right, bottom}; + /** + * Returns a Rect from position and size. + */ + static Rect MakeXYWH(float x, float y, float w, float h) { + return {x, y, w, h}; } - float x() const { - return left; + float left() const { + return x; } - float y() const { - return top; + float top() const { + return y; } - float width() const { - return right - left; + float right() const { + return x + width; } - float height() const { - return bottom - top; + float bottom() const { + return y + height; } bool isEmpty() const { - return left >= right || top >= bottom; + return width <= 0 || height <= 0; } - Point center() const { - return {(left + right) * 0.5f, (top + bottom) * 0.5f}; + void setEmpty() { + x = y = width = height = 0; } - void setEmpty() { - left = top = right = bottom = 0.0f; + bool operator==(const Rect& other) const { + return x == other.x && y == other.y && width == other.width && height == other.height; } - void join(const Rect& other) { - if (other.isEmpty()) { - return; - } - if (isEmpty()) { - *this = other; - return; - } - left = std::min(left, other.left); - top = std::min(top, other.top); - right = std::max(right, other.right); - bottom = std::max(bottom, other.bottom); + bool operator!=(const Rect& other) const { + return !(*this == other); } }; /** - * Represents a color with RGBA components in floating point (0.0 to 1.0). + * An RGBA color with floating-point components in [0, 1]. */ struct Color { - float red = 0.0f; - float green = 0.0f; - float blue = 0.0f; - float alpha = 1.0f; + float red = 0; + float green = 0; + float blue = 0; + float alpha = 1; - static Color Black() { - return {0.0f, 0.0f, 0.0f, 1.0f}; - } - - static Color White() { - return {1.0f, 1.0f, 1.0f, 1.0f}; - } - - static Color Transparent() { - return {0.0f, 0.0f, 0.0f, 0.0f}; - } + /** + * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). + */ + static Color FromHex(uint32_t hex, bool hasAlpha = false); - static Color FromRGBA(float r, float g, float b, float a = 1.0f) { - return {r, g, b, a}; - } + /** + * Returns a Color from RGBA components in [0, 1]. + */ + static Color FromRGBA(float r, float g, float b, float a = 1); /** - * Creates a color from a 32-bit hex value in AARRGGBB format. + * Parses a color string. Supports: + * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" + * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" + * Returns black if parsing fails. */ - static Color FromHex(uint32_t hex) { - return {static_cast((hex >> 16) & 0xFF) / 255.0f, - static_cast((hex >> 8) & 0xFF) / 255.0f, - static_cast(hex & 0xFF) / 255.0f, - static_cast((hex >> 24) & 0xFF) / 255.0f}; - } + static Color Parse(const std::string& str); /** - * Converts the color to a 32-bit hex value in AARRGGBB format. + * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". */ - uint32_t toHex() const { - auto r = static_cast(red * 255.0f + 0.5f); - auto g = static_cast(green * 255.0f + 0.5f); - auto b = static_cast(blue * 255.0f + 0.5f); - auto a = static_cast(alpha * 255.0f + 0.5f); - return (a << 24) | (r << 16) | (g << 8) | b; - } + std::string toHexString(bool includeAlpha = false) const; bool operator==(const Color& other) const { return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; @@ -180,89 +135,357 @@ struct Color { bool operator!=(const Color& other) const { return !(*this == other); } - - Color withAlpha(float newAlpha) const { - return {red, green, blue, newAlpha}; - } }; /** - * Represents a 3x3 transformation matrix for 2D graphics. - * The matrix is stored in row-major order: - * | scaleX skewX transX | - * | skewY scaleY transY | - * | 0 0 1 | + * A 2D affine transformation matrix. + * Matrix form: + * | a c tx | + * | b d ty | + * | 0 0 1 | */ struct Matrix { - float scaleX = 1.0f; - float skewX = 0.0f; - float transX = 0.0f; - float skewY = 0.0f; - float scaleY = 1.0f; - float transY = 0.0f; + float a = 1; // scaleX + float b = 0; // skewY + float c = 0; // skewX + float d = 1; // scaleY + float tx = 0; // transX + float ty = 0; // transY + /** + * Returns the identity matrix. + */ static Matrix Identity() { - return {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f}; + return {}; } - static Matrix Translate(float tx, float ty) { - return {1.0f, 0.0f, tx, 0.0f, 1.0f, ty}; - } + /** + * Returns a translation matrix. + */ + static Matrix Translate(float x, float y); - static Matrix Scale(float sx, float sy) { - return {sx, 0.0f, 0.0f, 0.0f, sy, 0.0f}; - } + /** + * Returns a scale matrix. + */ + static Matrix Scale(float sx, float sy); - static Matrix Rotate(float degrees) { - auto radians = degrees * 3.14159265358979323846f / 180.0f; - auto cosValue = std::cos(radians); - auto sinValue = std::sin(radians); - return {cosValue, -sinValue, 0.0f, sinValue, cosValue, 0.0f}; - } + /** + * Returns a rotation matrix (angle in degrees). + */ + static Matrix Rotate(float degrees); - static Matrix MakeAll(float scaleX, float skewX, float transX, float skewY, float scaleY, - float transY) { - return {scaleX, skewX, transX, skewY, scaleY, transY}; - } + /** + * Parses a matrix string "a,b,c,d,tx,ty". + */ + static Matrix Parse(const std::string& str); + /** + * Returns the matrix as a string "a,b,c,d,tx,ty". + */ + std::string toString() const; + + /** + * Returns true if this is the identity matrix. + */ bool isIdentity() const { - return scaleX == 1.0f && skewX == 0.0f && transX == 0.0f && skewY == 0.0f && scaleY == 1.0f && - transY == 0.0f; + return a == 1 && b == 0 && c == 0 && d == 1 && tx == 0 && ty == 0; } - Matrix operator*(const Matrix& other) const { - return {scaleX * other.scaleX + skewX * other.skewY, - scaleX * other.skewX + skewX * other.scaleY, - scaleX * other.transX + skewX * other.transY + transX, - skewY * other.scaleX + scaleY * other.skewY, - skewY * other.skewX + scaleY * other.scaleY, - skewY * other.transX + scaleY * other.transY + transY}; - } + /** + * Concatenates this matrix with another. + */ + Matrix operator*(const Matrix& other) const; - Point mapPoint(const Point& point) const { - return {scaleX * point.x + skewX * point.y + transX, - skewY * point.x + scaleY * point.y + transY}; - } + /** + * Transforms a point by this matrix. + */ + Point mapPoint(const Point& point) const; - void preTranslate(float tx, float ty) { - transX += scaleX * tx + skewX * ty; - transY += skewY * tx + scaleY * ty; + bool operator==(const Matrix& other) const { + return a == other.a && b == other.b && c == other.c && d == other.d && tx == other.tx && + ty == other.ty; } - void preScale(float sx, float sy) { - scaleX *= sx; - skewX *= sy; - skewY *= sx; - scaleY *= sy; + bool operator!=(const Matrix& other) const { + return !(*this == other); } +}; - void preConcat(const Matrix& other) { - *this = other * (*this); - } +//============================================================================== +// Enumerations +//============================================================================== - void postConcat(const Matrix& other) { - *this = (*this) * other; - } +/** + * Blend modes for compositing. + */ +enum class BlendMode { + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, + Add +}; + +/** + * Line cap styles for strokes. + */ +enum class LineCap { + Butt, + Round, + Square +}; + +/** + * Line join styles for strokes. + */ +enum class LineJoin { + Miter, + Round, + Bevel +}; + +/** + * Fill rules for paths. + */ +enum class FillRule { + Winding, + EvenOdd +}; + +/** + * Stroke alignment relative to path. + */ +enum class StrokeAlign { + Center, + Inside, + Outside +}; + +/** + * Placement of fill/stroke relative to child layers. + */ +enum class Placement { + Background, + Foreground +}; + +/** + * Tile modes for patterns and gradients. + */ +enum class TileMode { + Clamp, + Repeat, + Mirror, + Decal +}; + +/** + * Sampling modes for images. + */ +enum class SamplingMode { + Nearest, + Linear, + Mipmap +}; + +/** + * Mask types for layer masking. + */ +enum class MaskType { + Alpha, + Luminance, + Contour +}; + +/** + * Polystar types. + */ +enum class PolystarType { + Polygon, + Star +}; + +/** + * Trim path types. + */ +enum class TrimType { + Separate, + Continuous +}; + +/** + * Path boolean operations. + */ +enum class PathOp { + Append, + Union, + Intersect, + Xor, + Difference +}; + +/** + * Text horizontal alignment. + */ +enum class TextAlign { + Left, + Center, + Right, + Justify +}; + +/** + * Text vertical alignment. + */ +enum class VerticalAlign { + Top, + Center, + Bottom +}; + +/** + * Text overflow handling. + */ +enum class Overflow { + Clip, + Visible, + Ellipsis +}; + +/** + * Font style. + */ +enum class FontStyle { + Normal, + Italic }; +/** + * Text path alignment. + */ +enum class TextPathAlign { + Start, + Center, + End +}; + +/** + * Range selector unit. + */ +enum class SelectorUnit { + Index, + Percentage +}; + +/** + * Range selector shape. + */ +enum class SelectorShape { + Square, + RampUp, + RampDown, + Triangle, + Round, + Smooth +}; + +/** + * Range selector combination mode. + */ +enum class SelectorMode { + Add, + Subtract, + Intersect, + Min, + Max, + Difference +}; + +/** + * Repeater stacking order. + */ +enum class RepeaterOrder { + BelowOriginal, + AboveOriginal +}; + +//============================================================================== +// Enum string conversion utilities +//============================================================================== + +std::string BlendModeToString(BlendMode mode); +BlendMode BlendModeFromString(const std::string& str); + +std::string LineCapToString(LineCap cap); +LineCap LineCapFromString(const std::string& str); + +std::string LineJoinToString(LineJoin join); +LineJoin LineJoinFromString(const std::string& str); + +std::string FillRuleToString(FillRule rule); +FillRule FillRuleFromString(const std::string& str); + +std::string StrokeAlignToString(StrokeAlign align); +StrokeAlign StrokeAlignFromString(const std::string& str); + +std::string PlacementToString(Placement placement); +Placement PlacementFromString(const std::string& str); + +std::string TileModeToString(TileMode mode); +TileMode TileModeFromString(const std::string& str); + +std::string SamplingModeToString(SamplingMode mode); +SamplingMode SamplingModeFromString(const std::string& str); + +std::string MaskTypeToString(MaskType type); +MaskType MaskTypeFromString(const std::string& str); + +std::string PolystarTypeToString(PolystarType type); +PolystarType PolystarTypeFromString(const std::string& str); + +std::string TrimTypeToString(TrimType type); +TrimType TrimTypeFromString(const std::string& str); + +std::string PathOpToString(PathOp op); +PathOp PathOpFromString(const std::string& str); + +std::string TextAlignToString(TextAlign align); +TextAlign TextAlignFromString(const std::string& str); + +std::string VerticalAlignToString(VerticalAlign align); +VerticalAlign VerticalAlignFromString(const std::string& str); + +std::string OverflowToString(Overflow overflow); +Overflow OverflowFromString(const std::string& str); + +std::string FontStyleToString(FontStyle style); +FontStyle FontStyleFromString(const std::string& str); + +std::string TextPathAlignToString(TextPathAlign align); +TextPathAlign TextPathAlignFromString(const std::string& str); + +std::string SelectorUnitToString(SelectorUnit unit); +SelectorUnit SelectorUnitFromString(const std::string& str); + +std::string SelectorShapeToString(SelectorShape shape); +SelectorShape SelectorShapeFromString(const std::string& str); + +std::string SelectorModeToString(SelectorMode mode); +SelectorMode SelectorModeFromString(const std::string& str); + +std::string RepeaterOrderToString(RepeaterOrder order); +RepeaterOrder RepeaterOrderFromString(const std::string& str); + } // namespace pagx diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp index 18ca8925ae..18ccdf040c 100644 --- a/pagx/src/PAGXDocument.cpp +++ b/pagx/src/PAGXDocument.cpp @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2026 Tencent. All rights reserved. +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -24,14 +24,10 @@ namespace pagx { -std::shared_ptr PAGXDocument::Make(float width, float height) { +std::shared_ptr PAGXDocument::Make(float docWidth, float docHeight) { auto doc = std::shared_ptr(new PAGXDocument()); - doc->_width = width; - doc->_height = height; - doc->_root = PAGXNode::Make(PAGXNodeType::Document); - doc->_root->setFloatAttribute("width", width); - doc->_root->setFloatAttribute("height", height); - doc->_resources = PAGXNode::Make(PAGXNodeType::Resources); + doc->width = docWidth; + doc->height = docHeight; return doc; } @@ -40,17 +36,13 @@ std::shared_ptr PAGXDocument::FromFile(const std::string& filePath if (!file.is_open()) { return nullptr; } - - std::stringstream buffer; + std::stringstream buffer = {}; buffer << file.rdbuf(); - std::string content = buffer.str(); - - auto doc = FromXML(content); + auto doc = FromXML(buffer.str()); if (doc) { - // Extract base path from file path auto lastSlash = filePath.find_last_of("/\\"); if (lastSlash != std::string::npos) { - doc->_basePath = filePath.substr(0, lastSlash + 1); + doc->basePath = filePath.substr(0, lastSlash + 1); } } return doc; @@ -64,62 +56,62 @@ std::shared_ptr PAGXDocument::FromXML(const uint8_t* data, size_t return PAGXXMLParser::Parse(data, length); } -void PAGXDocument::setSize(float width, float height) { - _width = width; - _height = height; - if (_root) { - _root->setFloatAttribute("width", width); - _root->setFloatAttribute("height", height); - } -} - -void PAGXDocument::setRoot(std::unique_ptr root) { - _root = std::move(root); -} - -std::unique_ptr PAGXDocument::createNode(PAGXNodeType type) { - return PAGXNode::Make(type); +std::string PAGXDocument::toXML() const { + return PAGXXMLWriter::Write(*this); } -PAGXNode* PAGXDocument::getResourceById(const std::string& id) const { - auto it = _resourceMap.find(id); - if (it != _resourceMap.end()) { - return it->second; +std::shared_ptr PAGXDocument::clone() const { + auto doc = std::shared_ptr(new PAGXDocument()); + doc->version = version; + doc->width = width; + doc->height = height; + doc->basePath = basePath; + for (const auto& resource : resources) { + doc->resources.push_back( + std::unique_ptr(static_cast(resource->clone().release()))); } - return nullptr; + for (const auto& layer : layers) { + doc->layers.push_back( + std::unique_ptr(static_cast(layer->clone().release()))); + } + doc->resourceMapDirty = true; + return doc; } -void PAGXDocument::addResource(std::unique_ptr resource) { - if (!resource || resource->id().empty()) { - return; +ResourceNode* PAGXDocument::findResource(const std::string& id) const { + if (resourceMapDirty) { + rebuildResourceMap(); } - auto id = resource->id(); - auto* rawPtr = resource.get(); - _resources->appendChild(std::move(resource)); - _resourceMap[id] = rawPtr; + auto it = resourceMap.find(id); + return it != resourceMap.end() ? it->second : nullptr; } -std::vector PAGXDocument::getResourceIds() const { - std::vector ids; - ids.reserve(_resourceMap.size()); - for (const auto& pair : _resourceMap) { - ids.push_back(pair.first); - } - return ids; +LayerNode* PAGXDocument::findLayer(const std::string& id) const { + return findLayerRecursive(layers, id); } -std::string PAGXDocument::toXML() const { - return PAGXXMLWriter::Write(this); +void PAGXDocument::rebuildResourceMap() const { + resourceMap.clear(); + for (const auto& resource : resources) { + if (!resource->id.empty()) { + resourceMap[resource->id] = resource.get(); + } + } + resourceMapDirty = false; } -bool PAGXDocument::saveToFile(const std::string& filePath) const { - std::string xml = toXML(); - std::ofstream file(filePath, std::ios::binary); - if (!file.is_open()) { - return false; +LayerNode* PAGXDocument::findLayerRecursive(const std::vector>& layers, + const std::string& id) { + for (const auto& layer : layers) { + if (layer->id == id) { + return layer.get(); + } + auto found = findLayerRecursive(layer->children, id); + if (found) { + return found; + } } - file.write(xml.data(), static_cast(xml.size())); - return file.good(); + return nullptr; } } // namespace pagx diff --git a/pagx/src/PAGXNode.cpp b/pagx/src/PAGXNode.cpp index a3ee022305..db582967a6 100644 --- a/pagx/src/PAGXNode.cpp +++ b/pagx/src/PAGXNode.cpp @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2026 Tencent. All rights reserved. +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -17,349 +17,82 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "pagx/PAGXNode.h" -#include -#include -#include namespace pagx { -const char* PAGXNodeTypeName(PAGXNodeType type) { +const char* NodeTypeName(NodeType type) { switch (type) { - case PAGXNodeType::Document: - return "Document"; - case PAGXNodeType::Resources: - return "Resources"; - case PAGXNodeType::Layer: - return "Layer"; - case PAGXNodeType::Group: - return "Group"; - case PAGXNodeType::Rectangle: + case NodeType::SolidColor: + return "SolidColor"; + case NodeType::LinearGradient: + return "LinearGradient"; + case NodeType::RadialGradient: + return "RadialGradient"; + case NodeType::ConicGradient: + return "ConicGradient"; + case NodeType::DiamondGradient: + return "DiamondGradient"; + case NodeType::ImagePattern: + return "ImagePattern"; + case NodeType::ColorStop: + return "ColorStop"; + case NodeType::Rectangle: return "Rectangle"; - case PAGXNodeType::Ellipse: + case NodeType::Ellipse: return "Ellipse"; - case PAGXNodeType::Polystar: + case NodeType::Polystar: return "Polystar"; - case PAGXNodeType::Path: + case NodeType::Path: return "Path"; - case PAGXNodeType::Text: - return "Text"; - case PAGXNodeType::Fill: + case NodeType::TextSpan: + return "TextSpan"; + case NodeType::Fill: return "Fill"; - case PAGXNodeType::Stroke: + case NodeType::Stroke: return "Stroke"; - case PAGXNodeType::TrimPath: + case NodeType::TrimPath: return "TrimPath"; - case PAGXNodeType::RoundCorner: + case NodeType::RoundCorner: return "RoundCorner"; - case PAGXNodeType::MergePath: + case NodeType::MergePath: return "MergePath"; - case PAGXNodeType::Repeater: + case NodeType::TextModifier: + return "TextModifier"; + case NodeType::TextPath: + return "TextPath"; + case NodeType::TextLayout: + return "TextLayout"; + case NodeType::RangeSelector: + return "RangeSelector"; + case NodeType::Repeater: return "Repeater"; - case PAGXNodeType::SolidColor: - return "SolidColor"; - case PAGXNodeType::LinearGradient: - return "LinearGradient"; - case PAGXNodeType::RadialGradient: - return "RadialGradient"; - case PAGXNodeType::ConicGradient: - return "ConicGradient"; - case PAGXNodeType::DiamondGradient: - return "DiamondGradient"; - case PAGXNodeType::ImagePattern: - return "ImagePattern"; - case PAGXNodeType::BlurFilter: - return "BlurFilter"; - case PAGXNodeType::DropShadowFilter: - return "DropShadowFilter"; - case PAGXNodeType::DropShadowStyle: + case NodeType::Group: + return "Group"; + case NodeType::DropShadowStyle: return "DropShadowStyle"; - case PAGXNodeType::InnerShadowStyle: + case NodeType::InnerShadowStyle: return "InnerShadowStyle"; - case PAGXNodeType::TextSpan: - return "TextSpan"; - case PAGXNodeType::Mask: - return "Mask"; - case PAGXNodeType::ClipPath: - return "ClipPath"; - case PAGXNodeType::Image: + case NodeType::BackgroundBlurStyle: + return "BackgroundBlurStyle"; + case NodeType::BlurFilter: + return "BlurFilter"; + case NodeType::DropShadowFilter: + return "DropShadowFilter"; + case NodeType::InnerShadowFilter: + return "InnerShadowFilter"; + case NodeType::BlendFilter: + return "BlendFilter"; + case NodeType::ColorMatrixFilter: + return "ColorMatrixFilter"; + case NodeType::Image: return "Image"; - case PAGXNodeType::Unknown: + case NodeType::Composition: + return "Composition"; + case NodeType::Layer: + return "Layer"; default: return "Unknown"; } } -PAGXNodeType PAGXNodeTypeFromName(const std::string& name) { - if (name == "Document") - return PAGXNodeType::Document; - if (name == "Resources") - return PAGXNodeType::Resources; - if (name == "Layer") - return PAGXNodeType::Layer; - if (name == "Group") - return PAGXNodeType::Group; - if (name == "Rectangle") - return PAGXNodeType::Rectangle; - if (name == "Ellipse") - return PAGXNodeType::Ellipse; - if (name == "Polystar") - return PAGXNodeType::Polystar; - if (name == "Path") - return PAGXNodeType::Path; - if (name == "Text") - return PAGXNodeType::Text; - if (name == "Fill") - return PAGXNodeType::Fill; - if (name == "Stroke") - return PAGXNodeType::Stroke; - if (name == "TrimPath") - return PAGXNodeType::TrimPath; - if (name == "RoundCorner") - return PAGXNodeType::RoundCorner; - if (name == "MergePath") - return PAGXNodeType::MergePath; - if (name == "Repeater") - return PAGXNodeType::Repeater; - if (name == "SolidColor") - return PAGXNodeType::SolidColor; - if (name == "LinearGradient") - return PAGXNodeType::LinearGradient; - if (name == "RadialGradient") - return PAGXNodeType::RadialGradient; - if (name == "ConicGradient") - return PAGXNodeType::ConicGradient; - if (name == "DiamondGradient") - return PAGXNodeType::DiamondGradient; - if (name == "ImagePattern") - return PAGXNodeType::ImagePattern; - if (name == "BlurFilter") - return PAGXNodeType::BlurFilter; - if (name == "DropShadowFilter") - return PAGXNodeType::DropShadowFilter; - if (name == "DropShadowStyle") - return PAGXNodeType::DropShadowStyle; - if (name == "InnerShadowStyle") - return PAGXNodeType::InnerShadowStyle; - if (name == "TextSpan") - return PAGXNodeType::TextSpan; - if (name == "Mask") - return PAGXNodeType::Mask; - if (name == "ClipPath") - return PAGXNodeType::ClipPath; - if (name == "Image") - return PAGXNodeType::Image; - return PAGXNodeType::Unknown; -} - -std::unique_ptr PAGXNode::Make(PAGXNodeType type) { - return std::unique_ptr(new PAGXNode(type)); -} - -bool PAGXNode::hasAttribute(const std::string& name) const { - return _attributes.find(name) != _attributes.end(); -} - -std::string PAGXNode::getAttribute(const std::string& name, const std::string& defaultValue) const { - auto it = _attributes.find(name); - if (it != _attributes.end()) { - return it->second; - } - return defaultValue; -} - -float PAGXNode::getFloatAttribute(const std::string& name, float defaultValue) const { - auto it = _attributes.find(name); - if (it != _attributes.end()) { - return std::stof(it->second); - } - return defaultValue; -} - -int PAGXNode::getIntAttribute(const std::string& name, int defaultValue) const { - auto it = _attributes.find(name); - if (it != _attributes.end()) { - return std::stoi(it->second); - } - return defaultValue; -} - -bool PAGXNode::getBoolAttribute(const std::string& name, bool defaultValue) const { - auto it = _attributes.find(name); - if (it != _attributes.end()) { - const auto& value = it->second; - return value == "true" || value == "1"; - } - return defaultValue; -} - -Color PAGXNode::getColorAttribute(const std::string& name, const Color& defaultValue) const { - auto it = _attributes.find(name); - if (it != _attributes.end()) { - const auto& value = it->second; - if (value.empty()) { - return defaultValue; - } - // Parse hex color like "#RRGGBB" or "#AARRGGBB" - if (value[0] == '#' && (value.length() == 7 || value.length() == 9)) { - uint32_t hex = std::stoul(value.substr(1), nullptr, 16); - if (value.length() == 7) { - hex = 0xFF000000 | hex; // Add full alpha - } - return Color::FromHex(hex); - } - } - return defaultValue; -} - -Matrix PAGXNode::getMatrixAttribute(const std::string& name) const { - auto it = _attributes.find(name); - if (it != _attributes.end()) { - const auto& value = it->second; - // Parse "scaleX skewX transX skewY scaleY transY" - std::istringstream iss(value); - Matrix matrix = Matrix::Identity(); - iss >> matrix.scaleX >> matrix.skewX >> matrix.transX >> matrix.skewY >> matrix.scaleY >> - matrix.transY; - return matrix; - } - return Matrix::Identity(); -} - -PathData PAGXNode::getPathAttribute(const std::string& name) const { - auto it = _attributes.find(name); - if (it != _attributes.end()) { - return PathData::FromSVGString(it->second); - } - return PathData(); -} - -void PAGXNode::setAttribute(const std::string& name, const std::string& value) { - _attributes[name] = value; -} - -void PAGXNode::setFloatAttribute(const std::string& name, float value) { - std::ostringstream oss; - oss << value; - _attributes[name] = oss.str(); -} - -void PAGXNode::setIntAttribute(const std::string& name, int value) { - _attributes[name] = std::to_string(value); -} - -void PAGXNode::setBoolAttribute(const std::string& name, bool value) { - _attributes[name] = value ? "true" : "false"; -} - -void PAGXNode::setColorAttribute(const std::string& name, const Color& color) { - uint32_t hex = color.toHex(); - std::ostringstream oss; - oss << "#" << std::hex << std::uppercase; - oss.width(8); - oss.fill('0'); - oss << hex; - _attributes[name] = oss.str(); -} - -void PAGXNode::setMatrixAttribute(const std::string& name, const Matrix& matrix) { - std::ostringstream oss; - oss << matrix.scaleX << " " << matrix.skewX << " " << matrix.transX << " " << matrix.skewY << " " - << matrix.scaleY << " " << matrix.transY; - _attributes[name] = oss.str(); -} - -void PAGXNode::setPathAttribute(const std::string& name, const PathData& path) { - _attributes[name] = path.toSVGString(); -} - -void PAGXNode::removeAttribute(const std::string& name) { - _attributes.erase(name); -} - -std::vector PAGXNode::getAttributeNames() const { - std::vector names; - names.reserve(_attributes.size()); - for (const auto& pair : _attributes) { - names.push_back(pair.first); - } - return names; -} - -PAGXNode* PAGXNode::childAt(size_t index) const { - if (index < _children.size()) { - return _children[index].get(); - } - return nullptr; -} - -void PAGXNode::appendChild(std::unique_ptr child) { - if (child) { - child->_parent = this; - _children.push_back(std::move(child)); - } -} - -void PAGXNode::insertChild(size_t index, std::unique_ptr child) { - if (child) { - child->_parent = this; - if (index >= _children.size()) { - _children.push_back(std::move(child)); - } else { - _children.insert(_children.begin() + static_cast(index), std::move(child)); - } - } -} - -std::unique_ptr PAGXNode::removeChild(size_t index) { - if (index < _children.size()) { - auto child = std::move(_children[index]); - _children.erase(_children.begin() + static_cast(index)); - child->_parent = nullptr; - return child; - } - return nullptr; -} - -void PAGXNode::clearChildren() { - for (auto& child : _children) { - child->_parent = nullptr; - } - _children.clear(); -} - -PAGXNode* PAGXNode::findById(const std::string& id) { - if (_id == id) { - return this; - } - for (auto& child : _children) { - auto found = child->findById(id); - if (found) { - return found; - } - } - return nullptr; -} - -std::vector PAGXNode::findByType(PAGXNodeType type) { - std::vector result; - if (_type == type) { - result.push_back(this); - } - for (auto& child : _children) { - auto childResults = child->findByType(type); - result.insert(result.end(), childResults.begin(), childResults.end()); - } - return result; -} - -std::unique_ptr PAGXNode::clone() const { - auto copy = PAGXNode::Make(_type); - copy->_id = _id; - copy->_attributes = _attributes; - for (const auto& child : _children) { - copy->appendChild(child->clone()); - } - return copy; -} - } // namespace pagx diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp new file mode 100644 index 0000000000..b76d662c16 --- /dev/null +++ b/pagx/src/PAGXTypes.cpp @@ -0,0 +1,356 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/PAGXTypes.h" +#include +#include +#include +#include + +namespace pagx { + +//============================================================================== +// Color implementation +//============================================================================== + +Color Color::FromHex(uint32_t hex, bool hasAlpha) { + Color color = {}; + if (hasAlpha) { + color.red = static_cast((hex >> 24) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.blue = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.alpha = static_cast(hex & 0xFF) / 255.0f; + } else { + color.red = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.blue = static_cast(hex & 0xFF) / 255.0f; + color.alpha = 1.0f; + } + return color; +} + +Color Color::FromRGBA(float r, float g, float b, float a) { + return {r, g, b, a}; +} + +static int ParseHexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return 0; +} + +Color Color::Parse(const std::string& str) { + if (str.empty()) { + return {}; + } + if (str[0] == '#') { + auto hex = str.substr(1); + if (hex.size() == 3) { + int r = ParseHexDigit(hex[0]); + int g = ParseHexDigit(hex[1]); + int b = ParseHexDigit(hex[2]); + return Color::FromRGBA(static_cast(r * 17) / 255.0f, + static_cast(g * 17) / 255.0f, + static_cast(b * 17) / 255.0f, 1.0f); + } + if (hex.size() == 6) { + int r = ParseHexDigit(hex[0]) * 16 + ParseHexDigit(hex[1]); + int g = ParseHexDigit(hex[2]) * 16 + ParseHexDigit(hex[3]); + int b = ParseHexDigit(hex[4]) * 16 + ParseHexDigit(hex[5]); + return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, + static_cast(b) / 255.0f, 1.0f); + } + if (hex.size() == 8) { + int r = ParseHexDigit(hex[0]) * 16 + ParseHexDigit(hex[1]); + int g = ParseHexDigit(hex[2]) * 16 + ParseHexDigit(hex[3]); + int b = ParseHexDigit(hex[4]) * 16 + ParseHexDigit(hex[5]); + int a = ParseHexDigit(hex[6]) * 16 + ParseHexDigit(hex[7]); + return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, + static_cast(b) / 255.0f, static_cast(a) / 255.0f); + } + } + if (str.substr(0, 4) == "rgb(" || str.substr(0, 5) == "rgba(") { + auto start = str.find('('); + auto end = str.find(')'); + if (start != std::string::npos && end != std::string::npos) { + auto values = str.substr(start + 1, end - start - 1); + std::istringstream iss(values); + std::string token = {}; + std::vector components = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + components.push_back(std::stof(trimmed)); + } + if (components.size() >= 3) { + float r = components[0] / 255.0f; + float g = components[1] / 255.0f; + float b = components[2] / 255.0f; + float a = components.size() >= 4 ? components[3] : 1.0f; + return Color::FromRGBA(r, g, b, a); + } + } + } + return {}; +} + +std::string Color::toHexString(bool includeAlpha) const { + auto toHex = [](float v) { + int i = static_cast(std::round(v * 255.0f)); + i = std::max(0, std::min(255, i)); + char buf[3] = {}; + snprintf(buf, sizeof(buf), "%02X", i); + return std::string(buf); + }; + std::string result = "#" + toHex(red) + toHex(green) + toHex(blue); + if (includeAlpha && alpha < 1.0f) { + result += toHex(alpha); + } + return result; +} + +//============================================================================== +// Matrix implementation +//============================================================================== + +Matrix Matrix::Translate(float x, float y) { + Matrix m = {}; + m.tx = x; + m.ty = y; + return m; +} + +Matrix Matrix::Scale(float sx, float sy) { + Matrix m = {}; + m.a = sx; + m.d = sy; + return m; +} + +Matrix Matrix::Rotate(float degrees) { + Matrix m = {}; + float radians = degrees * 3.14159265358979323846f / 180.0f; + float cosVal = std::cos(radians); + float sinVal = std::sin(radians); + m.a = cosVal; + m.b = sinVal; + m.c = -sinVal; + m.d = cosVal; + return m; +} + +Matrix Matrix::Parse(const std::string& str) { + Matrix m = {}; + std::istringstream iss(str); + std::string token = {}; + std::vector values = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + if (!trimmed.empty()) { + values.push_back(std::stof(trimmed)); + } + } + if (values.size() >= 6) { + m.a = values[0]; + m.b = values[1]; + m.c = values[2]; + m.d = values[3]; + m.tx = values[4]; + m.ty = values[5]; + } + return m; +} + +std::string Matrix::toString() const { + std::ostringstream oss = {}; + oss << a << "," << b << "," << c << "," << d << "," << tx << "," << ty; + return oss.str(); +} + +Matrix Matrix::operator*(const Matrix& other) const { + Matrix result = {}; + result.a = a * other.a + c * other.b; + result.b = b * other.a + d * other.b; + result.c = a * other.c + c * other.d; + result.d = b * other.c + d * other.d; + result.tx = a * other.tx + c * other.ty + tx; + result.ty = b * other.tx + d * other.ty + ty; + return result; +} + +Point Matrix::mapPoint(const Point& point) const { + return {a * point.x + c * point.y + tx, b * point.x + d * point.y + ty}; +} + +//============================================================================== +// Enum string conversions +//============================================================================== + +#define DEFINE_ENUM_CONVERSION(EnumType, ...) \ + static const std::unordered_map EnumType##ToStringMap = {__VA_ARGS__}; \ + static const std::unordered_map StringTo##EnumType##Map = [] { \ + std::unordered_map map = {}; \ + for (const auto& pair : EnumType##ToStringMap) { \ + map[pair.second] = pair.first; \ + } \ + return map; \ + }(); \ + std::string EnumType##ToString(EnumType value) { \ + auto it = EnumType##ToStringMap.find(value); \ + return it != EnumType##ToStringMap.end() ? it->second : ""; \ + } \ + EnumType EnumType##FromString(const std::string& str) { \ + auto it = StringTo##EnumType##Map.find(str); \ + return it != StringTo##EnumType##Map.end() ? it->second : EnumType##ToStringMap.begin()->first; \ + } + +DEFINE_ENUM_CONVERSION(BlendMode, + {BlendMode::Normal, "normal"}, + {BlendMode::Multiply, "multiply"}, + {BlendMode::Screen, "screen"}, + {BlendMode::Overlay, "overlay"}, + {BlendMode::Darken, "darken"}, + {BlendMode::Lighten, "lighten"}, + {BlendMode::ColorDodge, "colorDodge"}, + {BlendMode::ColorBurn, "colorBurn"}, + {BlendMode::HardLight, "hardLight"}, + {BlendMode::SoftLight, "softLight"}, + {BlendMode::Difference, "difference"}, + {BlendMode::Exclusion, "exclusion"}, + {BlendMode::Hue, "hue"}, + {BlendMode::Saturation, "saturation"}, + {BlendMode::Color, "color"}, + {BlendMode::Luminosity, "luminosity"}, + {BlendMode::Add, "add"}) + +DEFINE_ENUM_CONVERSION(LineCap, + {LineCap::Butt, "butt"}, + {LineCap::Round, "round"}, + {LineCap::Square, "square"}) + +DEFINE_ENUM_CONVERSION(LineJoin, + {LineJoin::Miter, "miter"}, + {LineJoin::Round, "round"}, + {LineJoin::Bevel, "bevel"}) + +DEFINE_ENUM_CONVERSION(FillRule, + {FillRule::Winding, "winding"}, + {FillRule::EvenOdd, "evenOdd"}) + +DEFINE_ENUM_CONVERSION(StrokeAlign, + {StrokeAlign::Center, "center"}, + {StrokeAlign::Inside, "inside"}, + {StrokeAlign::Outside, "outside"}) + +DEFINE_ENUM_CONVERSION(Placement, + {Placement::Background, "background"}, + {Placement::Foreground, "foreground"}) + +DEFINE_ENUM_CONVERSION(TileMode, + {TileMode::Clamp, "clamp"}, + {TileMode::Repeat, "repeat"}, + {TileMode::Mirror, "mirror"}, + {TileMode::Decal, "decal"}) + +DEFINE_ENUM_CONVERSION(SamplingMode, + {SamplingMode::Nearest, "nearest"}, + {SamplingMode::Linear, "linear"}, + {SamplingMode::Mipmap, "mipmap"}) + +DEFINE_ENUM_CONVERSION(MaskType, + {MaskType::Alpha, "alpha"}, + {MaskType::Luminance, "luminance"}, + {MaskType::Contour, "contour"}) + +DEFINE_ENUM_CONVERSION(PolystarType, + {PolystarType::Polygon, "polygon"}, + {PolystarType::Star, "star"}) + +DEFINE_ENUM_CONVERSION(TrimType, + {TrimType::Separate, "separate"}, + {TrimType::Continuous, "continuous"}) + +DEFINE_ENUM_CONVERSION(PathOp, + {PathOp::Append, "append"}, + {PathOp::Union, "union"}, + {PathOp::Intersect, "intersect"}, + {PathOp::Xor, "xor"}, + {PathOp::Difference, "difference"}) + +DEFINE_ENUM_CONVERSION(TextAlign, + {TextAlign::Left, "left"}, + {TextAlign::Center, "center"}, + {TextAlign::Right, "right"}, + {TextAlign::Justify, "justify"}) + +DEFINE_ENUM_CONVERSION(VerticalAlign, + {VerticalAlign::Top, "top"}, + {VerticalAlign::Center, "center"}, + {VerticalAlign::Bottom, "bottom"}) + +DEFINE_ENUM_CONVERSION(Overflow, + {Overflow::Clip, "clip"}, + {Overflow::Visible, "visible"}, + {Overflow::Ellipsis, "ellipsis"}) + +DEFINE_ENUM_CONVERSION(FontStyle, + {FontStyle::Normal, "normal"}, + {FontStyle::Italic, "italic"}) + +DEFINE_ENUM_CONVERSION(TextPathAlign, + {TextPathAlign::Start, "start"}, + {TextPathAlign::Center, "center"}, + {TextPathAlign::End, "end"}) + +DEFINE_ENUM_CONVERSION(SelectorUnit, + {SelectorUnit::Index, "index"}, + {SelectorUnit::Percentage, "percentage"}) + +DEFINE_ENUM_CONVERSION(SelectorShape, + {SelectorShape::Square, "square"}, + {SelectorShape::RampUp, "rampUp"}, + {SelectorShape::RampDown, "rampDown"}, + {SelectorShape::Triangle, "triangle"}, + {SelectorShape::Round, "round"}, + {SelectorShape::Smooth, "smooth"}) + +DEFINE_ENUM_CONVERSION(SelectorMode, + {SelectorMode::Add, "add"}, + {SelectorMode::Subtract, "subtract"}, + {SelectorMode::Intersect, "intersect"}, + {SelectorMode::Min, "min"}, + {SelectorMode::Max, "max"}, + {SelectorMode::Difference, "difference"}) + +DEFINE_ENUM_CONVERSION(RepeaterOrder, + {RepeaterOrder::BelowOriginal, "belowOriginal"}, + {RepeaterOrder::AboveOriginal, "aboveOriginal"}) + +#undef DEFINE_ENUM_CONVERSION + +} // namespace pagx diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 4f0aa383fb..54280b1be2 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2026 Tencent. All rights reserved. +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -17,306 +17,1107 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "PAGXXMLParser.h" -#include #include -#include +#include namespace pagx { -// Simple XML tokenizer and parser (no external dependencies) +//============================================================================== +// Simple XML tokenizer +//============================================================================== + class XMLTokenizer { public: - XMLTokenizer(const char* data, size_t length) : _data(data), _length(length), _pos(0) { + XMLTokenizer(const uint8_t* data, size_t length) + : data(reinterpret_cast(data)), length(length) { + } + + std::unique_ptr parse() { + skipWhitespace(); + skipXMLDeclaration(); + skipWhitespace(); + return parseElement(); + } + + private: + const char* data = nullptr; + size_t length = 0; + size_t pos = 0; + + char peek() const { + return pos < length ? data[pos] : '\0'; } - bool isEnd() const { - return _pos >= _length; + char get() { + return pos < length ? data[pos++] : '\0'; } void skipWhitespace() { - while (_pos < _length && std::isspace(_data[_pos])) { - ++_pos; + while (pos < length && (data[pos] == ' ' || data[pos] == '\t' || data[pos] == '\n' || + data[pos] == '\r')) { + pos++; } } - bool match(char c) { - if (_pos < _length && _data[_pos] == c) { - ++_pos; - return true; + void skipXMLDeclaration() { + if (pos + 5 < length && strncmp(data + pos, "')) { + pos++; + } + if (pos + 1 < length) { + pos += 2; + } } - return false; } - bool matchString(const char* str) { - size_t len = std::strlen(str); - if (_pos + len <= _length && std::strncmp(_data + _pos, str, len) == 0) { - _pos += len; - return true; + void skipComment() { + if (pos + 4 < length && strncmp(data + pos, " - + @@ -1055,7 +1045,7 @@ y = centerY + outerRadius * sin(angle) - + @@ -1177,14 +1167,14 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 将所有形状合并为单个形状。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `op` | PathOp | append | 合并操作(见下方) | +| `mode` | MergePathOp | append | 合并操作(见下方) | -**PathOp(路径合并操作)**: +**MergePathOp(路径合并操作)**: | 值 | 说明 | |------|------| @@ -1204,7 +1194,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: - + ``` @@ -1275,14 +1265,14 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 对选定范围内的字形应用变换和样式覆盖。 ```xml - - + + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `anchor` | point | 0.5,0.5 | 锚点(归一化) | +| `anchorPoint` | point | 0.5,0.5 | 锚点(归一化) | | `position` | point | 0,0 | 位置偏移 | | `rotation` | float | 0 | 旋转 | | `scale` | point | 1,1 | 缩放 | @@ -1306,11 +1296,11 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: factor = clamp(selectorFactor × weight, -1, 1) // 位置和旋转:线性应用 factor -transform = translate(-anchor × factor) +transform = translate(-anchorPoint × factor) × scale(1 + (scale - 1) × factor) // 缩放从 1 插值到目标值 × skew(skew × factor, skewAxis) × rotate(rotation × factor) - × translate(anchor × factor) + × translate(anchorPoint × factor) × translate(position × factor) // 透明度:使用 factor 的绝对值 @@ -1332,7 +1322,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 范围选择器定义 TextModifier 影响的字形范围和影响程度。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1346,8 +1336,8 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | `easeOut` | float | 0 | 缓出量 | | `mode` | SelectorMode | add | 组合模式(见下方) | | `weight` | float | 1 | 选择器权重 | -| `randomize` | bool | false | 随机顺序 | -| `seed` | int | 0 | 随机种子 | +| `randomizeOrder` | bool | false | 随机顺序 | +| `randomSeed` | int | 0 | 随机种子 | **SelectorUnit(单位)**: @@ -1490,7 +1480,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 复制累积的内容和已渲染的样式,对每个副本应用渐进变换。Repeater 对 Path 和字形列表同时生效,且不会触发文本转形状。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1498,7 +1488,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | `copies` | float | 3 | 副本数 | | `offset` | float | 0 | 起始偏移 | | `order` | RepeaterOrder | belowOriginal | 堆叠顺序(见下方) | -| `anchor` | point | 0,0 | 锚点 | +| `anchorPoint` | point | 0,0 | 锚点 | | `position` | point | 100,100 | 每个副本的位置偏移 | | `rotation` | float | 0 | 每个副本的旋转 | | `scale` | point | 1,1 | 每个副本的缩放 | @@ -1508,11 +1498,11 @@ finalColor = blend(originalColor, overrideColor, blendFactor) **变换计算**(第 i 个副本,i 从 0 开始): ``` progress = i + offset -matrix = translate(-anchor) +matrix = translate(-anchorPoint) × scale(scale^progress) // 指数缩放 × rotate(rotation × progress) // 线性旋转 × translate(position × progress) // 线性位移 - × translate(anchor) + × translate(anchorPoint) ``` **透明度插值**: @@ -1564,7 +1554,7 @@ alpha = lerp(startAlpha, endAlpha, t) Group 是带变换属性的矢量元素容器。 ```xml - + ``` @@ -1572,7 +1562,7 @@ Group 是带变换属性的矢量元素容器。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `name` | string | "" | 组名称 | -| `anchor` | point | 0,0 | 锚点 "x,y" | +| `anchorPoint` | point | 0,0 | 锚点 "x,y" | | `position` | point | 0,0 | 位置 "x,y" | | `rotation` | float | 0 | 旋转角度 | | `scale` | point | 1,1 | 缩放 "sx,sy" | @@ -1584,7 +1574,7 @@ Group 是带变换属性的矢量元素容器。 变换按以下顺序应用(后应用的变换先计算): -1. 平移到锚点的负方向(`translate(-anchor)`) +1. 平移到锚点的负方向(`translate(-anchorPoint)`) 2. 缩放(`scale`) 3. 倾斜(`skew` 沿 `skewAxis` 方向) 4. 旋转(`rotation`) @@ -1592,7 +1582,7 @@ Group 是带变换属性的矢量元素容器。 **变换矩阵**: ``` -M = translate(position) × rotate(rotation) × skew(skew, skewAxis) × scale(scale) × translate(-anchor) +M = translate(position) × rotate(rotation) × skew(skew, skewAxis) × scale(scale) × translate(-anchorPoint) ``` **倾斜变换**: @@ -1655,7 +1645,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: **示例 4 - 多重填充**: ```xml - + @@ -1672,9 +1662,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: **示例 6 - 混合叠加**: ```xml - + - + @@ -1717,7 +1707,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - + @@ -1725,7 +1715,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - @@ -1736,7 +1726,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - + @@ -1744,7 +1734,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - + @@ -1764,14 +1754,14 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - + - + From 0bc6ffaefc336e64f0a1982a853292c3f8e67233 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 19:38:01 +0800 Subject: [PATCH 010/678] Replace handwritten XML tokenizer with expat-based DOM parser for PAGX SVG parsing. --- DEPS | 5 + pagx/CMakeLists.txt | 22 ++ pagx/src/svg/PAGXSVGParser.cpp | 623 ++++++++++--------------------- pagx/src/svg/SVGParserInternal.h | 65 ++-- pagx/src/xml/XMLDOM.cpp | 226 +++++++++++ pagx/src/xml/XMLDOM.h | 121 ++++++ pagx/src/xml/XMLParser.cpp | 182 +++++++++ pagx/src/xml/XMLParser.h | 93 +++++ 8 files changed, 872 insertions(+), 465 deletions(-) create mode 100644 pagx/src/xml/XMLDOM.cpp create mode 100644 pagx/src/xml/XMLDOM.h create mode 100644 pagx/src/xml/XMLParser.cpp create mode 100644 pagx/src/xml/XMLParser.h diff --git a/DEPS b/DEPS index 0f9f1ab3ec..d71afe767e 100644 --- a/DEPS +++ b/DEPS @@ -34,6 +34,11 @@ "url": "https://github.com/lz4/lz4.git", "commit": "cacca37747572717ceb1f156eb9840644205ca4f", "dir": "third_party/lz4" + }, + { + "url": "https://github.com/libexpat/libexpat.git", + "commit": "88b3ed553d8ad335559254863a33360d55b9f1d6", + "dir": "third_party/expat" } ] }, diff --git a/pagx/CMakeLists.txt b/pagx/CMakeLists.txt index 0fcf21ea5c..f1207759aa 100644 --- a/pagx/CMakeLists.txt +++ b/pagx/CMakeLists.txt @@ -20,6 +20,10 @@ file(GLOB PAGX_CORE_SOURCES ) list(APPEND PAGX_SOURCES ${PAGX_CORE_SOURCES}) +# XML parser sources (based on expat) +file(GLOB PAGX_XML_SOURCES src/xml/*.cpp) +list(APPEND PAGX_SOURCES ${PAGX_XML_SOURCES}) + # SVG parser (also independent of tgfx) if (PAGX_BUILD_SVG) file(GLOB PAGX_SVG_SOURCES src/svg/*.cpp) @@ -47,6 +51,24 @@ target_include_directories(pagx PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ) +# expat include directory (use tgfx's expat if available, otherwise use libpag's) +if (TARGET tgfx) + # Use tgfx's expat directory + target_include_directories(pagx PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/tgfx/third_party/expat + ) +else() + # Use libpag's expat directory + target_include_directories(pagx PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/expat + ) +endif() + +# Windows requires XML_STATIC definition for static expat +if(WIN32) + target_compile_definitions(pagx PRIVATE XML_STATIC) +endif() + if (PAGX_BUILD_SVG) target_include_directories(pagx PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/svg diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 8251802033..313017defa 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -19,25 +19,17 @@ #include "pagx/PAGXSVGParser.h" #include #include -#include #include #include #include "SVGParserInternal.h" +#include "xml/XMLDOM.h" namespace pagx { std::shared_ptr PAGXSVGParser::Parse(const std::string& filePath, const Options& options) { - std::ifstream file(filePath, std::ios::binary); - if (!file.is_open()) { - return nullptr; - } - - std::stringstream buffer; - buffer << file.rdbuf(); - std::string content = buffer.str(); - - auto doc = Parse(reinterpret_cast(content.data()), content.size(), options); + SVGParserImpl parser(options); + auto doc = parser.parseFile(filePath); if (doc) { auto lastSlash = filePath.find_last_of("/\\"); if (lastSlash != std::string::npos) { @@ -63,308 +55,45 @@ std::shared_ptr PAGXSVGParser::ParseString(const std::string& svgC SVGParserImpl::SVGParserImpl(const PAGXSVGParser::Options& options) : _options(options) { } -// Simple XML parser for SVG -class SVGXMLTokenizer { - public: - SVGXMLTokenizer(const char* data, size_t length) : _data(data), _length(length), _pos(0) { - } - - bool isEnd() const { - return _pos >= _length; - } - - void skipWhitespace() { - while (_pos < _length && std::isspace(_data[_pos])) { - ++_pos; - } - } - - bool match(char c) { - if (_pos < _length && _data[_pos] == c) { - ++_pos; - return true; - } - return false; - } - - bool matchString(const char* str) { - size_t len = std::strlen(str); - if (_pos + len <= _length && std::strncmp(_data + _pos, str, len) == 0) { - _pos += len; - return true; - } - return false; - } - - std::string readUntil(char delimiter) { - std::string result; - while (_pos < _length && _data[_pos] != delimiter) { - result += _data[_pos++]; - } - return result; - } - - std::string readUntilAny(const char* delimiters) { - std::string result; - while (_pos < _length) { - bool isDelim = false; - for (const char* d = delimiters; *d; ++d) { - if (_data[_pos] == *d) { - isDelim = true; - break; - } - } - if (isDelim) { - break; - } - result += _data[_pos++]; - } - return result; - } - - std::string readName() { - std::string result; - while (_pos < _length && - (std::isalnum(_data[_pos]) || _data[_pos] == '_' || _data[_pos] == '-' || - _data[_pos] == ':')) { - result += _data[_pos++]; - } - return result; - } - - std::string readQuotedString() { - char quote = _data[_pos++]; - std::string result; - while (_pos < _length && _data[_pos] != quote) { - if (_data[_pos] == '&') { - result += readEscapeSequence(); - } else { - result += _data[_pos++]; - } - } - if (_pos < _length) { - ++_pos; - } - return result; - } - - std::string readEscapeSequence() { - std::string seq; - ++_pos; - while (_pos < _length && _data[_pos] != ';') { - seq += _data[_pos++]; - } - if (_pos < _length) { - ++_pos; - } - if (seq == "lt") - return "<"; - if (seq == "gt") - return ">"; - if (seq == "amp") - return "&"; - if (seq == "quot") - return "\""; - if (seq == "apos") - return "'"; - if (!seq.empty() && seq[0] == '#') { - int code = 0; - if (seq.length() > 1 && seq[1] == 'x') { - code = std::stoi(seq.substr(2), nullptr, 16); - } else { - code = std::stoi(seq.substr(1)); - } - if (code > 0 && code < 128) { - return std::string(1, static_cast(code)); - } - } - return "&" + seq + ";"; - } - - std::string readTextContent() { - std::string result; - while (_pos < _length && _data[_pos] != '<') { - if (_data[_pos] == '&') { - result += readEscapeSequence(); - } else { - result += _data[_pos++]; - } - } - return result; - } - - void skipComment() { - while (_pos + 2 < _length) { - if (_data[_pos] == '-' && _data[_pos + 1] == '-' && _data[_pos + 2] == '>') { - _pos += 3; - return; - } - ++_pos; - } - } - - void skipCDATA() { - while (_pos + 2 < _length) { - if (_data[_pos] == ']' && _data[_pos + 1] == ']' && _data[_pos + 2] == '>') { - _pos += 3; - return; - } - ++_pos; - } - } - - void skipProcessingInstruction() { - while (_pos + 1 < _length) { - if (_data[_pos] == '?' && _data[_pos + 1] == '>') { - _pos += 2; - return; - } - ++_pos; - } - } - - private: - const char* _data = nullptr; - size_t _length = 0; - size_t _pos = 0; -}; - -static std::unique_ptr ParseSVGXMLElement(SVGXMLTokenizer& tokenizer); - -static void ParseSVGXMLAttributes(SVGXMLTokenizer& tokenizer, SVGXMLNode* node) { - while (true) { - tokenizer.skipWhitespace(); - if (tokenizer.isEnd()) { - break; - } - - std::string name = tokenizer.readName(); - if (name.empty()) { - break; - } - - tokenizer.skipWhitespace(); - if (!tokenizer.match('=')) { - break; - } - - tokenizer.skipWhitespace(); - std::string value = tokenizer.readQuotedString(); - node->attributes[name] = value; - } -} - -static std::unique_ptr ParseSVGXMLElement(SVGXMLTokenizer& tokenizer) { - tokenizer.skipWhitespace(); - - if (tokenizer.isEnd() || !tokenizer.match('<')) { - return nullptr; - } - - while (true) { - if (tokenizer.matchString("!--")) { - tokenizer.skipComment(); - tokenizer.skipWhitespace(); - if (!tokenizer.match('<')) { - return nullptr; - } - } else if (tokenizer.matchString("![CDATA[")) { - tokenizer.skipCDATA(); - tokenizer.skipWhitespace(); - if (!tokenizer.match('<')) { - return nullptr; - } - } else if (tokenizer.matchString("?")) { - tokenizer.skipProcessingInstruction(); - tokenizer.skipWhitespace(); - if (!tokenizer.match('<')) { - return nullptr; - } - } else if (tokenizer.matchString("!DOCTYPE")) { - tokenizer.readUntil('>'); - tokenizer.match('>'); - tokenizer.skipWhitespace(); - if (!tokenizer.match('<')) { - return nullptr; - } - } else { - break; - } - } - - if (tokenizer.match('/')) { +std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { + if (!data || length == 0) { return nullptr; } - std::string tagName = tokenizer.readName(); - if (tagName.empty()) { + auto dom = DOM::Make(data, length); + if (!dom) { return nullptr; } - auto node = std::make_unique(); - node->tagName = tagName; - - ParseSVGXMLAttributes(tokenizer, node.get()); - - tokenizer.skipWhitespace(); - - if (tokenizer.match('/')) { - tokenizer.match('>'); - return node; - } - - if (!tokenizer.match('>')) { - return node; - } - - while (true) { - std::string text = tokenizer.readTextContent(); - if (!text.empty()) { - // Trim whitespace - size_t start = text.find_first_not_of(" \t\n\r"); - size_t end = text.find_last_not_of(" \t\n\r"); - if (start != std::string::npos && end != std::string::npos) { - node->textContent += text.substr(start, end - start + 1); - } - } - - if (tokenizer.isEnd()) { - break; - } - - auto child = ParseSVGXMLElement(tokenizer); - if (!child) { - tokenizer.skipWhitespace(); - tokenizer.readUntil('>'); - tokenizer.match('>'); - break; - } + return parseDOM(dom); +} - node->children.push_back(std::move(child)); +std::shared_ptr SVGParserImpl::parseFile(const std::string& filePath) { + auto dom = DOM::MakeFromFile(filePath); + if (!dom) { + return nullptr; } - return node; + return parseDOM(dom); } -std::unique_ptr SVGParserImpl::parseXML(const char* data, size_t length) { - SVGXMLTokenizer tokenizer(data, length); - return ParseSVGXMLElement(tokenizer); +std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, + const std::string& name, + const std::string& defaultValue) const { + auto [found, value] = node->findAttribute(name); + return found ? value : defaultValue; } -std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { - if (!data || length == 0) { +std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& dom) { + auto root = dom->getRootNode(); + if (!root || root->name != "svg") { return nullptr; } - auto xmlRoot = parseXML(reinterpret_cast(data), length); - if (!xmlRoot || xmlRoot->tagName != "svg") { - return nullptr; - } - - // Parse viewBox and dimensions - auto viewBox = parseViewBox(xmlRoot->getAttribute("viewBox")); - float width = parseLength(xmlRoot->getAttribute("width"), 0); - float height = parseLength(xmlRoot->getAttribute("height"), 0); + // Parse viewBox and dimensions. + auto viewBox = parseViewBox(getAttribute(root, "viewBox")); + float width = parseLength(getAttribute(root, "width"), 0); + float height = parseLength(getAttribute(root, "height"), 0); if (viewBox.size() >= 4) { _viewBoxWidth = viewBox[2]; @@ -386,46 +115,51 @@ std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t l _document = PAGXDocument::Make(width, height); - // First pass: collect defs - for (auto& child : xmlRoot->children) { - if (child->tagName == "defs") { - parseDefs(child.get()); + // First pass: collect defs. + auto child = root->getFirstChild(); + while (child) { + if (child->name == "defs") { + parseDefs(child); } + child = child->getNextSibling(); } - // Second pass: convert elements to a root layer + // Second pass: convert elements to a root layer. auto rootLayer = std::make_unique(); rootLayer->name = "root"; - for (auto& child : xmlRoot->children) { - if (child->tagName == "defs") { - continue; - } - auto layer = convertToLayer(child.get()); - if (layer) { - rootLayer->children.push_back(std::move(layer)); + child = root->getFirstChild(); + while (child) { + if (child->name != "defs") { + auto layer = convertToLayer(child); + if (layer) { + rootLayer->children.push_back(std::move(layer)); + } } + child = child->getNextSibling(); } _document->layers.push_back(std::move(rootLayer)); return _document; } -void SVGParserImpl::parseDefs(SVGXMLNode* defsNode) { - for (auto& child : defsNode->children) { - std::string id = child->getAttribute("id"); +void SVGParserImpl::parseDefs(const std::shared_ptr& defsNode) { + auto child = defsNode->getFirstChild(); + while (child) { + std::string id = getAttribute(child, "id"); if (!id.empty()) { - _defs[id] = child.get(); + _defs[id] = child; } - // Also handle nested defs - if (child->tagName == "defs") { - parseDefs(child.get()); + // Also handle nested defs. + if (child->name == "defs") { + parseDefs(child); } + child = child->getNextSibling(); } } -std::unique_ptr SVGParserImpl::convertToLayer(SVGXMLNode* element) { - const auto& tag = element->tagName; +std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element) { + const auto& tag = element->name; if (tag == "defs" || tag == "linearGradient" || tag == "radialGradient" || tag == "pattern" || tag == "mask" || tag == "clipPath" || tag == "filter") { @@ -434,48 +168,50 @@ std::unique_ptr SVGParserImpl::convertToLayer(SVGXMLNode* element) { auto layer = std::make_unique(); - // Parse common layer attributes - layer->id = element->getAttribute("id"); - layer->name = element->getAttribute("id"); + // Parse common layer attributes. + layer->id = getAttribute(element, "id"); + layer->name = getAttribute(element, "id"); - std::string transform = element->getAttribute("transform"); + std::string transform = getAttribute(element, "transform"); if (!transform.empty()) { layer->matrix = parseTransform(transform); } - std::string opacity = element->getAttribute("opacity"); + std::string opacity = getAttribute(element, "opacity"); if (!opacity.empty()) { layer->alpha = std::stof(opacity); } - std::string display = element->getAttribute("display"); + std::string display = getAttribute(element, "display"); if (display == "none") { layer->visible = false; } - std::string visibility = element->getAttribute("visibility"); + std::string visibility = getAttribute(element, "visibility"); if (visibility == "hidden") { layer->visible = false; } - // Convert contents + // Convert contents. if (tag == "g" || tag == "svg") { - // Group: convert children as child layers - for (auto& child : element->children) { - auto childLayer = convertToLayer(child.get()); + // Group: convert children as child layers. + auto child = element->getFirstChild(); + while (child) { + auto childLayer = convertToLayer(child); if (childLayer) { layer->children.push_back(std::move(childLayer)); } + child = child->getNextSibling(); } } else { - // Shape element: convert to vector contents + // Shape element: convert to vector contents. convertChildren(element, layer->contents); } return layer; } -void SVGParserImpl::convertChildren(SVGXMLNode* element, +void SVGParserImpl::convertChildren(const std::shared_ptr& element, std::vector>& contents) { auto shapeElement = convertElement(element); if (shapeElement) { @@ -485,8 +221,9 @@ void SVGParserImpl::convertChildren(SVGXMLNode* element, addFillStroke(element, contents); } -std::unique_ptr SVGParserImpl::convertElement(SVGXMLNode* element) { - const auto& tag = element->tagName; +std::unique_ptr SVGParserImpl::convertElement( + const std::shared_ptr& element) { + const auto& tag = element->name; if (tag == "rect") { return convertRect(element); @@ -509,43 +246,46 @@ std::unique_ptr SVGParserImpl::convertElement(SVGXMLNode* ele return nullptr; } -std::unique_ptr SVGParserImpl::convertG(SVGXMLNode* element) { +std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& element) { auto group = std::make_unique(); - group->name = element->getAttribute("id"); + group->name = getAttribute(element, "id"); - std::string transform = element->getAttribute("transform"); + std::string transform = getAttribute(element, "transform"); if (!transform.empty()) { - // For GroupNode, we need to decompose the matrix into position/rotation/scale - // For simplicity, just store as position offset for translation + // For GroupNode, we need to decompose the matrix into position/rotation/scale. + // For simplicity, just store as position offset for translation. Matrix m = parseTransform(transform); group->position = {m.tx, m.ty}; - // Note: Full matrix decomposition would be more complex + // Note: Full matrix decomposition would be more complex. } - std::string opacity = element->getAttribute("opacity"); + std::string opacity = getAttribute(element, "opacity"); if (!opacity.empty()) { group->alpha = std::stof(opacity); } - for (auto& child : element->children) { - auto childElement = convertElement(child.get()); + auto child = element->getFirstChild(); + while (child) { + auto childElement = convertElement(child); if (childElement) { group->elements.push_back(std::move(childElement)); } - addFillStroke(child.get(), group->elements); + addFillStroke(child, group->elements); + child = child->getNextSibling(); } return group; } -std::unique_ptr SVGParserImpl::convertRect(SVGXMLNode* element) { - float x = parseLength(element->getAttribute("x"), _viewBoxWidth); - float y = parseLength(element->getAttribute("y"), _viewBoxHeight); - float width = parseLength(element->getAttribute("width"), _viewBoxWidth); - float height = parseLength(element->getAttribute("height"), _viewBoxHeight); - float rx = parseLength(element->getAttribute("rx"), _viewBoxWidth); - float ry = parseLength(element->getAttribute("ry"), _viewBoxHeight); +std::unique_ptr SVGParserImpl::convertRect( + const std::shared_ptr& element) { + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); + float width = parseLength(getAttribute(element, "width"), _viewBoxWidth); + float height = parseLength(getAttribute(element, "height"), _viewBoxHeight); + float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); + float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); if (ry == 0) { ry = rx; @@ -561,10 +301,11 @@ std::unique_ptr SVGParserImpl::convertRect(SVGXMLNode* elemen return rect; } -std::unique_ptr SVGParserImpl::convertCircle(SVGXMLNode* element) { - float cx = parseLength(element->getAttribute("cx"), _viewBoxWidth); - float cy = parseLength(element->getAttribute("cy"), _viewBoxHeight); - float r = parseLength(element->getAttribute("r"), _viewBoxWidth); +std::unique_ptr SVGParserImpl::convertCircle( + const std::shared_ptr& element) { + float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); + float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); + float r = parseLength(getAttribute(element, "r"), _viewBoxWidth); auto ellipse = std::make_unique(); ellipse->centerX = cx; @@ -575,11 +316,12 @@ std::unique_ptr SVGParserImpl::convertCircle(SVGXMLNode* elem return ellipse; } -std::unique_ptr SVGParserImpl::convertEllipse(SVGXMLNode* element) { - float cx = parseLength(element->getAttribute("cx"), _viewBoxWidth); - float cy = parseLength(element->getAttribute("cy"), _viewBoxHeight); - float rx = parseLength(element->getAttribute("rx"), _viewBoxWidth); - float ry = parseLength(element->getAttribute("ry"), _viewBoxHeight); +std::unique_ptr SVGParserImpl::convertEllipse( + const std::shared_ptr& element) { + float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); + float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); + float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); + float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); auto ellipse = std::make_unique(); ellipse->centerX = cx; @@ -590,11 +332,12 @@ std::unique_ptr SVGParserImpl::convertEllipse(SVGXMLNode* ele return ellipse; } -std::unique_ptr SVGParserImpl::convertLine(SVGXMLNode* element) { - float x1 = parseLength(element->getAttribute("x1"), _viewBoxWidth); - float y1 = parseLength(element->getAttribute("y1"), _viewBoxHeight); - float x2 = parseLength(element->getAttribute("x2"), _viewBoxWidth); - float y2 = parseLength(element->getAttribute("y2"), _viewBoxHeight); +std::unique_ptr SVGParserImpl::convertLine( + const std::shared_ptr& element) { + float x1 = parseLength(getAttribute(element, "x1"), _viewBoxWidth); + float y1 = parseLength(getAttribute(element, "y1"), _viewBoxHeight); + float x2 = parseLength(getAttribute(element, "x2"), _viewBoxWidth); + float y2 = parseLength(getAttribute(element, "y2"), _viewBoxHeight); auto path = std::make_unique(); path->d.moveTo(x1, y1); @@ -603,45 +346,58 @@ std::unique_ptr SVGParserImpl::convertLine(SVGXMLNode* elemen return path; } -std::unique_ptr SVGParserImpl::convertPolyline(SVGXMLNode* element) { +std::unique_ptr SVGParserImpl::convertPolyline( + const std::shared_ptr& element) { auto path = std::make_unique(); - path->d = parsePoints(element->getAttribute("points"), false); + path->d = parsePoints(getAttribute(element, "points"), false); return path; } -std::unique_ptr SVGParserImpl::convertPolygon(SVGXMLNode* element) { +std::unique_ptr SVGParserImpl::convertPolygon( + const std::shared_ptr& element) { auto path = std::make_unique(); - path->d = parsePoints(element->getAttribute("points"), true); + path->d = parsePoints(getAttribute(element, "points"), true); return path; } -std::unique_ptr SVGParserImpl::convertPath(SVGXMLNode* element) { +std::unique_ptr SVGParserImpl::convertPath( + const std::shared_ptr& element) { auto path = std::make_unique(); - std::string d = element->getAttribute("d"); + std::string d = getAttribute(element, "d"); if (!d.empty()) { path->d = PathData::FromSVGString(d); } return path; } -std::unique_ptr SVGParserImpl::convertText(SVGXMLNode* element) { +std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr& element) { auto group = std::make_unique(); - float x = parseLength(element->getAttribute("x"), _viewBoxWidth); - float y = parseLength(element->getAttribute("y"), _viewBoxHeight); + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); + + // Get text content from child text nodes. + std::string textContent; + auto child = element->getFirstChild(); + while (child) { + if (child->type == DOMNodeType::Text) { + textContent += child->name; + } + child = child->getNextSibling(); + } - if (!element->textContent.empty()) { + if (!textContent.empty()) { auto textSpan = std::make_unique(); textSpan->x = x; textSpan->y = y; - textSpan->text = element->textContent; + textSpan->text = textContent; - std::string fontFamily = element->getAttribute("font-family"); + std::string fontFamily = getAttribute(element, "font-family"); if (!fontFamily.empty()) { textSpan->font = fontFamily; } - std::string fontSize = element->getAttribute("font-size"); + std::string fontSize = getAttribute(element, "font-size"); if (!fontSize.empty()) { textSpan->fontSize = parseLength(fontSize, _viewBoxHeight); } @@ -653,10 +409,11 @@ std::unique_ptr SVGParserImpl::convertText(SVGXMLNode* element) { return group; } -std::unique_ptr SVGParserImpl::convertUse(SVGXMLNode* element) { - std::string href = element->getAttribute("xlink:href"); +std::unique_ptr SVGParserImpl::convertUse( + const std::shared_ptr& element) { + std::string href = getAttribute(element, "xlink:href"); if (href.empty()) { - href = element->getAttribute("href"); + href = getAttribute(element, "href"); } std::string refId = resolveUrl(href); @@ -668,10 +425,10 @@ std::unique_ptr SVGParserImpl::convertUse(SVGXMLNode* element if (_options.expandUseReferences) { auto node = convertElement(it->second); if (node) { - float x = parseLength(element->getAttribute("x"), _viewBoxWidth); - float y = parseLength(element->getAttribute("y"), _viewBoxHeight); + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); if (x != 0 || y != 0) { - // Wrap in a group with translation + // Wrap in a group with translation. auto group = std::make_unique(); group->position = {x, y}; group->elements.push_back(std::move(node)); @@ -681,62 +438,68 @@ std::unique_ptr SVGParserImpl::convertUse(SVGXMLNode* element return node; } - // For non-expanded use references, just create an empty group for now + // For non-expanded use references, just create an empty group for now. auto group = std::make_unique(); group->name = "_useRef:" + refId; return group; } -std::unique_ptr SVGParserImpl::convertLinearGradient(SVGXMLNode* element) { +std::unique_ptr SVGParserImpl::convertLinearGradient( + const std::shared_ptr& element) { auto gradient = std::make_unique(); - gradient->id = element->getAttribute("id"); - gradient->startX = parseLength(element->getAttribute("x1", "0%"), 1.0f); - gradient->startY = parseLength(element->getAttribute("y1", "0%"), 1.0f); - gradient->endX = parseLength(element->getAttribute("x2", "100%"), 1.0f); - gradient->endY = parseLength(element->getAttribute("y2", "0%"), 1.0f); + gradient->id = getAttribute(element, "id"); + gradient->startX = parseLength(getAttribute(element, "x1", "0%"), 1.0f); + gradient->startY = parseLength(getAttribute(element, "y1", "0%"), 1.0f); + gradient->endX = parseLength(getAttribute(element, "x2", "100%"), 1.0f); + gradient->endY = parseLength(getAttribute(element, "y2", "0%"), 1.0f); - // Parse stops - for (auto& child : element->children) { - if (child->tagName == "stop") { + // Parse stops. + auto child = element->getFirstChild(); + while (child) { + if (child->name == "stop") { ColorStopNode stop; - stop.offset = parseLength(child->getAttribute("offset", "0"), 1.0f); - stop.color = parseColor(child->getAttribute("stop-color", "#000000")); - float opacity = parseLength(child->getAttribute("stop-opacity", "1"), 1.0f); + stop.offset = parseLength(getAttribute(child, "offset", "0"), 1.0f); + stop.color = parseColor(getAttribute(child, "stop-color", "#000000")); + float opacity = parseLength(getAttribute(child, "stop-opacity", "1"), 1.0f); stop.color.alpha = opacity; gradient->colorStops.push_back(stop); } + child = child->getNextSibling(); } return gradient; } -std::unique_ptr SVGParserImpl::convertRadialGradient(SVGXMLNode* element) { +std::unique_ptr SVGParserImpl::convertRadialGradient( + const std::shared_ptr& element) { auto gradient = std::make_unique(); - gradient->id = element->getAttribute("id"); - gradient->centerX = parseLength(element->getAttribute("cx", "50%"), 1.0f); - gradient->centerY = parseLength(element->getAttribute("cy", "50%"), 1.0f); - gradient->radius = parseLength(element->getAttribute("r", "50%"), 1.0f); + gradient->id = getAttribute(element, "id"); + gradient->centerX = parseLength(getAttribute(element, "cx", "50%"), 1.0f); + gradient->centerY = parseLength(getAttribute(element, "cy", "50%"), 1.0f); + gradient->radius = parseLength(getAttribute(element, "r", "50%"), 1.0f); - // Parse stops - for (auto& child : element->children) { - if (child->tagName == "stop") { + // Parse stops. + auto child = element->getFirstChild(); + while (child) { + if (child->name == "stop") { ColorStopNode stop; - stop.offset = parseLength(child->getAttribute("offset", "0"), 1.0f); - stop.color = parseColor(child->getAttribute("stop-color", "#000000")); - float opacity = parseLength(child->getAttribute("stop-opacity", "1"), 1.0f); + stop.offset = parseLength(getAttribute(child, "offset", "0"), 1.0f); + stop.color = parseColor(getAttribute(child, "stop-color", "#000000")); + float opacity = parseLength(getAttribute(child, "stop-opacity", "1"), 1.0f); stop.color.alpha = opacity; gradient->colorStops.push_back(stop); } + child = child->getNextSibling(); } return gradient; } -void SVGParserImpl::addFillStroke(SVGXMLNode* element, +void SVGParserImpl::addFillStroke(const std::shared_ptr& element, std::vector>& contents) { - std::string fill = element->getAttribute("fill"); + std::string fill = getAttribute(element, "fill"); if (!fill.empty() && fill != "none") { auto fillNode = std::make_unique(); @@ -744,38 +507,38 @@ void SVGParserImpl::addFillStroke(SVGXMLNode* element, std::string refId = resolveUrl(fill); fillNode->color = "#" + refId; - // Try to inline the gradient + // Try to inline the gradient. auto it = _defs.find(refId); if (it != _defs.end()) { - if (it->second->tagName == "linearGradient") { + if (it->second->name == "linearGradient") { fillNode->colorSource = convertLinearGradient(it->second); - } else if (it->second->tagName == "radialGradient") { + } else if (it->second->name == "radialGradient") { fillNode->colorSource = convertRadialGradient(it->second); } } } else { Color color = parseColor(fill); - std::string fillOpacity = element->getAttribute("fill-opacity"); + std::string fillOpacity = getAttribute(element, "fill-opacity"); if (!fillOpacity.empty()) { color.alpha = std::stof(fillOpacity); } fillNode->color = color.toHexString(color.alpha < 1); } - std::string fillRule = element->getAttribute("fill-rule"); + std::string fillRule = getAttribute(element, "fill-rule"); if (fillRule == "evenodd") { fillNode->fillRule = FillRule::EvenOdd; } contents.push_back(std::move(fillNode)); } else if (fill.empty()) { - // SVG default is black fill + // SVG default is black fill. auto fillNode = std::make_unique(); fillNode->color = "#000000"; contents.push_back(std::move(fillNode)); } - std::string stroke = element->getAttribute("stroke"); + std::string stroke = getAttribute(element, "stroke"); if (!stroke.empty() && stroke != "none") { auto strokeNode = std::make_unique(); @@ -785,42 +548,42 @@ void SVGParserImpl::addFillStroke(SVGXMLNode* element, auto it = _defs.find(refId); if (it != _defs.end()) { - if (it->second->tagName == "linearGradient") { + if (it->second->name == "linearGradient") { strokeNode->colorSource = convertLinearGradient(it->second); - } else if (it->second->tagName == "radialGradient") { + } else if (it->second->name == "radialGradient") { strokeNode->colorSource = convertRadialGradient(it->second); } } } else { Color color = parseColor(stroke); - std::string strokeOpacity = element->getAttribute("stroke-opacity"); + std::string strokeOpacity = getAttribute(element, "stroke-opacity"); if (!strokeOpacity.empty()) { color.alpha = std::stof(strokeOpacity); } strokeNode->color = color.toHexString(color.alpha < 1); } - std::string strokeWidth = element->getAttribute("stroke-width"); + std::string strokeWidth = getAttribute(element, "stroke-width"); if (!strokeWidth.empty()) { strokeNode->strokeWidth = parseLength(strokeWidth, _viewBoxWidth); } - std::string strokeLinecap = element->getAttribute("stroke-linecap"); + std::string strokeLinecap = getAttribute(element, "stroke-linecap"); if (!strokeLinecap.empty()) { strokeNode->cap = LineCapFromString(strokeLinecap); } - std::string strokeLinejoin = element->getAttribute("stroke-linejoin"); + std::string strokeLinejoin = getAttribute(element, "stroke-linejoin"); if (!strokeLinejoin.empty()) { strokeNode->join = LineJoinFromString(strokeLinejoin); } - std::string strokeMiterlimit = element->getAttribute("stroke-miterlimit"); + std::string strokeMiterlimit = getAttribute(element, "stroke-miterlimit"); if (!strokeMiterlimit.empty()) { strokeNode->miterLimit = std::stof(strokeMiterlimit); } - std::string dashArray = element->getAttribute("stroke-dasharray"); + std::string dashArray = getAttribute(element, "stroke-dasharray"); if (!dashArray.empty() && dashArray != "none") { std::istringstream iss(dashArray); float val = 0; @@ -831,7 +594,7 @@ void SVGParserImpl::addFillStroke(SVGXMLNode* element, } } - std::string dashOffset = element->getAttribute("stroke-dashoffset"); + std::string dashOffset = getAttribute(element, "stroke-dashoffset"); if (!dashOffset.empty()) { strokeNode->dashOffset = parseLength(dashOffset, _viewBoxWidth); } @@ -989,7 +752,7 @@ Color SVGParserImpl::parseColor(const std::string& value) { } } - // Named colors (subset) + // Named colors (subset). static const std::unordered_map namedColors = { {"black", 0x000000}, {"white", 0xFFFFFF}, {"red", 0xFF0000}, {"green", 0x008000}, {"blue", 0x0000FF}, {"yellow", 0xFFFF00}, @@ -1031,7 +794,7 @@ float SVGParserImpl::parseLength(const std::string& value, float containerSize) return num * 1.333333f; } if (unit == "em" || unit == "rem") { - return num * 16.0f; // Assume 16px base font + return num * 16.0f; // Assume 16px base font. } if (unit == "in") { return num * 96.0f; @@ -1057,7 +820,7 @@ std::vector SVGParserImpl::parseViewBox(const std::string& value) { while (iss >> num) { result.push_back(num); char c = 0; - iss >> c; // Skip separator + iss >> c; // Skip separator. } return result; @@ -1097,7 +860,7 @@ std::string SVGParserImpl::resolveUrl(const std::string& url) { if (url.empty()) { return ""; } - // Handle url(#id) format + // Handle url(#id) format. if (url.find("url(") == 0) { size_t start = url.find('#'); size_t end = url.find(')'); @@ -1105,7 +868,7 @@ std::string SVGParserImpl::resolveUrl(const std::string& url) { return url.substr(start + 1, end - start - 1); } } - // Handle #id format + // Handle #id format. if (url[0] == '#') { return url.substr(1); } diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 26145ed585..ccbe694964 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -24,56 +24,47 @@ #include #include "pagx/PAGXDocument.h" #include "pagx/PAGXSVGParser.h" +#include "xml/XMLDOM.h" namespace pagx { /** - * Internal XML node representation for SVG parsing. - */ -struct SVGXMLNode { - std::string tagName = {}; - std::unordered_map attributes = {}; - std::vector> children = {}; - std::string textContent = {}; - - std::string getAttribute(const std::string& name, const std::string& defaultValue = "") const { - auto it = attributes.find(name); - return it != attributes.end() ? it->second : defaultValue; - } -}; - -/** - * Internal SVG parser implementation. + * Internal SVG parser implementation using expat-based XML DOM parsing. */ class SVGParserImpl { public: explicit SVGParserImpl(const PAGXSVGParser::Options& options); std::shared_ptr parse(const uint8_t* data, size_t length); + std::shared_ptr parseFile(const std::string& filePath); private: - std::unique_ptr parseXML(const char* data, size_t length); + std::shared_ptr parseDOM(const std::shared_ptr& dom); - void parseDefs(SVGXMLNode* defsNode); + void parseDefs(const std::shared_ptr& defsNode); - std::unique_ptr convertToLayer(SVGXMLNode* element); - void convertChildren(SVGXMLNode* element, std::vector>& contents); - std::unique_ptr convertElement(SVGXMLNode* element); - std::unique_ptr convertG(SVGXMLNode* element); - std::unique_ptr convertRect(SVGXMLNode* element); - std::unique_ptr convertCircle(SVGXMLNode* element); - std::unique_ptr convertEllipse(SVGXMLNode* element); - std::unique_ptr convertLine(SVGXMLNode* element); - std::unique_ptr convertPolyline(SVGXMLNode* element); - std::unique_ptr convertPolygon(SVGXMLNode* element); - std::unique_ptr convertPath(SVGXMLNode* element); - std::unique_ptr convertText(SVGXMLNode* element); - std::unique_ptr convertUse(SVGXMLNode* element); + std::unique_ptr convertToLayer(const std::shared_ptr& element); + void convertChildren(const std::shared_ptr& element, + std::vector>& contents); + std::unique_ptr convertElement(const std::shared_ptr& element); + std::unique_ptr convertG(const std::shared_ptr& element); + std::unique_ptr convertRect(const std::shared_ptr& element); + std::unique_ptr convertCircle(const std::shared_ptr& element); + std::unique_ptr convertEllipse(const std::shared_ptr& element); + std::unique_ptr convertLine(const std::shared_ptr& element); + std::unique_ptr convertPolyline(const std::shared_ptr& element); + std::unique_ptr convertPolygon(const std::shared_ptr& element); + std::unique_ptr convertPath(const std::shared_ptr& element); + std::unique_ptr convertText(const std::shared_ptr& element); + std::unique_ptr convertUse(const std::shared_ptr& element); - std::unique_ptr convertLinearGradient(SVGXMLNode* element); - std::unique_ptr convertRadialGradient(SVGXMLNode* element); + std::unique_ptr convertLinearGradient( + const std::shared_ptr& element); + std::unique_ptr convertRadialGradient( + const std::shared_ptr& element); - void addFillStroke(SVGXMLNode* element, std::vector>& contents); + void addFillStroke(const std::shared_ptr& element, + std::vector>& contents); Matrix parseTransform(const std::string& value); Color parseColor(const std::string& value); @@ -82,9 +73,13 @@ class SVGParserImpl { PathData parsePoints(const std::string& value, bool closed); std::string resolveUrl(const std::string& url); + // Helper to get attribute from DOMNode. + std::string getAttribute(const std::shared_ptr& node, const std::string& name, + const std::string& defaultValue = "") const; + PAGXSVGParser::Options _options = {}; std::shared_ptr _document = nullptr; - std::unordered_map _defs = {}; + std::unordered_map> _defs = {}; float _viewBoxWidth = 0; float _viewBoxHeight = 0; }; diff --git a/pagx/src/xml/XMLDOM.cpp b/pagx/src/xml/XMLDOM.cpp new file mode 100644 index 0000000000..f3f800e094 --- /dev/null +++ b/pagx/src/xml/XMLDOM.cpp @@ -0,0 +1,226 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "XMLDOM.h" +#include +#include +#include "XMLParser.h" + +namespace pagx { + +/** + * XML parser that builds a DOM tree from parsing events. + */ +class DOMParser : public XMLParser { + public: + DOMParser() = default; + + std::shared_ptr getRoot() const { + return _root; + } + + protected: + bool onStartElement(const std::string& element) override { + startCommon(element, DOMNodeType::Element); + return false; + } + + bool onAddAttribute(const std::string& name, const std::string& value) override { + _attributes.push_back({name, value}); + return false; + } + + bool onEndElement(const std::string& /*element*/) override { + if (_needToFlush) { + flushAttributes(); + } + _needToFlush = false; + --_level; + + auto parent = _parentStack.top(); + _parentStack.pop(); + + // Reverse children to correct order (they were added in reverse). + auto child = parent->firstChild; + std::shared_ptr prev = nullptr; + while (child) { + auto next = child->nextSibling; + child->nextSibling = prev; + prev = child; + child = next; + } + parent->firstChild = prev; + return false; + } + + bool onText(const std::string& text) override { + // Ignore text if it is empty or contains only whitespace. + if (text.find_first_not_of(" \n\r\t") != std::string::npos) { + startCommon(text, DOMNodeType::Text); + onEndElement(_elementName); + } + return false; + } + + private: + void flushAttributes() { + auto node = std::make_shared(); + node->name = _elementName; + node->firstChild = nullptr; + node->attributes.swap(_attributes); + node->type = _elementType; + + if (_root == nullptr) { + node->nextSibling = nullptr; + _root = node; + } else { + // Add siblings in reverse order; gets corrected in onEndElement(). + auto parent = _parentStack.top(); + node->nextSibling = parent->firstChild; + parent->firstChild = node; + } + _parentStack.push(node); + _attributes.clear(); + } + + void startCommon(const std::string& element, DOMNodeType type) { + if (_level > 0 && _needToFlush) { + flushAttributes(); + } + _needToFlush = true; + _elementName = element; + _elementType = type; + ++_level; + } + + std::stack> _parentStack; + std::shared_ptr _root = nullptr; + bool _needToFlush = true; + + std::vector _attributes; + std::string _elementName; + DOMNodeType _elementType = DOMNodeType::Element; + int _level = 0; +}; + +// ============== DOM ============== + +DOM::DOM(std::shared_ptr root) : _root(std::move(root)) { +} + +DOM::~DOM() = default; + +std::shared_ptr DOM::Make(const uint8_t* data, size_t length) { + DOMParser parser; + if (!parser.parse(data, length)) { + return nullptr; + } + auto root = parser.getRoot(); + if (!root) { + return nullptr; + } + return std::shared_ptr(new DOM(root)); +} + +std::shared_ptr DOM::MakeFromFile(const std::string& filePath) { + DOMParser parser; + if (!parser.parseFile(filePath)) { + return nullptr; + } + auto root = parser.getRoot(); + if (!root) { + return nullptr; + } + return std::shared_ptr(new DOM(root)); +} + +std::shared_ptr DOM::getRootNode() const { + return _root; +} + +// ============== DOMNode ============== + +DOMNode::~DOMNode() { + // Avoid recursive destruction crash on huge node counts: iteratively unlink children/siblings. + if (!firstChild && !nextSibling) { + return; + } + + std::vector> stack; + if (firstChild) { + stack.emplace_back(std::move(firstChild)); + } + if (nextSibling) { + stack.emplace_back(std::move(nextSibling)); + } + + while (!stack.empty()) { + auto node = std::move(stack.back()); + stack.pop_back(); + if (!node) { + continue; + } + if (node->firstChild) { + stack.emplace_back(std::move(node->firstChild)); + } + if (node->nextSibling) { + stack.emplace_back(std::move(node->nextSibling)); + } + } +} + +std::shared_ptr DOMNode::getFirstChild(const std::string& name) const { + auto child = firstChild; + if (!name.empty()) { + while (child && child->name != name) { + child = child->nextSibling; + } + } + return child; +} + +std::shared_ptr DOMNode::getNextSibling(const std::string& name) const { + auto sibling = nextSibling; + if (!name.empty()) { + while (sibling && sibling->name != name) { + sibling = sibling->nextSibling; + } + } + return sibling; +} + +std::tuple DOMNode::findAttribute(const std::string& attrName) const { + for (const auto& attr : attributes) { + if (attr.name == attrName) { + return {true, attr.value}; + } + } + return {false, ""}; +} + +int DOMNode::countChildren(const std::string& name) const { + int count = 0; + auto child = getFirstChild(name); + while (child) { + ++count; + child = child->getNextSibling(name); + } + return count; +} + +} // namespace pagx diff --git a/pagx/src/xml/XMLDOM.h b/pagx/src/xml/XMLDOM.h new file mode 100644 index 0000000000..8e7d5dabce --- /dev/null +++ b/pagx/src/xml/XMLDOM.h @@ -0,0 +1,121 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace pagx { + +/** + * Represents an XML attribute with name and value. + */ +struct DOMAttribute { + std::string name; + std::string value; +}; + +/** + * Type of DOM node. + */ +enum class DOMNodeType { + Element, + Text, +}; + +/** + * Represents a node in the XML DOM tree. + */ +struct DOMNode { + std::string name; + std::shared_ptr firstChild; + std::shared_ptr nextSibling; + std::vector attributes; + DOMNodeType type = DOMNodeType::Element; + + ~DOMNode(); + + /** + * Gets the first child node, optionally filtered by name. + * @param name Optional filter by element name. Empty string matches all. + * @return The first child node, or nullptr if not found. + */ + std::shared_ptr getFirstChild(const std::string& name = "") const; + + /** + * Gets the next sibling node, optionally filtered by name. + * @param name Optional filter by element name. Empty string matches all. + * @return The next sibling node, or nullptr if not found. + */ + std::shared_ptr getNextSibling(const std::string& name = "") const; + + /** + * Finds an attribute value by name. + * @param attrName The attribute name to find. + * @return A tuple of (found, value). If not found, returns (false, ""). + */ + std::tuple findAttribute(const std::string& attrName) const; + + /** + * Counts the number of children, optionally filtered by name. + * @param name Optional filter by element name. Empty string matches all. + * @return The count of matching children. + */ + int countChildren(const std::string& name = "") const; +}; + +/** + * Represents an XML DOM tree. + */ +class DOM { + public: + ~DOM(); + + /** + * Constructs a DOM tree from XML data in memory. + * @param data Pointer to XML data. + * @param length Length of the data in bytes. + * @return The DOM tree, or nullptr if parsing fails. + */ + static std::shared_ptr Make(const uint8_t* data, size_t length); + + /** + * Constructs a DOM tree from an XML file. + * @param filePath Path to the XML file. + * @return The DOM tree, or nullptr if parsing fails. + */ + static std::shared_ptr MakeFromFile(const std::string& filePath); + + /** + * Gets the root node of the DOM tree. + * @return The root node. + */ + std::shared_ptr getRootNode() const; + + private: + explicit DOM(std::shared_ptr root); + + std::shared_ptr _root = nullptr; +}; + +} // namespace pagx diff --git a/pagx/src/xml/XMLParser.cpp b/pagx/src/xml/XMLParser.cpp new file mode 100644 index 0000000000..7e9b61ae48 --- /dev/null +++ b/pagx/src/xml/XMLParser.cpp @@ -0,0 +1,182 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "XMLParser.h" +#include +#include +#include +#include +#include +#include "expat/lib/expat.h" + +namespace pagx { + +namespace { + +template +struct OverloadedFunctionObject { + template + auto operator()(Args&&... args) const -> decltype(P(std::forward(args)...)) { + return P(std::forward(args)...); + } +}; + +template +using FunctionObject = OverloadedFunctionObject, F>; + +template +class AutoTCallVProc : public std::unique_ptr> { + using inherited = std::unique_ptr>; + + public: + using inherited::inherited; + AutoTCallVProc(const AutoTCallVProc&) = delete; + AutoTCallVProc(AutoTCallVProc&& that) noexcept : inherited(std::move(that)) { + } + + operator T*() const { + return this->get(); + } +}; + +constexpr const void* HASH_SEED = &HASH_SEED; + +const XML_Memory_Handling_Suite XML_alloc = {malloc, realloc, free}; + +struct ParsingContext { + explicit ParsingContext(XMLParser* parser) + : _parser(parser), _XMLParser(XML_ParserCreate_MM(nullptr, &XML_alloc, nullptr)) { + } + + void flushText() { + if (!_bufferedText.empty()) { + _parser->text(_bufferedText); + _bufferedText.clear(); + } + } + + void appendText(const char* txt, size_t len) { + _bufferedText.insert(_bufferedText.end(), txt, &txt[len]); + } + + XMLParser* _parser; + AutoTCallVProc, XML_ParserFree> _XMLParser; + + private: + std::string _bufferedText; +}; + +#define HANDLER_CONTEXT(arg, name) ParsingContext* name = static_cast(arg) + +void XMLCALL start_element_handler(void* data, const char* tag, const char** attributes) { + HANDLER_CONTEXT(data, context); + context->flushText(); + + context->_parser->startElement(tag); + + for (size_t i = 0; attributes[i]; i += 2) { + context->_parser->addAttribute(attributes[i], attributes[i + 1]); + } +} + +void XMLCALL end_element_handler(void* data, const char* tag) { + HANDLER_CONTEXT(data, context); + context->flushText(); + + context->_parser->endElement(tag); +} + +void XMLCALL text_handler(void* data, const char* txt, int len) { + HANDLER_CONTEXT(data, context); + + context->appendText(txt, static_cast(len)); +} + +void XMLCALL entity_decl_handler(void* data, const XML_Char* /*entityName*/, + int /*is_parameter_entity*/, const XML_Char* /*value*/, + int /*value_length*/, const XML_Char* /*base*/, + const XML_Char* /*systemId*/, const XML_Char* /*publicId*/, + const XML_Char* /*notationName*/) { + HANDLER_CONTEXT(data, context); + // Disable entity processing to inhibit internal entity expansion. See expat CVE-2013-0340. + XML_StopParser(context->_XMLParser, XML_FALSE); +} + +} // anonymous namespace + +XMLParser::XMLParser() = default; +XMLParser::~XMLParser() = default; + +bool XMLParser::parse(const uint8_t* data, size_t length) { + if (data == nullptr || length == 0) { + return false; + } + + ParsingContext parsingContext(this); + if (!parsingContext._XMLParser) { + return false; + } + + // Avoid calls to rand_s if this is not set. This seed helps prevent DOS + // with a known hash sequence so an address is sufficient. The provided + // seed should not be zero as that results in a call to rand_s. + auto seed = static_cast(reinterpret_cast(HASH_SEED) & 0xFFFFFFFF); + XML_SetHashSalt(parsingContext._XMLParser, seed ? seed : 1); + + XML_SetUserData(parsingContext._XMLParser, &parsingContext); + XML_SetElementHandler(parsingContext._XMLParser, start_element_handler, end_element_handler); + XML_SetCharacterDataHandler(parsingContext._XMLParser, text_handler); + XML_SetEntityDeclHandler(parsingContext._XMLParser, entity_decl_handler); + + auto status = + XML_Parse(parsingContext._XMLParser, reinterpret_cast(data), + static_cast(length), true); + + return XML_STATUS_ERROR != status; +} + +bool XMLParser::parseFile(const std::string& filePath) { + std::ifstream file(filePath, std::ios::binary); + if (!file.is_open()) { + return false; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + + return parse(reinterpret_cast(content.data()), content.size()); +} + +bool XMLParser::startElement(const std::string& element) { + return this->onStartElement(element); +} + +bool XMLParser::addAttribute(const std::string& name, const std::string& value) { + return this->onAddAttribute(name, value); +} + +bool XMLParser::endElement(const std::string& element) { + return this->onEndElement(element); +} + +bool XMLParser::text(const std::string& text) { + return this->onText(text); +} + +} // namespace pagx diff --git a/pagx/src/xml/XMLParser.h b/pagx/src/xml/XMLParser.h new file mode 100644 index 0000000000..155da54b9e --- /dev/null +++ b/pagx/src/xml/XMLParser.h @@ -0,0 +1,93 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +namespace pagx { + +/** + * SAX-style XML parser based on expat. + * Subclasses override the callback methods to handle parsing events. + */ +class XMLParser { + public: + XMLParser(); + virtual ~XMLParser(); + + /** + * Parses XML data from a memory buffer. + * @param data Pointer to XML data. + * @param length Length of the data in bytes. + * @return true if parsing is successful, false otherwise. + */ + bool parse(const uint8_t* data, size_t length); + + /** + * Parses XML data from a file. + * @param filePath Path to the XML file. + * @return true if parsing is successful, false otherwise. + */ + bool parseFile(const std::string& filePath); + + protected: + /** + * Called when an element start tag is encountered. + * Override in subclasses to handle element start events. + * @param element The element name. + * @return true to stop parsing, false to continue. + */ + virtual bool onStartElement(const std::string& element) = 0; + + /** + * Called for each attribute of the current element. + * Override in subclasses to handle attributes. + * @param name The attribute name. + * @param value The attribute value. + * @return true to stop parsing, false to continue. + */ + virtual bool onAddAttribute(const std::string& name, const std::string& value) = 0; + + /** + * Called when an element end tag is encountered. + * Override in subclasses to handle element end events. + * @param element The element name. + * @return true to stop parsing, false to continue. + */ + virtual bool onEndElement(const std::string& element) = 0; + + /** + * Called when text content is encountered. + * Override in subclasses to handle text content. + * @param text The text content. + * @return true to stop parsing, false to continue. + */ + virtual bool onText(const std::string& text) = 0; + + public: + // Public for internal parser library calls, not intended for client use. + bool startElement(const std::string& element); + bool addAttribute(const std::string& name, const std::string& value); + bool endElement(const std::string& element); + bool text(const std::string& text); +}; + +} // namespace pagx From a9677642d33388b97d87f8d1ffe7878af68ef432 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 19:48:27 +0800 Subject: [PATCH 011/678] Make pagx use its own expat dependency instead of relying on tgfx and remove unnecessary tgfx SVG module dependency. --- CMakeLists.txt | 4 ---- pagx/CMakeLists.txt | 23 +++++++++++------------ pagx/src/xml/XMLParser.cpp | 2 +- vendor.json | 13 +++++++++++++ 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0935dccd7d..5d784b23e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,8 +102,6 @@ if (PAG_BUILD_TESTS) set(PAG_USE_HARFBUZZ ON) set(PAG_USE_SYSTEM_LZ4 OFF) set(PAG_BUILD_SHARED OFF) - set(PAG_BUILD_SVG ON) - set(PAG_BUILD_LAYERS ON) endif () message("PAG_USE_LIBAVC: ${PAG_USE_LIBAVC}") @@ -506,8 +504,6 @@ else () set(TGFX_ENABLE_PROFILING ${PAG_ENABLE_PROFILING}) set(TGFX_USE_THREADS ${PAG_USE_THREADS}) set(TGFX_USE_TEXT_GAMMA_CORRECTION ON) - set(TGFX_BUILD_SVG ${PAG_BUILD_SVG}) - set(TGFX_BUILD_LAYERS ${PAG_BUILD_LAYERS}) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) add_subdirectory(${TGFX_DIR} tgfx EXCLUDE_FROM_ALL) list(APPEND PAG_STATIC_LIBS $) diff --git a/pagx/CMakeLists.txt b/pagx/CMakeLists.txt index f1207759aa..46436d3551 100644 --- a/pagx/CMakeLists.txt +++ b/pagx/CMakeLists.txt @@ -51,18 +51,10 @@ target_include_directories(pagx PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ) -# expat include directory (use tgfx's expat if available, otherwise use libpag's) -if (TARGET tgfx) - # Use tgfx's expat directory - target_include_directories(pagx PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/tgfx/third_party/expat - ) -else() - # Use libpag's expat directory - target_include_directories(pagx PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/expat - ) -endif() +# expat include directory +target_include_directories(pagx PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/expat/expat/lib +) # Windows requires XML_STATIC definition for static expat if(WIN32) @@ -81,3 +73,10 @@ if (PAGX_BUILD_TGFX_ADAPTER) ) target_link_libraries(pagx PUBLIC tgfx) endif() + +# ============== Vendor (expat) ============== +# Build expat and merge into pagx static library +add_vendor_target(pagx-vendor STATIC_VENDORS expat CONFIG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) +find_vendor_libraries(pagx-vendor STATIC PAGX_VENDOR_STATIC_LIBRARIES) +add_dependencies(pagx pagx-vendor) +merge_libraries_into(pagx ${PAGX_VENDOR_STATIC_LIBRARIES}) diff --git a/pagx/src/xml/XMLParser.cpp b/pagx/src/xml/XMLParser.cpp index 7e9b61ae48..3f5d497ec8 100644 --- a/pagx/src/xml/XMLParser.cpp +++ b/pagx/src/xml/XMLParser.cpp @@ -22,7 +22,7 @@ #include #include #include -#include "expat/lib/expat.h" +#include "expat.h" namespace pagx { diff --git a/vendor.json b/vendor.json index d69eeccfa9..5c045d8165 100644 --- a/vendor.json +++ b/vendor.json @@ -51,6 +51,19 @@ "linux" ] } + }, + { + "name": "expat", + "dir": "expat/expat", + "cmake": { + "targets": [ + "expat" + ], + "arguments": [ + "-DBUILD_SHARED_LIBS=OFF", + "-DCMAKE_C_FLAGS=\"-w\"" + ] + } } ] } From a2452277d2d35fa557964cabf17286d42f432892 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 19:50:39 +0800 Subject: [PATCH 012/678] Remove tgfx SVGDOM dependency from PAGXTest to avoid crash in tgfx SVG renderer. --- test/src/PAGXTest.cpp | 41 +++++++++-------------------------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index abce381d9c..5a94ca0e7d 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -24,11 +24,8 @@ #include "pagx/PAGXTypes.h" #include "pagx/PathData.h" #include "tgfx/core/Data.h" -#include "tgfx/core/Stream.h" #include "tgfx/core/Surface.h" #include "tgfx/core/Typeface.h" -#include "tgfx/svg/SVGDOM.h" -#include "tgfx/svg/TextShaper.h" #include "utils/Baseline.h" #include "utils/DevicePool.h" #include "utils/ProjectPath.h" @@ -92,36 +89,10 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { auto context = device->lockContext(); ASSERT_TRUE(context != nullptr); - // Create text shaper for SVG rendering - auto textShaper = TextShaper::Make(GetFallbackTypefaces()); - for (const auto& svgPath : svgFiles) { std::string baseName = std::filesystem::path(svgPath).stem().string(); - // Load original SVG with text shaper - auto svgStream = Stream::MakeFromFile(svgPath); - if (svgStream == nullptr) { - continue; - } - auto svgDOM = SVGDOM::Make(*svgStream, textShaper); - if (svgDOM == nullptr) { - continue; - } - - auto containerSize = svgDOM->getContainerSize(); - int width = static_cast(containerSize.width); - int height = static_cast(containerSize.height); - if (width <= 0 || height <= 0) { - continue; - } - - // Render original SVG - auto svgSurface = Surface::Make(context, width, height); - auto svgCanvas = svgSurface->getCanvas(); - svgDOM->render(svgCanvas); - EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); - - // Convert to PAGX using new API + // Convert to PAGX using LayerBuilder API pagx::LayerBuilder::Options options; options.fallbackTypefaces = GetFallbackTypefaces(); auto content = pagx::LayerBuilder::FromSVGFile(svgPath, options); @@ -129,6 +100,12 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { continue; } + int width = static_cast(content.width); + int height = static_cast(content.height); + if (width <= 0 || height <= 0) { + continue; + } + // Save PAGX file to output directory pagx::PAGXSVGParser::Options parserOptions; auto doc = pagx::PAGXSVGParser::Parse(svgPath, parserOptions); @@ -138,11 +115,11 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { SaveFile(pagxData, "PAGXTest/" + baseName + ".pagx"); } - // Render PAGX + // Render PAGX and compare with baseline auto pagxSurface = Surface::Make(context, width, height); auto pagxCanvas = pagxSurface->getCanvas(); content.root->draw(pagxCanvas); - EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName + "_pagx")); + EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName)); } device->unlock(); From 8d2ac2e844e1a3a9ded3825017cd62707af8b867 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 19:54:42 +0800 Subject: [PATCH 013/678] Enable tgfx SVG and Layers modules for PAG tests. --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d784b23e0..0935dccd7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,8 @@ if (PAG_BUILD_TESTS) set(PAG_USE_HARFBUZZ ON) set(PAG_USE_SYSTEM_LZ4 OFF) set(PAG_BUILD_SHARED OFF) + set(PAG_BUILD_SVG ON) + set(PAG_BUILD_LAYERS ON) endif () message("PAG_USE_LIBAVC: ${PAG_USE_LIBAVC}") @@ -504,6 +506,8 @@ else () set(TGFX_ENABLE_PROFILING ${PAG_ENABLE_PROFILING}) set(TGFX_USE_THREADS ${PAG_USE_THREADS}) set(TGFX_USE_TEXT_GAMMA_CORRECTION ON) + set(TGFX_BUILD_SVG ${PAG_BUILD_SVG}) + set(TGFX_BUILD_LAYERS ${PAG_BUILD_LAYERS}) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) add_subdirectory(${TGFX_DIR} tgfx EXCLUDE_FROM_ALL) list(APPEND PAG_STATIC_LIBS $) From 6e3948ea2e58f5b1abb90942499874d2c547c170 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 20:05:02 +0800 Subject: [PATCH 014/678] Simplify tgfx SVG and Layers options by using TGFX_ prefix directly in test block. --- CMakeLists.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0935dccd7d..2579bd061a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,8 +102,8 @@ if (PAG_BUILD_TESTS) set(PAG_USE_HARFBUZZ ON) set(PAG_USE_SYSTEM_LZ4 OFF) set(PAG_BUILD_SHARED OFF) - set(PAG_BUILD_SVG ON) - set(PAG_BUILD_LAYERS ON) + set(TGFX_BUILD_SVG ON) + set(TGFX_BUILD_LAYERS ON) endif () message("PAG_USE_LIBAVC: ${PAG_USE_LIBAVC}") @@ -506,8 +506,6 @@ else () set(TGFX_ENABLE_PROFILING ${PAG_ENABLE_PROFILING}) set(TGFX_USE_THREADS ${PAG_USE_THREADS}) set(TGFX_USE_TEXT_GAMMA_CORRECTION ON) - set(TGFX_BUILD_SVG ${PAG_BUILD_SVG}) - set(TGFX_BUILD_LAYERS ${PAG_BUILD_LAYERS}) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) add_subdirectory(${TGFX_DIR} tgfx EXCLUDE_FROM_ALL) list(APPEND PAG_STATIC_LIBS $) From 8532a7c2216a11d8f7a31d911f3bc61faf7ac315 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 20:15:43 +0800 Subject: [PATCH 015/678] Align PAGX data structures and XML attributes with the updated spec. --- pagx/include/pagx/PAGXNode.h | 48 ++++++++----------- pagx/include/pagx/PAGXTypes.h | 26 ++++++++-- pagx/src/PAGXTypes.cpp | 14 +++--- pagx/src/PAGXXMLParser.cpp | 88 +++++++++++++++++++++------------- pagx/src/PAGXXMLParser.h | 1 + pagx/src/PAGXXMLWriter.cpp | 84 +++++++++++++++++++------------- pagx/src/svg/PAGXSVGParser.cpp | 46 +++++++++--------- pagx/src/tgfx/LayerBuilder.cpp | 23 ++++----- test/src/PAGXTest.cpp | 56 +++++++++++----------- 9 files changed, 213 insertions(+), 173 deletions(-) diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index 1a734c3fbc..b6e7567b4f 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -178,10 +178,8 @@ struct SolidColorNode : public ColorSourceNode { * A linear gradient. */ struct LinearGradientNode : public ColorSourceNode { - float startX = 0; - float startY = 0; - float endX = 0; - float endY = 0; + Point startPoint = {}; + Point endPoint = {}; Matrix matrix = {}; std::vector colorStops = {}; @@ -198,8 +196,7 @@ struct LinearGradientNode : public ColorSourceNode { * A radial gradient. */ struct RadialGradientNode : public ColorSourceNode { - float centerX = 0; - float centerY = 0; + Point center = {}; float radius = 0; Matrix matrix = {}; std::vector colorStops = {}; @@ -217,8 +214,7 @@ struct RadialGradientNode : public ColorSourceNode { * A conic (sweep) gradient. */ struct ConicGradientNode : public ColorSourceNode { - float centerX = 0; - float centerY = 0; + Point center = {}; float startAngle = 0; float endAngle = 360; Matrix matrix = {}; @@ -237,8 +233,7 @@ struct ConicGradientNode : public ColorSourceNode { * A diamond gradient. */ struct DiamondGradientNode : public ColorSourceNode { - float centerX = 0; - float centerY = 0; + Point center = {}; float halfDiagonal = 0; Matrix matrix = {}; std::vector colorStops = {}; @@ -284,10 +279,8 @@ class VectorElementNode : public PAGXNode {}; * A rectangle shape. */ struct RectangleNode : public VectorElementNode { - float centerX = 0; - float centerY = 0; - float width = 0; - float height = 0; + Point center = {}; + Size size = {}; float roundness = 0; bool reversed = false; @@ -304,10 +297,8 @@ struct RectangleNode : public VectorElementNode { * An ellipse shape. */ struct EllipseNode : public VectorElementNode { - float centerX = 0; - float centerY = 0; - float width = 0; - float height = 0; + Point center = {}; + Size size = {}; bool reversed = false; NodeType type() const override { @@ -323,10 +314,9 @@ struct EllipseNode : public VectorElementNode { * A polygon or star shape. */ struct PolystarNode : public VectorElementNode { - float centerX = 0; - float centerY = 0; + Point center = {}; PolystarType polystarType = PolystarType::Star; - float points = 5; + float pointCount = 5; float outerRadius = 100; float innerRadius = 50; float rotation = 0; @@ -347,7 +337,7 @@ struct PolystarNode : public VectorElementNode { * A path shape. */ struct PathNode : public VectorElementNode { - PathData d = {}; + PathData data = {}; bool reversed = false; NodeType type() const override { @@ -499,7 +489,7 @@ struct RoundCornerNode : public VectorElementNode { * Merge path modifier. */ struct MergePathNode : public VectorElementNode { - PathOp op = PathOp::Append; + MergePathMode mode = MergePathMode::Append; NodeType type() const override { return NodeType::MergePath; @@ -527,8 +517,8 @@ struct RangeSelectorNode : public PAGXNode { float easeOut = 0; SelectorMode mode = SelectorMode::Add; float weight = 1; - bool randomize = false; - int seed = 0; + bool randomizeOrder = false; + int randomSeed = 0; NodeType type() const override { return NodeType::RangeSelector; @@ -543,7 +533,7 @@ struct RangeSelectorNode : public PAGXNode { * Text modifier. */ struct TextModifierNode : public VectorElementNode { - Point anchor = {0.5f, 0.5f}; + Point anchorPoint = {0.5f, 0.5f}; Point position = {}; float rotation = 0; Point scale = {1, 1}; @@ -617,7 +607,7 @@ struct RepeaterNode : public VectorElementNode { float copies = 3; float offset = 0; RepeaterOrder order = RepeaterOrder::BelowOriginal; - Point anchor = {}; + Point anchorPoint = {}; Point position = {100, 100}; float rotation = 0; Point scale = {1, 1}; @@ -642,7 +632,7 @@ struct RepeaterNode : public VectorElementNode { */ struct GroupNode : public VectorElementNode { std::string name = {}; - Point anchor = {}; + Point anchorPoint = {}; Point position = {}; float rotation = 0; Point scale = {1, 1}; @@ -658,7 +648,7 @@ struct GroupNode : public VectorElementNode { std::unique_ptr clone() const override { auto node = std::make_unique(); node->name = name; - node->anchor = anchor; + node->anchorPoint = anchorPoint; node->position = position; node->rotation = rotation; node->scale = scale; diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h index f915953c22..bc73918ca7 100644 --- a/pagx/include/pagx/PAGXTypes.h +++ b/pagx/include/pagx/PAGXTypes.h @@ -40,6 +40,22 @@ struct Point { } }; +/** + * A size with width and height. + */ +struct Size { + float width = 0; + float height = 0; + + bool operator==(const Size& other) const { + return width == other.width && height == other.height; + } + + bool operator!=(const Size& other) const { + return !(*this == other); + } +}; + /** * A rectangle defined by position and size. */ @@ -235,7 +251,7 @@ enum class BlendMode { Saturation, Color, Luminosity, - Add + PlusLighter }; /** @@ -326,9 +342,9 @@ enum class TrimType { }; /** - * Path boolean operations. + * Path merge modes (boolean operations). */ -enum class PathOp { +enum class MergePathMode { Append, Union, Intersect, @@ -458,8 +474,8 @@ PolystarType PolystarTypeFromString(const std::string& str); std::string TrimTypeToString(TrimType type); TrimType TrimTypeFromString(const std::string& str); -std::string PathOpToString(PathOp op); -PathOp PathOpFromString(const std::string& str); +std::string MergePathModeToString(MergePathMode mode); +MergePathMode MergePathModeFromString(const std::string& str); std::string TextAlignToString(TextAlign align); TextAlign TextAlignFromString(const std::string& str); diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp index b76d662c16..448d7be042 100644 --- a/pagx/src/PAGXTypes.cpp +++ b/pagx/src/PAGXTypes.cpp @@ -246,7 +246,7 @@ DEFINE_ENUM_CONVERSION(BlendMode, {BlendMode::Saturation, "saturation"}, {BlendMode::Color, "color"}, {BlendMode::Luminosity, "luminosity"}, - {BlendMode::Add, "add"}) + {BlendMode::PlusLighter, "plusLighter"}) DEFINE_ENUM_CONVERSION(LineCap, {LineCap::Butt, "butt"}, @@ -295,12 +295,12 @@ DEFINE_ENUM_CONVERSION(TrimType, {TrimType::Separate, "separate"}, {TrimType::Continuous, "continuous"}) -DEFINE_ENUM_CONVERSION(PathOp, - {PathOp::Append, "append"}, - {PathOp::Union, "union"}, - {PathOp::Intersect, "intersect"}, - {PathOp::Xor, "xor"}, - {PathOp::Difference, "difference"}) +DEFINE_ENUM_CONVERSION(MergePathMode, + {MergePathMode::Append, "append"}, + {MergePathMode::Union, "union"}, + {MergePathMode::Intersect, "intersect"}, + {MergePathMode::Xor, "xor"}, + {MergePathMode::Difference, "difference"}) DEFINE_ENUM_CONVERSION(TextAlign, {TextAlign::Left, "left"}, diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 54280b1be2..5d46a5062b 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -532,10 +532,10 @@ std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* std::unique_ptr PAGXXMLParser::parseRectangle(const XMLNode* node) { auto rect = std::make_unique(); - rect->centerX = getFloatAttribute(node, "centerX", 0); - rect->centerY = getFloatAttribute(node, "centerY", 0); - rect->width = getFloatAttribute(node, "width", 0); - rect->height = getFloatAttribute(node, "height", 0); + auto centerStr = getAttribute(node, "center", "0,0"); + rect->center = parsePoint(centerStr); + auto sizeStr = getAttribute(node, "size", "0,0"); + rect->size = parseSize(sizeStr); rect->roundness = getFloatAttribute(node, "roundness", 0); rect->reversed = getBoolAttribute(node, "reversed", false); return rect; @@ -543,20 +543,20 @@ std::unique_ptr PAGXXMLParser::parseRectangle(const XMLNode* node std::unique_ptr PAGXXMLParser::parseEllipse(const XMLNode* node) { auto ellipse = std::make_unique(); - ellipse->centerX = getFloatAttribute(node, "centerX", 0); - ellipse->centerY = getFloatAttribute(node, "centerY", 0); - ellipse->width = getFloatAttribute(node, "width", 0); - ellipse->height = getFloatAttribute(node, "height", 0); + auto centerStr = getAttribute(node, "center", "0,0"); + ellipse->center = parsePoint(centerStr); + auto sizeStr = getAttribute(node, "size", "0,0"); + ellipse->size = parseSize(sizeStr); ellipse->reversed = getBoolAttribute(node, "reversed", false); return ellipse; } std::unique_ptr PAGXXMLParser::parsePolystar(const XMLNode* node) { auto polystar = std::make_unique(); - polystar->centerX = getFloatAttribute(node, "centerX", 0); - polystar->centerY = getFloatAttribute(node, "centerY", 0); - polystar->polystarType = PolystarTypeFromString(getAttribute(node, "type", "star")); - polystar->points = getFloatAttribute(node, "points", 5); + auto centerStr = getAttribute(node, "center", "0,0"); + polystar->center = parsePoint(centerStr); + polystar->polystarType = PolystarTypeFromString(getAttribute(node, "polystarType", "star")); + polystar->pointCount = getFloatAttribute(node, "pointCount", 5); polystar->outerRadius = getFloatAttribute(node, "outerRadius", 100); polystar->innerRadius = getFloatAttribute(node, "innerRadius", 50); polystar->rotation = getFloatAttribute(node, "rotation", 0); @@ -568,9 +568,9 @@ std::unique_ptr PAGXXMLParser::parsePolystar(const XMLNode* node) std::unique_ptr PAGXXMLParser::parsePath(const XMLNode* node) { auto path = std::make_unique(); - auto dAttr = getAttribute(node, "d"); - if (!dAttr.empty()) { - path->d = PathData::FromSVGString(dAttr); + auto dataAttr = getAttribute(node, "data"); + if (!dataAttr.empty()) { + path->data = PathData::FromSVGString(dataAttr); } path->reversed = getBoolAttribute(node, "reversed", false); return path; @@ -662,14 +662,14 @@ std::unique_ptr PAGXXMLParser::parseRoundCorner(const XMLNode* std::unique_ptr PAGXXMLParser::parseMergePath(const XMLNode* node) { auto merge = std::make_unique(); - merge->op = PathOpFromString(getAttribute(node, "op", "append")); + merge->mode = MergePathModeFromString(getAttribute(node, "mode", "append")); return merge; } std::unique_ptr PAGXXMLParser::parseTextModifier(const XMLNode* node) { auto modifier = std::make_unique(); - auto anchorStr = getAttribute(node, "anchor", "0.5,0.5"); - modifier->anchor = parsePoint(anchorStr); + auto anchorStr = getAttribute(node, "anchorPoint", "0.5,0.5"); + modifier->anchorPoint = parsePoint(anchorStr); auto positionStr = getAttribute(node, "position", "0,0"); modifier->position = parsePoint(positionStr); modifier->rotation = getFloatAttribute(node, "rotation", 0); @@ -720,8 +720,8 @@ std::unique_ptr PAGXXMLParser::parseRepeater(const XMLNode* node) repeater->copies = getFloatAttribute(node, "copies", 3); repeater->offset = getFloatAttribute(node, "offset", 0); repeater->order = RepeaterOrderFromString(getAttribute(node, "order", "belowOriginal")); - auto anchorStr = getAttribute(node, "anchor", "0,0"); - repeater->anchor = parsePoint(anchorStr); + auto anchorStr = getAttribute(node, "anchorPoint", "0,0"); + repeater->anchorPoint = parsePoint(anchorStr); auto positionStr = getAttribute(node, "position", "100,100"); repeater->position = parsePoint(positionStr); repeater->rotation = getFloatAttribute(node, "rotation", 0); @@ -735,8 +735,8 @@ std::unique_ptr PAGXXMLParser::parseRepeater(const XMLNode* node) std::unique_ptr PAGXXMLParser::parseGroup(const XMLNode* node) { auto group = std::make_unique(); group->name = getAttribute(node, "name"); - auto anchorStr = getAttribute(node, "anchor", "0,0"); - group->anchor = parsePoint(anchorStr); + auto anchorStr = getAttribute(node, "anchorPoint", "0,0"); + group->anchorPoint = parsePoint(anchorStr); auto positionStr = getAttribute(node, "position", "0,0"); group->position = parsePoint(positionStr); group->rotation = getFloatAttribute(node, "rotation", 0); @@ -767,8 +767,8 @@ RangeSelectorNode PAGXXMLParser::parseRangeSelector(const XMLNode* node) { selector.easeOut = getFloatAttribute(node, "easeOut", 0); selector.mode = SelectorModeFromString(getAttribute(node, "mode", "add")); selector.weight = getFloatAttribute(node, "weight", 1); - selector.randomize = getBoolAttribute(node, "randomize", false); - selector.seed = getIntAttribute(node, "seed", 0); + selector.randomizeOrder = getBoolAttribute(node, "randomizeOrder", false); + selector.randomSeed = getIntAttribute(node, "randomSeed", 0); return selector; } @@ -789,10 +789,10 @@ std::unique_ptr PAGXXMLParser::parseSolidColor(const XMLNode* no std::unique_ptr PAGXXMLParser::parseLinearGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); - gradient->startX = getFloatAttribute(node, "startX", 0); - gradient->startY = getFloatAttribute(node, "startY", 0); - gradient->endX = getFloatAttribute(node, "endX", 0); - gradient->endY = getFloatAttribute(node, "endY", 0); + auto startPointStr = getAttribute(node, "startPoint", "0,0"); + gradient->startPoint = parsePoint(startPointStr); + auto endPointStr = getAttribute(node, "endPoint", "0,0"); + gradient->endPoint = parsePoint(endPointStr); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { gradient->matrix = Matrix::Parse(matrixStr); @@ -808,8 +808,8 @@ std::unique_ptr PAGXXMLParser::parseLinearGradient(const XML std::unique_ptr PAGXXMLParser::parseRadialGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); - gradient->centerX = getFloatAttribute(node, "centerX", 0); - gradient->centerY = getFloatAttribute(node, "centerY", 0); + auto centerStr = getAttribute(node, "center", "0,0"); + gradient->center = parsePoint(centerStr); gradient->radius = getFloatAttribute(node, "radius", 0); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { @@ -826,8 +826,8 @@ std::unique_ptr PAGXXMLParser::parseRadialGradient(const XML std::unique_ptr PAGXXMLParser::parseConicGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); - gradient->centerX = getFloatAttribute(node, "centerX", 0); - gradient->centerY = getFloatAttribute(node, "centerY", 0); + auto centerStr = getAttribute(node, "center", "0,0"); + gradient->center = parsePoint(centerStr); gradient->startAngle = getFloatAttribute(node, "startAngle", 0); gradient->endAngle = getFloatAttribute(node, "endAngle", 360); auto matrixStr = getAttribute(node, "matrix"); @@ -845,8 +845,8 @@ std::unique_ptr PAGXXMLParser::parseConicGradient(const XMLNo std::unique_ptr PAGXXMLParser::parseDiamondGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); - gradient->centerX = getFloatAttribute(node, "centerX", 0); - gradient->centerY = getFloatAttribute(node, "centerY", 0); + auto centerStr = getAttribute(node, "center", "0,0"); + gradient->center = parsePoint(centerStr); gradient->halfDiagonal = getFloatAttribute(node, "halfDiagonal", 0); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { @@ -1080,6 +1080,26 @@ Point PAGXXMLParser::parsePoint(const std::string& str) { return point; } +Size PAGXXMLParser::parseSize(const std::string& str) { + Size size = {}; + std::istringstream iss(str); + std::string token = {}; + std::vector values = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + if (!trimmed.empty()) { + values.push_back(std::stof(trimmed)); + } + } + if (values.size() >= 2) { + size.width = values[0]; + size.height = values[1]; + } + return size; +} + Rect PAGXXMLParser::parseRect(const std::string& str) { Rect rect = {}; std::istringstream iss(str); diff --git a/pagx/src/PAGXXMLParser.h b/pagx/src/PAGXXMLParser.h index 5801840667..3d402005fa 100644 --- a/pagx/src/PAGXXMLParser.h +++ b/pagx/src/PAGXXMLParser.h @@ -108,6 +108,7 @@ class PAGXXMLParser { static bool getBoolAttribute(const XMLNode* node, const std::string& name, bool defaultValue = false); static Point parsePoint(const std::string& str); + static Size parseSize(const std::string& str); static Rect parseRect(const std::string& str); static std::vector parseFloatList(const std::string& str); }; diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index d9140d2669..3701160f29 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -149,6 +149,12 @@ static std::string pointToString(const Point& p) { return oss.str(); } +static std::string sizeToString(const Size& s) { + std::ostringstream oss = {}; + oss << s.width << "," << s.height; + return oss.str(); +} + static std::string rectToString(const Rect& r) { std::ostringstream oss = {}; oss << r.x << "," << r.y << "," << r.width << "," << r.height; @@ -189,10 +195,12 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { auto grad = static_cast(node); xml.openElement("LinearGradient"); xml.addAttribute("id", grad->id); - xml.addAttribute("startX", grad->startX); - xml.addAttribute("startY", grad->startY); - xml.addAttribute("endX", grad->endX); - xml.addAttribute("endY", grad->endY); + if (grad->startPoint.x != 0 || grad->startPoint.y != 0) { + xml.addAttribute("startPoint", pointToString(grad->startPoint)); + } + if (grad->endPoint.x != 0 || grad->endPoint.y != 0) { + xml.addAttribute("endPoint", pointToString(grad->endPoint)); + } if (!grad->matrix.isIdentity()) { xml.addAttribute("matrix", grad->matrix.toString()); } @@ -209,8 +217,9 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { auto grad = static_cast(node); xml.openElement("RadialGradient"); xml.addAttribute("id", grad->id); - xml.addAttribute("centerX", grad->centerX); - xml.addAttribute("centerY", grad->centerY); + if (grad->center.x != 0 || grad->center.y != 0) { + xml.addAttribute("center", pointToString(grad->center)); + } xml.addAttribute("radius", grad->radius); if (!grad->matrix.isIdentity()) { xml.addAttribute("matrix", grad->matrix.toString()); @@ -228,8 +237,9 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { auto grad = static_cast(node); xml.openElement("ConicGradient"); xml.addAttribute("id", grad->id); - xml.addAttribute("centerX", grad->centerX); - xml.addAttribute("centerY", grad->centerY); + if (grad->center.x != 0 || grad->center.y != 0) { + xml.addAttribute("center", pointToString(grad->center)); + } xml.addAttribute("startAngle", grad->startAngle); xml.addAttribute("endAngle", grad->endAngle, 360.0f); if (!grad->matrix.isIdentity()) { @@ -248,8 +258,9 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { auto grad = static_cast(node); xml.openElement("DiamondGradient"); xml.addAttribute("id", grad->id); - xml.addAttribute("centerX", grad->centerX); - xml.addAttribute("centerY", grad->centerY); + if (grad->center.x != 0 || grad->center.y != 0) { + xml.addAttribute("center", pointToString(grad->center)); + } xml.addAttribute("halfDiagonal", grad->halfDiagonal); if (!grad->matrix.isIdentity()) { xml.addAttribute("matrix", grad->matrix.toString()); @@ -293,10 +304,12 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::Rectangle: { auto rect = static_cast(node); xml.openElement("Rectangle"); - xml.addAttribute("centerX", rect->centerX); - xml.addAttribute("centerY", rect->centerY); - xml.addAttribute("width", rect->width); - xml.addAttribute("height", rect->height); + if (rect->center.x != 0 || rect->center.y != 0) { + xml.addAttribute("center", pointToString(rect->center)); + } + if (rect->size.width != 0 || rect->size.height != 0) { + xml.addAttribute("size", sizeToString(rect->size)); + } xml.addAttribute("roundness", rect->roundness); xml.addAttribute("reversed", rect->reversed); xml.closeElementSelfClosing(); @@ -305,10 +318,12 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::Ellipse: { auto ellipse = static_cast(node); xml.openElement("Ellipse"); - xml.addAttribute("centerX", ellipse->centerX); - xml.addAttribute("centerY", ellipse->centerY); - xml.addAttribute("width", ellipse->width); - xml.addAttribute("height", ellipse->height); + if (ellipse->center.x != 0 || ellipse->center.y != 0) { + xml.addAttribute("center", pointToString(ellipse->center)); + } + if (ellipse->size.width != 0 || ellipse->size.height != 0) { + xml.addAttribute("size", sizeToString(ellipse->size)); + } xml.addAttribute("reversed", ellipse->reversed); xml.closeElementSelfClosing(); break; @@ -316,10 +331,11 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::Polystar: { auto polystar = static_cast(node); xml.openElement("Polystar"); - xml.addAttribute("centerX", polystar->centerX); - xml.addAttribute("centerY", polystar->centerY); - xml.addAttribute("type", PolystarTypeToString(polystar->polystarType)); - xml.addAttribute("points", polystar->points, 5.0f); + if (polystar->center.x != 0 || polystar->center.y != 0) { + xml.addAttribute("center", pointToString(polystar->center)); + } + xml.addAttribute("polystarType", PolystarTypeToString(polystar->polystarType)); + xml.addAttribute("pointCount", polystar->pointCount, 5.0f); xml.addAttribute("outerRadius", polystar->outerRadius, 100.0f); xml.addAttribute("innerRadius", polystar->innerRadius, 50.0f); xml.addAttribute("rotation", polystar->rotation); @@ -332,8 +348,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::Path: { auto path = static_cast(node); xml.openElement("Path"); - if (!path->d.isEmpty()) { - xml.addAttribute("d", path->d.toSVGString()); + if (!path->data.isEmpty()) { + xml.addAttribute("data", path->data.toSVGString()); } xml.addAttribute("reversed", path->reversed); xml.closeElementSelfClosing(); @@ -437,8 +453,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::MergePath: { auto merge = static_cast(node); xml.openElement("MergePath"); - if (merge->op != PathOp::Append) { - xml.addAttribute("op", PathOpToString(merge->op)); + if (merge->mode != MergePathMode::Append) { + xml.addAttribute("mode", MergePathModeToString(merge->mode)); } xml.closeElementSelfClosing(); break; @@ -446,8 +462,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::TextModifier: { auto modifier = static_cast(node); xml.openElement("TextModifier"); - if (modifier->anchor.x != 0.5f || modifier->anchor.y != 0.5f) { - xml.addAttribute("anchor", pointToString(modifier->anchor)); + if (modifier->anchorPoint.x != 0.5f || modifier->anchorPoint.y != 0.5f) { + xml.addAttribute("anchorPoint", pointToString(modifier->anchorPoint)); } if (modifier->position.x != 0 || modifier->position.y != 0) { xml.addAttribute("position", pointToString(modifier->position)); @@ -485,8 +501,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { xml.addAttribute("mode", SelectorModeToString(selector.mode)); } xml.addAttribute("weight", selector.weight, 1.0f); - xml.addAttribute("randomize", selector.randomize); - xml.addAttribute("seed", selector.seed); + xml.addAttribute("randomizeOrder", selector.randomizeOrder); + xml.addAttribute("randomSeed", selector.randomSeed); xml.closeElementSelfClosing(); } xml.closeElement(); @@ -535,8 +551,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { if (repeater->order != RepeaterOrder::BelowOriginal) { xml.addAttribute("order", RepeaterOrderToString(repeater->order)); } - if (repeater->anchor.x != 0 || repeater->anchor.y != 0) { - xml.addAttribute("anchor", pointToString(repeater->anchor)); + if (repeater->anchorPoint.x != 0 || repeater->anchorPoint.y != 0) { + xml.addAttribute("anchorPoint", pointToString(repeater->anchorPoint)); } if (repeater->position.x != 100 || repeater->position.y != 100) { xml.addAttribute("position", pointToString(repeater->position)); @@ -554,8 +570,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { auto group = static_cast(node); xml.openElement("Group"); xml.addAttribute("name", group->name); - if (group->anchor.x != 0 || group->anchor.y != 0) { - xml.addAttribute("anchor", pointToString(group->anchor)); + if (group->anchorPoint.x != 0 || group->anchorPoint.y != 0) { + xml.addAttribute("anchorPoint", pointToString(group->anchorPoint)); } if (group->position.x != 0 || group->position.y != 0) { xml.addAttribute("position", pointToString(group->position)); diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 313017defa..3f66cc9363 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -292,10 +292,10 @@ std::unique_ptr SVGParserImpl::convertRect( } auto rect = std::make_unique(); - rect->centerX = x + width / 2; - rect->centerY = y + height / 2; - rect->width = width; - rect->height = height; + rect->center.x = x + width / 2; + rect->center.y = y + height / 2; + rect->size.width = width; + rect->size.height = height; rect->roundness = std::max(rx, ry); return rect; @@ -308,10 +308,10 @@ std::unique_ptr SVGParserImpl::convertCircle( float r = parseLength(getAttribute(element, "r"), _viewBoxWidth); auto ellipse = std::make_unique(); - ellipse->centerX = cx; - ellipse->centerY = cy; - ellipse->width = r * 2; - ellipse->height = r * 2; + ellipse->center.x = cx; + ellipse->center.y = cy; + ellipse->size.width = r * 2; + ellipse->size.height = r * 2; return ellipse; } @@ -324,10 +324,10 @@ std::unique_ptr SVGParserImpl::convertEllipse( float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); auto ellipse = std::make_unique(); - ellipse->centerX = cx; - ellipse->centerY = cy; - ellipse->width = rx * 2; - ellipse->height = ry * 2; + ellipse->center.x = cx; + ellipse->center.y = cy; + ellipse->size.width = rx * 2; + ellipse->size.height = ry * 2; return ellipse; } @@ -340,8 +340,8 @@ std::unique_ptr SVGParserImpl::convertLine( float y2 = parseLength(getAttribute(element, "y2"), _viewBoxHeight); auto path = std::make_unique(); - path->d.moveTo(x1, y1); - path->d.lineTo(x2, y2); + path->data.moveTo(x1, y1); + path->data.lineTo(x2, y2); return path; } @@ -349,14 +349,14 @@ std::unique_ptr SVGParserImpl::convertLine( std::unique_ptr SVGParserImpl::convertPolyline( const std::shared_ptr& element) { auto path = std::make_unique(); - path->d = parsePoints(getAttribute(element, "points"), false); + path->data = parsePoints(getAttribute(element, "points"), false); return path; } std::unique_ptr SVGParserImpl::convertPolygon( const std::shared_ptr& element) { auto path = std::make_unique(); - path->d = parsePoints(getAttribute(element, "points"), true); + path->data = parsePoints(getAttribute(element, "points"), true); return path; } @@ -365,7 +365,7 @@ std::unique_ptr SVGParserImpl::convertPath( auto path = std::make_unique(); std::string d = getAttribute(element, "d"); if (!d.empty()) { - path->d = PathData::FromSVGString(d); + path->data = PathData::FromSVGString(d); } return path; } @@ -449,10 +449,10 @@ std::unique_ptr SVGParserImpl::convertLinearGradient( auto gradient = std::make_unique(); gradient->id = getAttribute(element, "id"); - gradient->startX = parseLength(getAttribute(element, "x1", "0%"), 1.0f); - gradient->startY = parseLength(getAttribute(element, "y1", "0%"), 1.0f); - gradient->endX = parseLength(getAttribute(element, "x2", "100%"), 1.0f); - gradient->endY = parseLength(getAttribute(element, "y2", "0%"), 1.0f); + gradient->startPoint.x = parseLength(getAttribute(element, "x1", "0%"), 1.0f); + gradient->startPoint.y = parseLength(getAttribute(element, "y1", "0%"), 1.0f); + gradient->endPoint.x = parseLength(getAttribute(element, "x2", "100%"), 1.0f); + gradient->endPoint.y = parseLength(getAttribute(element, "y2", "0%"), 1.0f); // Parse stops. auto child = element->getFirstChild(); @@ -476,8 +476,8 @@ std::unique_ptr SVGParserImpl::convertRadialGradient( auto gradient = std::make_unique(); gradient->id = getAttribute(element, "id"); - gradient->centerX = parseLength(getAttribute(element, "cx", "50%"), 1.0f); - gradient->centerY = parseLength(getAttribute(element, "cy", "50%"), 1.0f); + gradient->center.x = parseLength(getAttribute(element, "cx", "50%"), 1.0f); + gradient->center.y = parseLength(getAttribute(element, "cy", "50%"), 1.0f); gradient->radius = parseLength(getAttribute(element, "r", "50%"), 1.0f); // Parse stops. diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 7b183d7434..2d4ac0530d 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -212,8 +212,8 @@ class LayerBuilderImpl { std::shared_ptr convertRectangle(const RectangleNode* node) { auto rect = std::make_shared(); - rect->setCenter(tgfx::Point::Make(node->centerX, node->centerY)); - rect->setSize({node->width, node->height}); + rect->setCenter(ToTGFX(node->center)); + rect->setSize({node->size.width, node->size.height}); rect->setRoundness(node->roundness); rect->setReversed(node->reversed); return rect; @@ -221,16 +221,16 @@ class LayerBuilderImpl { std::shared_ptr convertEllipse(const EllipseNode* node) { auto ellipse = std::make_shared(); - ellipse->setCenter(tgfx::Point::Make(node->centerX, node->centerY)); - ellipse->setSize({node->width, node->height}); + ellipse->setCenter(ToTGFX(node->center)); + ellipse->setSize({node->size.width, node->size.height}); ellipse->setReversed(node->reversed); return ellipse; } std::shared_ptr convertPolystar(const PolystarNode* node) { auto polystar = std::make_shared(); - polystar->setCenter(tgfx::Point::Make(node->centerX, node->centerY)); - polystar->setPointCount(node->points); + polystar->setCenter(ToTGFX(node->center)); + polystar->setPointCount(node->pointCount); polystar->setOuterRadius(node->outerRadius); polystar->setInnerRadius(node->innerRadius); polystar->setOuterRoundness(node->outerRoundness); @@ -247,7 +247,7 @@ class LayerBuilderImpl { std::shared_ptr convertPath(const PathNode* node) { auto shapePath = std::make_shared(); - auto tgfxPath = ToTGFX(node->d); + auto tgfxPath = ToTGFX(node->data); shapePath->setPath(tgfxPath); return shapePath; } @@ -357,10 +357,8 @@ class LayerBuilderImpl { positions = {0.0f, 1.0f}; } - auto startPoint = tgfx::Point::Make(node->startX, node->startY); - auto endPoint = tgfx::Point::Make(node->endX, node->endY); - - return tgfx::Gradient::MakeLinear(startPoint, endPoint, colors, positions); + return tgfx::Gradient::MakeLinear(ToTGFX(node->startPoint), ToTGFX(node->endPoint), colors, + positions); } std::shared_ptr convertRadialGradient(const RadialGradientNode* node) { @@ -377,8 +375,7 @@ class LayerBuilderImpl { positions = {0.0f, 1.0f}; } - auto center = tgfx::Point::Make(node->centerX, node->centerY); - return tgfx::Gradient::MakeRadial(center, node->radius, colors, positions); + return tgfx::Gradient::MakeRadial(ToTGFX(node->center), node->radius, colors, positions); } std::shared_ptr convertTrimPath(const TrimPathNode* node) { diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 5a94ca0e7d..6eacbf83c6 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -166,22 +166,22 @@ PAG_TEST(PAGXTest, PathDataForEach) { PAG_TEST(PAGXTest, PAGXNodeBasic) { // Test RectangleNode creation auto rect = std::make_unique(); - rect->centerX = 50; - rect->centerY = 50; - rect->width = 100; - rect->height = 80; + rect->center.x = 50; + rect->center.y = 50; + rect->size.width = 100; + rect->size.height = 80; rect->roundness = 10; EXPECT_EQ(rect->type(), pagx::NodeType::Rectangle); EXPECT_STREQ(pagx::NodeTypeName(rect->type()), "Rectangle"); - EXPECT_FLOAT_EQ(rect->centerX, 50); - EXPECT_FLOAT_EQ(rect->width, 100); + EXPECT_FLOAT_EQ(rect->center.x, 50); + EXPECT_FLOAT_EQ(rect->size.width, 100); // Test PathNode creation auto path = std::make_unique(); - path->d = pagx::PathData::FromSVGString("M0 0 L100 100"); + path->data = pagx::PathData::FromSVGString("M0 0 L100 100"); EXPECT_EQ(path->type(), pagx::NodeType::Path); - EXPECT_GT(path->d.verbs().size(), 0u); + EXPECT_GT(path->data.verbs().size(), 0u); // Test FillNode creation auto fill = std::make_unique(); @@ -218,10 +218,10 @@ PAG_TEST(PAGXTest, PAGXDocumentXMLExport) { group->name = "testGroup"; auto rect = std::make_unique(); - rect->centerX = 50; - rect->centerY = 50; - rect->width = 80; - rect->height = 60; + rect->center.x = 50; + rect->center.y = 50; + rect->size.width = 80; + rect->size.height = 60; auto fill = std::make_unique(); fill->color = "#0000FF"; @@ -256,10 +256,10 @@ PAG_TEST(PAGXTest, PAGXDocumentRoundTrip) { layer->name = "TestLayer"; auto rect = std::make_unique(); - rect->centerX = 50; - rect->centerY = 50; - rect->width = 80; - rect->height = 60; + rect->center.x = 50; + rect->center.y = 50; + rect->size.width = 80; + rect->size.height = 60; auto fill = std::make_unique(); fill->color = "#00FF00"; @@ -347,10 +347,10 @@ PAG_TEST(PAGXTest, ColorSourceNodes) { // Test LinearGradientNode auto linear = std::make_unique(); - linear->startX = 0; - linear->startY = 0; - linear->endX = 100; - linear->endY = 0; + linear->startPoint.x = 0; + linear->startPoint.y = 0; + linear->endPoint.x = 100; + linear->endPoint.y = 0; pagx::ColorStopNode stop1; stop1.offset = 0; @@ -368,8 +368,8 @@ PAG_TEST(PAGXTest, ColorSourceNodes) { // Test RadialGradientNode auto radial = std::make_unique(); - radial->centerX = 50; - radial->centerY = 50; + radial->center.x = 50; + radial->center.y = 50; radial->radius = 50; radial->colorStops = linear->colorStops; @@ -412,18 +412,18 @@ PAG_TEST(PAGXTest, LayerNodeStylesFilters) { PAG_TEST(PAGXTest, NodeClone) { // Test simple node clone auto rect = std::make_unique(); - rect->centerX = 50; - rect->centerY = 50; - rect->width = 100; - rect->height = 80; + rect->center.x = 50; + rect->center.y = 50; + rect->size.width = 100; + rect->size.height = 80; auto cloned = rect->clone(); ASSERT_TRUE(cloned != nullptr); EXPECT_EQ(cloned->type(), pagx::NodeType::Rectangle); auto clonedRect = static_cast(cloned.get()); - EXPECT_FLOAT_EQ(clonedRect->centerX, 50); - EXPECT_FLOAT_EQ(clonedRect->width, 100); + EXPECT_FLOAT_EQ(clonedRect->center.x, 50); + EXPECT_FLOAT_EQ(clonedRect->size.width, 100); // Test group with children clone auto group = std::make_unique(); From 0f7d58398a6591903aedf199543576f8495cc0a9 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 20:18:31 +0800 Subject: [PATCH 016/678] Update tgfx commit to fix crash issue. --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index d71afe767e..b20b19af92 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "b8251c0dd494a24fafa9735de3c265c20b144c7e", + "commit": "ced4fe464a8459e4efda8608927660e16bd009bb", "dir": "third_party/tgfx" }, { From dd93993b9402f018a089984238b7def30882f0cd Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 20:22:58 +0800 Subject: [PATCH 017/678] Restore tgfx SVGDOM rendering test for SVG to PAGX comparison. --- test/src/PAGXTest.cpp | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 6eacbf83c6..0218189661 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -24,8 +24,11 @@ #include "pagx/PAGXTypes.h" #include "pagx/PathData.h" #include "tgfx/core/Data.h" +#include "tgfx/core/Stream.h" #include "tgfx/core/Surface.h" #include "tgfx/core/Typeface.h" +#include "tgfx/svg/SVGDOM.h" +#include "tgfx/svg/TextShaper.h" #include "utils/Baseline.h" #include "utils/DevicePool.h" #include "utils/ProjectPath.h" @@ -89,23 +92,43 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { auto context = device->lockContext(); ASSERT_TRUE(context != nullptr); + // Create text shaper for SVG rendering + auto textShaper = TextShaper::Make(GetFallbackTypefaces()); + for (const auto& svgPath : svgFiles) { std::string baseName = std::filesystem::path(svgPath).stem().string(); - // Convert to PAGX using LayerBuilder API - pagx::LayerBuilder::Options options; - options.fallbackTypefaces = GetFallbackTypefaces(); - auto content = pagx::LayerBuilder::FromSVGFile(svgPath, options); - if (content.root == nullptr) { + // Load original SVG with text shaper + auto svgStream = Stream::MakeFromFile(svgPath); + if (svgStream == nullptr) { + continue; + } + auto svgDOM = SVGDOM::Make(*svgStream, textShaper); + if (svgDOM == nullptr) { continue; } - int width = static_cast(content.width); - int height = static_cast(content.height); + auto containerSize = svgDOM->getContainerSize(); + int width = static_cast(containerSize.width); + int height = static_cast(containerSize.height); if (width <= 0 || height <= 0) { continue; } + // Render original SVG + auto svgSurface = Surface::Make(context, width, height); + auto svgCanvas = svgSurface->getCanvas(); + svgDOM->render(svgCanvas); + EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); + + // Convert to PAGX using new API + pagx::LayerBuilder::Options options; + options.fallbackTypefaces = GetFallbackTypefaces(); + auto content = pagx::LayerBuilder::FromSVGFile(svgPath, options); + if (content.root == nullptr) { + continue; + } + // Save PAGX file to output directory pagx::PAGXSVGParser::Options parserOptions; auto doc = pagx::PAGXSVGParser::Parse(svgPath, parserOptions); @@ -115,11 +138,11 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { SaveFile(pagxData, "PAGXTest/" + baseName + ".pagx"); } - // Render PAGX and compare with baseline + // Render PAGX auto pagxSurface = Surface::Make(context, width, height); auto pagxCanvas = pagxSurface->getCanvas(); content.root->draw(pagxCanvas); - EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName)); + EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName + "_pagx")); } device->unlock(); From 55216b941361c94e6cba386ce9aecdd01152c5a4 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 20:27:21 +0800 Subject: [PATCH 018/678] Add complete SVG named color keywords support in SVG parser. --- pagx/src/svg/PAGXSVGParser.cpp | 159 +++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 8 deletions(-) diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 3f66cc9363..057065c894 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -752,16 +752,159 @@ Color SVGParserImpl::parseColor(const std::string& value) { } } - // Named colors (subset). + // SVG named colors (complete list from SVG 1.1 specification). + // clang-format off static const std::unordered_map namedColors = { - {"black", 0x000000}, {"white", 0xFFFFFF}, {"red", 0xFF0000}, - {"green", 0x008000}, {"blue", 0x0000FF}, {"yellow", 0xFFFF00}, - {"cyan", 0x00FFFF}, {"magenta", 0xFF00FF}, {"gray", 0x808080}, - {"grey", 0x808080}, {"silver", 0xC0C0C0}, {"maroon", 0x800000}, - {"olive", 0x808000}, {"lime", 0x00FF00}, {"aqua", 0x00FFFF}, - {"teal", 0x008080}, {"navy", 0x000080}, {"fuchsia", 0xFF00FF}, - {"purple", 0x800080}, {"orange", 0xFFA500}, {"transparent", 0x000000}, + {"aliceblue", 0xF0F8FF}, + {"antiquewhite", 0xFAEBD7}, + {"aqua", 0x00FFFF}, + {"aquamarine", 0x7FFFD4}, + {"azure", 0xF0FFFF}, + {"beige", 0xF5F5DC}, + {"bisque", 0xFFE4C4}, + {"black", 0x000000}, + {"blanchedalmond", 0xFFEBCD}, + {"blue", 0x0000FF}, + {"blueviolet", 0x8A2BE2}, + {"brown", 0xA52A2A}, + {"burlywood", 0xDEB887}, + {"cadetblue", 0x5F9EA0}, + {"chartreuse", 0x7FFF00}, + {"chocolate", 0xD2691E}, + {"coral", 0xFF7F50}, + {"cornflowerblue", 0x6495ED}, + {"cornsilk", 0xFFF8DC}, + {"crimson", 0xDC143C}, + {"cyan", 0x00FFFF}, + {"darkblue", 0x00008B}, + {"darkcyan", 0x008B8B}, + {"darkgoldenrod", 0xB8860B}, + {"darkgray", 0xA9A9A9}, + {"darkgreen", 0x006400}, + {"darkgrey", 0xA9A9A9}, + {"darkkhaki", 0xBDB76B}, + {"darkmagenta", 0x8B008B}, + {"darkolivegreen", 0x556B2F}, + {"darkorange", 0xFF8C00}, + {"darkorchid", 0x9932CC}, + {"darkred", 0x8B0000}, + {"darksalmon", 0xE9967A}, + {"darkseagreen", 0x8FBC8F}, + {"darkslateblue", 0x483D8B}, + {"darkslategray", 0x2F4F4F}, + {"darkslategrey", 0x2F4F4F}, + {"darkturquoise", 0x00CED1}, + {"darkviolet", 0x9400D3}, + {"deeppink", 0xFF1493}, + {"deepskyblue", 0x00BFFF}, + {"dimgray", 0x696969}, + {"dimgrey", 0x696969}, + {"dodgerblue", 0x1E90FF}, + {"firebrick", 0xB22222}, + {"floralwhite", 0xFFFAF0}, + {"forestgreen", 0x228B22}, + {"fuchsia", 0xFF00FF}, + {"gainsboro", 0xDCDCDC}, + {"ghostwhite", 0xF8F8FF}, + {"gold", 0xFFD700}, + {"goldenrod", 0xDAA520}, + {"gray", 0x808080}, + {"green", 0x008000}, + {"greenyellow", 0xADFF2F}, + {"grey", 0x808080}, + {"honeydew", 0xF0FFF0}, + {"hotpink", 0xFF69B4}, + {"indianred", 0xCD5C5C}, + {"indigo", 0x4B0082}, + {"ivory", 0xFFFFF0}, + {"khaki", 0xF0E68C}, + {"lavender", 0xE6E6FA}, + {"lavenderblush", 0xFFF0F5}, + {"lawngreen", 0x7CFC00}, + {"lemonchiffon", 0xFFFACD}, + {"lightblue", 0xADD8E6}, + {"lightcoral", 0xF08080}, + {"lightcyan", 0xE0FFFF}, + {"lightgoldenrodyellow", 0xFAFAD2}, + {"lightgray", 0xD3D3D3}, + {"lightgreen", 0x90EE90}, + {"lightgrey", 0xD3D3D3}, + {"lightpink", 0xFFB6C1}, + {"lightsalmon", 0xFFA07A}, + {"lightseagreen", 0x20B2AA}, + {"lightskyblue", 0x87CEFA}, + {"lightslategray", 0x778899}, + {"lightslategrey", 0x778899}, + {"lightsteelblue", 0xB0C4DE}, + {"lightyellow", 0xFFFFE0}, + {"lime", 0x00FF00}, + {"limegreen", 0x32CD32}, + {"linen", 0xFAF0E6}, + {"magenta", 0xFF00FF}, + {"maroon", 0x800000}, + {"mediumaquamarine", 0x66CDAA}, + {"mediumblue", 0x0000CD}, + {"mediumorchid", 0xBA55D3}, + {"mediumpurple", 0x9370DB}, + {"mediumseagreen", 0x3CB371}, + {"mediumslateblue", 0x7B68EE}, + {"mediumspringgreen", 0x00FA9A}, + {"mediumturquoise", 0x48D1CC}, + {"mediumvioletred", 0xC71585}, + {"midnightblue", 0x191970}, + {"mintcream", 0xF5FFFA}, + {"mistyrose", 0xFFE4E1}, + {"moccasin", 0xFFE4B5}, + {"navajowhite", 0xFFDEAD}, + {"navy", 0x000080}, + {"oldlace", 0xFDF5E6}, + {"olive", 0x808000}, + {"olivedrab", 0x6B8E23}, + {"orange", 0xFFA500}, + {"orangered", 0xFF4500}, + {"orchid", 0xDA70D6}, + {"palegoldenrod", 0xEEE8AA}, + {"palegreen", 0x98FB98}, + {"paleturquoise", 0xAFEEEE}, + {"palevioletred", 0xDB7093}, + {"papayawhip", 0xFFEFD5}, + {"peachpuff", 0xFFDAB9}, + {"peru", 0xCD853F}, + {"pink", 0xFFC0CB}, + {"plum", 0xDDA0DD}, + {"powderblue", 0xB0E0E6}, + {"purple", 0x800080}, + {"red", 0xFF0000}, + {"rosybrown", 0xBC8F8F}, + {"royalblue", 0x4169E1}, + {"saddlebrown", 0x8B4513}, + {"salmon", 0xFA8072}, + {"sandybrown", 0xF4A460}, + {"seagreen", 0x2E8B57}, + {"seashell", 0xFFF5EE}, + {"sienna", 0xA0522D}, + {"silver", 0xC0C0C0}, + {"skyblue", 0x87CEEB}, + {"slateblue", 0x6A5ACD}, + {"slategray", 0x708090}, + {"slategrey", 0x708090}, + {"snow", 0xFFFAFA}, + {"springgreen", 0x00FF7F}, + {"steelblue", 0x4682B4}, + {"tan", 0xD2B48C}, + {"teal", 0x008080}, + {"thistle", 0xD8BFD8}, + {"tomato", 0xFF6347}, + {"transparent", 0x000000}, + {"turquoise", 0x40E0D0}, + {"violet", 0xEE82EE}, + {"wheat", 0xF5DEB3}, + {"white", 0xFFFFFF}, + {"whitesmoke", 0xF5F5F5}, + {"yellow", 0xFFFF00}, + {"yellowgreen", 0x9ACD32}, }; + // clang-format on auto it = namedColors.find(value); if (it != namedColors.end()) { From b598ebff19ba4c93361d0833ef2ad9d37c2414b8 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 20:33:27 +0800 Subject: [PATCH 019/678] Add rebeccapurple color keyword from CSS Color 4 specification. --- pagx/src/svg/PAGXSVGParser.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 057065c894..01a2fe2cb0 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -752,7 +752,7 @@ Color SVGParserImpl::parseColor(const std::string& value) { } } - // SVG named colors (complete list from SVG 1.1 specification). + // SVG/CSS named colors (CSS Color 3 + CSS Color 4 rebeccapurple). // clang-format off static const std::unordered_map namedColors = { {"aliceblue", 0xF0F8FF}, @@ -903,6 +903,8 @@ Color SVGParserImpl::parseColor(const std::string& value) { {"whitesmoke", 0xF5F5F5}, {"yellow", 0xFFFF00}, {"yellowgreen", 0x9ACD32}, + // CSS Color 4 addition + {"rebeccapurple", 0x663399}, }; // clang-format on From a4a07d753d042030de1ae20637ea09fe8c5150af Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 21:02:23 +0800 Subject: [PATCH 020/678] Fix SVG parser fill inheritance and add pattern support. --- pagx/src/svg/PAGXSVGParser.cpp | 200 +++++++++++++++++++++++++++---- pagx/src/svg/SVGParserInternal.h | 30 ++++- 2 files changed, 199 insertions(+), 31 deletions(-) diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 01a2fe2cb0..fb4ebc71a6 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -128,10 +128,14 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr auto rootLayer = std::make_unique(); rootLayer->name = "root"; + // Compute initial inherited style from the root element. + InheritedStyle rootStyle = {}; + rootStyle = computeInheritedStyle(root, rootStyle); + child = root->getFirstChild(); while (child) { if (child->name != "defs") { - auto layer = convertToLayer(child); + auto layer = convertToLayer(child, rootStyle); if (layer) { rootLayer->children.push_back(std::move(layer)); } @@ -143,6 +147,38 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr return _document; } +InheritedStyle SVGParserImpl::computeInheritedStyle(const std::shared_ptr& element, + const InheritedStyle& parentStyle) { + InheritedStyle style = parentStyle; + + std::string fill = getAttribute(element, "fill"); + if (!fill.empty()) { + style.fill = fill; + } + + std::string stroke = getAttribute(element, "stroke"); + if (!stroke.empty()) { + style.stroke = stroke; + } + + std::string fillOpacity = getAttribute(element, "fill-opacity"); + if (!fillOpacity.empty()) { + style.fillOpacity = fillOpacity; + } + + std::string strokeOpacity = getAttribute(element, "stroke-opacity"); + if (!strokeOpacity.empty()) { + style.strokeOpacity = strokeOpacity; + } + + std::string fillRule = getAttribute(element, "fill-rule"); + if (!fillRule.empty()) { + style.fillRule = fillRule; + } + + return style; +} + void SVGParserImpl::parseDefs(const std::shared_ptr& defsNode) { auto child = defsNode->getFirstChild(); while (child) { @@ -158,7 +194,8 @@ void SVGParserImpl::parseDefs(const std::shared_ptr& defsNode) { } } -std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element) { +std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, + const InheritedStyle& parentStyle) { const auto& tag = element->name; if (tag == "defs" || tag == "linearGradient" || tag == "radialGradient" || tag == "pattern" || @@ -166,6 +203,9 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr(); // Parse common layer attributes. @@ -197,7 +237,7 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrgetFirstChild(); while (child) { - auto childLayer = convertToLayer(child); + auto childLayer = convertToLayer(child, inheritedStyle); if (childLayer) { layer->children.push_back(std::move(childLayer)); } @@ -205,20 +245,21 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrcontents); + convertChildren(element, layer->contents, inheritedStyle); } return layer; } void SVGParserImpl::convertChildren(const std::shared_ptr& element, - std::vector>& contents) { + std::vector>& contents, + const InheritedStyle& inheritedStyle) { auto shapeElement = convertElement(element); if (shapeElement) { contents.push_back(std::move(shapeElement)); } - addFillStroke(element, contents); + addFillStroke(element, contents, inheritedStyle); } std::unique_ptr SVGParserImpl::convertElement( @@ -246,7 +287,11 @@ std::unique_ptr SVGParserImpl::convertElement( return nullptr; } -std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& element) { +std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& element, + const InheritedStyle& parentStyle) { + // Compute inherited style for this group element. + InheritedStyle inheritedStyle = computeInheritedStyle(element, parentStyle); + auto group = std::make_unique(); group->name = getAttribute(element, "id"); @@ -271,7 +316,7 @@ std::unique_ptr SVGParserImpl::convertG(const std::shared_ptrelements.push_back(std::move(childElement)); } - addFillStroke(child, group->elements); + addFillStroke(child, group->elements, inheritedStyle); child = child->getNextSibling(); } @@ -370,7 +415,8 @@ std::unique_ptr SVGParserImpl::convertPath( return path; } -std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr& element) { +std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr& element, + const InheritedStyle& inheritedStyle) { auto group = std::make_unique(); float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); @@ -405,7 +451,7 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptrelements.push_back(std::move(textSpan)); } - addFillStroke(element, group->elements); + addFillStroke(element, group->elements, inheritedStyle); return group; } @@ -497,48 +543,143 @@ std::unique_ptr SVGParserImpl::convertRadialGradient( return gradient; } +std::unique_ptr SVGParserImpl::convertPattern( + const std::shared_ptr& element) { + auto pattern = std::make_unique(); + + pattern->id = getAttribute(element, "id"); + + // SVG patterns use repeat by default. + pattern->tileModeX = TileMode::Repeat; + pattern->tileModeY = TileMode::Repeat; + + // Parse pattern dimensions for calculating tile scale. + float patternWidth = parseLength(getAttribute(element, "width"), 1.0f); + float patternHeight = parseLength(getAttribute(element, "height"), 1.0f); + + // Check patternContentUnits - objectBoundingBox means dimensions are 0-1 ratios. + std::string patternUnits = getAttribute(element, "patternContentUnits", "userSpaceOnUse"); + bool isObjectBoundingBox = (patternUnits == "objectBoundingBox"); + + // Look for image reference inside the pattern. + // Pattern may contain or direct element. + auto child = element->getFirstChild(); + while (child) { + if (child->name == "use") { + std::string href = getAttribute(child, "xlink:href"); + if (href.empty()) { + href = getAttribute(child, "href"); + } + std::string imageId = resolveUrl(href); + + // Find the referenced image in defs. + auto imgIt = _defs.find(imageId); + if (imgIt != _defs.end() && imgIt->second->name == "image") { + std::string imageHref = getAttribute(imgIt->second, "xlink:href"); + if (imageHref.empty()) { + imageHref = getAttribute(imgIt->second, "href"); + } + + // Store the image reference or data URI. + pattern->image = imageHref; + + // Get image dimensions. + float imageWidth = parseLength(getAttribute(imgIt->second, "width"), 1.0f); + float imageHeight = parseLength(getAttribute(imgIt->second, "height"), 1.0f); + + // Parse transform from the use element to get scaling. + std::string useTransform = getAttribute(child, "transform"); + if (!useTransform.empty()) { + Matrix useMatrix = parseTransform(useTransform); + // The use transform typically scales the image. + // Combine with image dimensions: scaleX = useMatrix.a * imageWidth + float scaleX = useMatrix.a * imageWidth; + float scaleY = useMatrix.d * imageHeight; + pattern->matrix = {scaleX, 0, 0, scaleY, 0, 0}; + } + } + } else if (child->name == "image") { + // Direct image element inside pattern. + std::string imageHref = getAttribute(child, "xlink:href"); + if (imageHref.empty()) { + imageHref = getAttribute(child, "href"); + } + pattern->image = imageHref; + } + child = child->getNextSibling(); + } + + return pattern; +} + void SVGParserImpl::addFillStroke(const std::shared_ptr& element, - std::vector>& contents) { + std::vector>& contents, + const InheritedStyle& inheritedStyle) { + // Determine effective fill value (element attribute overrides inherited). std::string fill = getAttribute(element, "fill"); - if (!fill.empty() && fill != "none") { - auto fillNode = std::make_unique(); + if (fill.empty()) { + fill = inheritedStyle.fill; + } - if (fill.find("url(") == 0) { + // Only add fill if we have an effective fill value that is not "none". + // If fill is empty and no inherited value, SVG default is black fill. + // But if inherited value is "none", we skip fill entirely. + if (fill != "none") { + if (fill.empty()) { + // No fill specified anywhere - use SVG default black. + auto fillNode = std::make_unique(); + fillNode->color = "#000000"; + contents.push_back(std::move(fillNode)); + } else if (fill.find("url(") == 0) { + auto fillNode = std::make_unique(); std::string refId = resolveUrl(fill); fillNode->color = "#" + refId; - // Try to inline the gradient. + // Try to inline the gradient or pattern. auto it = _defs.find(refId); if (it != _defs.end()) { if (it->second->name == "linearGradient") { fillNode->colorSource = convertLinearGradient(it->second); } else if (it->second->name == "radialGradient") { fillNode->colorSource = convertRadialGradient(it->second); + } else if (it->second->name == "pattern") { + fillNode->colorSource = convertPattern(it->second); } } + contents.push_back(std::move(fillNode)); } else { + auto fillNode = std::make_unique(); Color color = parseColor(fill); + + // Determine effective fill-opacity. std::string fillOpacity = getAttribute(element, "fill-opacity"); + if (fillOpacity.empty()) { + fillOpacity = inheritedStyle.fillOpacity; + } if (!fillOpacity.empty()) { color.alpha = std::stof(fillOpacity); } fillNode->color = color.toHexString(color.alpha < 1); - } - std::string fillRule = getAttribute(element, "fill-rule"); - if (fillRule == "evenodd") { - fillNode->fillRule = FillRule::EvenOdd; - } + // Determine effective fill-rule. + std::string fillRule = getAttribute(element, "fill-rule"); + if (fillRule.empty()) { + fillRule = inheritedStyle.fillRule; + } + if (fillRule == "evenodd") { + fillNode->fillRule = FillRule::EvenOdd; + } - contents.push_back(std::move(fillNode)); - } else if (fill.empty()) { - // SVG default is black fill. - auto fillNode = std::make_unique(); - fillNode->color = "#000000"; - contents.push_back(std::move(fillNode)); + contents.push_back(std::move(fillNode)); + } } + // Determine effective stroke value (element attribute overrides inherited). std::string stroke = getAttribute(element, "stroke"); + if (stroke.empty()) { + stroke = inheritedStyle.stroke; + } + if (!stroke.empty() && stroke != "none") { auto strokeNode = std::make_unique(); @@ -552,11 +693,18 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, strokeNode->colorSource = convertLinearGradient(it->second); } else if (it->second->name == "radialGradient") { strokeNode->colorSource = convertRadialGradient(it->second); + } else if (it->second->name == "pattern") { + strokeNode->colorSource = convertPattern(it->second); } } } else { Color color = parseColor(stroke); + + // Determine effective stroke-opacity. std::string strokeOpacity = getAttribute(element, "stroke-opacity"); + if (strokeOpacity.empty()) { + strokeOpacity = inheritedStyle.strokeOpacity; + } if (!strokeOpacity.empty()) { color.alpha = std::stof(strokeOpacity); } diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index ccbe694964..e5cd70a161 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -28,6 +28,17 @@ namespace pagx { +/** + * Inherited SVG style properties that cascade down the element tree. + */ +struct InheritedStyle { + std::string fill = ""; // Empty means not set, "none" means no fill. + std::string stroke = ""; // Empty means not set. + std::string fillOpacity = ""; // Empty means not set. + std::string strokeOpacity = ""; // Empty means not set. + std::string fillRule = ""; // Empty means not set. +}; + /** * Internal SVG parser implementation using expat-based XML DOM parsing. */ @@ -43,11 +54,14 @@ class SVGParserImpl { void parseDefs(const std::shared_ptr& defsNode); - std::unique_ptr convertToLayer(const std::shared_ptr& element); + std::unique_ptr convertToLayer(const std::shared_ptr& element, + const InheritedStyle& parentStyle); void convertChildren(const std::shared_ptr& element, - std::vector>& contents); + std::vector>& contents, + const InheritedStyle& inheritedStyle); std::unique_ptr convertElement(const std::shared_ptr& element); - std::unique_ptr convertG(const std::shared_ptr& element); + std::unique_ptr convertG(const std::shared_ptr& element, + const InheritedStyle& inheritedStyle); std::unique_ptr convertRect(const std::shared_ptr& element); std::unique_ptr convertCircle(const std::shared_ptr& element); std::unique_ptr convertEllipse(const std::shared_ptr& element); @@ -55,16 +69,22 @@ class SVGParserImpl { std::unique_ptr convertPolyline(const std::shared_ptr& element); std::unique_ptr convertPolygon(const std::shared_ptr& element); std::unique_ptr convertPath(const std::shared_ptr& element); - std::unique_ptr convertText(const std::shared_ptr& element); + std::unique_ptr convertText(const std::shared_ptr& element, + const InheritedStyle& inheritedStyle); std::unique_ptr convertUse(const std::shared_ptr& element); std::unique_ptr convertLinearGradient( const std::shared_ptr& element); std::unique_ptr convertRadialGradient( const std::shared_ptr& element); + std::unique_ptr convertPattern(const std::shared_ptr& element); void addFillStroke(const std::shared_ptr& element, - std::vector>& contents); + std::vector>& contents, + const InheritedStyle& inheritedStyle); + + InheritedStyle computeInheritedStyle(const std::shared_ptr& element, + const InheritedStyle& parentStyle); Matrix parseTransform(const std::string& value); Color parseColor(const std::string& value); From d5520e11fc411015678e962e28dd83a5fe78f2a1 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 21:37:03 +0800 Subject: [PATCH 021/678] Simplify SVG pattern handling by computing matrix from shape bounds directly. --- pagx/src/svg/PAGXSVGParser.cpp | 114 +++++++++++++++++++++++----- pagx/src/svg/SVGParserInternal.h | 6 +- pagx/src/tgfx/LayerBuilder.cpp | 126 +++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+), 19 deletions(-) diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index fb4ebc71a6..c6d5096127 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -544,7 +544,7 @@ std::unique_ptr SVGParserImpl::convertRadialGradient( } std::unique_ptr SVGParserImpl::convertPattern( - const std::shared_ptr& element) { + const std::shared_ptr& element, const Rect& shapeBounds) { auto pattern = std::make_unique(); pattern->id = getAttribute(element, "id"); @@ -553,16 +553,15 @@ std::unique_ptr SVGParserImpl::convertPattern( pattern->tileModeX = TileMode::Repeat; pattern->tileModeY = TileMode::Repeat; - // Parse pattern dimensions for calculating tile scale. + // Parse pattern dimensions from SVG attributes. float patternWidth = parseLength(getAttribute(element, "width"), 1.0f); float patternHeight = parseLength(getAttribute(element, "height"), 1.0f); - // Check patternContentUnits - objectBoundingBox means dimensions are 0-1 ratios. - std::string patternUnits = getAttribute(element, "patternContentUnits", "userSpaceOnUse"); - bool isObjectBoundingBox = (patternUnits == "objectBoundingBox"); + // Check patternContentUnits - objectBoundingBox means content coordinates are 0-1 ratios. + std::string contentUnits = getAttribute(element, "patternContentUnits", "userSpaceOnUse"); + bool isObjectBoundingBox = (contentUnits == "objectBoundingBox"); // Look for image reference inside the pattern. - // Pattern may contain or direct element. auto child = element->getFirstChild(); while (child) { if (child->name == "use") { @@ -580,22 +579,29 @@ std::unique_ptr SVGParserImpl::convertPattern( imageHref = getAttribute(imgIt->second, "href"); } - // Store the image reference or data URI. pattern->image = imageHref; - // Get image dimensions. + // Get image dimensions from SVG. float imageWidth = parseLength(getAttribute(imgIt->second, "width"), 1.0f); float imageHeight = parseLength(getAttribute(imgIt->second, "height"), 1.0f); - // Parse transform from the use element to get scaling. + // Parse transform on the use element. std::string useTransform = getAttribute(child, "transform"); - if (!useTransform.empty()) { - Matrix useMatrix = parseTransform(useTransform); - // The use transform typically scales the image. - // Combine with image dimensions: scaleX = useMatrix.a * imageWidth - float scaleX = useMatrix.a * imageWidth; - float scaleY = useMatrix.d * imageHeight; - pattern->matrix = {scaleX, 0, 0, scaleY, 0, 0}; + Matrix useMatrix = useTransform.empty() ? Matrix::Identity() : parseTransform(useTransform); + + if (isObjectBoundingBox) { + // For objectBoundingBox, the pattern content is in 0-1 space. + // The use transform (e.g., scale(0.005)) maps image to 0-1 space. + // We need to scale from image space to shape bounds. + // Final scale: imageSize * useScale * shapeBounds + float scaleX = imageWidth * useMatrix.a * shapeBounds.width; + float scaleY = imageHeight * useMatrix.d * shapeBounds.height; + pattern->matrix = Matrix::Scale(scaleX, scaleY); + } else { + // For userSpaceOnUse, use the transform directly. + float scaleX = imageWidth * useMatrix.a; + float scaleY = imageHeight * useMatrix.d; + pattern->matrix = Matrix::Scale(scaleX, scaleY); } } } else if (child->name == "image") { @@ -605,6 +611,17 @@ std::unique_ptr SVGParserImpl::convertPattern( imageHref = getAttribute(child, "href"); } pattern->image = imageHref; + + float imageWidth = parseLength(getAttribute(child, "width"), 1.0f); + float imageHeight = parseLength(getAttribute(child, "height"), 1.0f); + + if (isObjectBoundingBox) { + // Scale image to shape bounds. + pattern->matrix = Matrix::Scale(imageWidth * shapeBounds.width, + imageHeight * shapeBounds.height); + } else { + pattern->matrix = Matrix::Scale(imageWidth, imageHeight); + } } child = child->getNextSibling(); } @@ -615,6 +632,9 @@ std::unique_ptr SVGParserImpl::convertPattern( void SVGParserImpl::addFillStroke(const std::shared_ptr& element, std::vector>& contents, const InheritedStyle& inheritedStyle) { + // Get shape bounds for pattern calculations (computed once, used if needed). + Rect shapeBounds = getShapeBounds(element); + // Determine effective fill value (element attribute overrides inherited). std::string fill = getAttribute(element, "fill"); if (fill.empty()) { @@ -643,7 +663,7 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } else if (it->second->name == "radialGradient") { fillNode->colorSource = convertRadialGradient(it->second); } else if (it->second->name == "pattern") { - fillNode->colorSource = convertPattern(it->second); + fillNode->colorSource = convertPattern(it->second, shapeBounds); } } contents.push_back(std::move(fillNode)); @@ -694,7 +714,7 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } else if (it->second->name == "radialGradient") { strokeNode->colorSource = convertRadialGradient(it->second); } else if (it->second->name == "pattern") { - strokeNode->colorSource = convertPattern(it->second); + strokeNode->colorSource = convertPattern(it->second, shapeBounds); } } } else { @@ -751,6 +771,64 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } } +Rect SVGParserImpl::getShapeBounds(const std::shared_ptr& element) { + const auto& tag = element->name; + + if (tag == "rect") { + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); + float width = parseLength(getAttribute(element, "width"), _viewBoxWidth); + float height = parseLength(getAttribute(element, "height"), _viewBoxHeight); + return Rect::MakeXYWH(x, y, width, height); + } + + if (tag == "circle") { + float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); + float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); + float r = parseLength(getAttribute(element, "r"), _viewBoxWidth); + return Rect::MakeXYWH(cx - r, cy - r, r * 2, r * 2); + } + + if (tag == "ellipse") { + float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); + float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); + float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); + float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); + return Rect::MakeXYWH(cx - rx, cy - ry, rx * 2, ry * 2); + } + + if (tag == "line") { + float x1 = parseLength(getAttribute(element, "x1"), _viewBoxWidth); + float y1 = parseLength(getAttribute(element, "y1"), _viewBoxHeight); + float x2 = parseLength(getAttribute(element, "x2"), _viewBoxWidth); + float y2 = parseLength(getAttribute(element, "y2"), _viewBoxHeight); + float minX = std::min(x1, x2); + float minY = std::min(y1, y2); + float maxX = std::max(x1, x2); + float maxY = std::max(y1, y2); + return Rect::MakeXYWH(minX, minY, maxX - minX, maxY - minY); + } + + if (tag == "path") { + std::string d = getAttribute(element, "d"); + if (!d.empty()) { + auto pathData = PathData::FromSVGString(d); + return pathData.getBounds(); + } + } + + // For polyline and polygon, parse points and compute bounds. + if (tag == "polyline" || tag == "polygon") { + std::string pointsStr = getAttribute(element, "points"); + if (!pointsStr.empty()) { + auto pathData = parsePoints(pointsStr, tag == "polygon"); + return pathData.getBounds(); + } + } + + return Rect::MakeXYWH(0, 0, 0, 0); +} + Matrix SVGParserImpl::parseTransform(const std::string& value) { Matrix result = Matrix::Identity(); if (value.empty()) { diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index e5cd70a161..a8d19fcfbb 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -77,12 +77,16 @@ class SVGParserImpl { const std::shared_ptr& element); std::unique_ptr convertRadialGradient( const std::shared_ptr& element); - std::unique_ptr convertPattern(const std::shared_ptr& element); + std::unique_ptr convertPattern(const std::shared_ptr& element, + const Rect& shapeBounds); void addFillStroke(const std::shared_ptr& element, std::vector>& contents, const InheritedStyle& inheritedStyle); + // Compute shape bounds from SVG element attributes. + Rect getShapeBounds(const std::shared_ptr& element); + InheritedStyle computeInheritedStyle(const std::shared_ptr& element, const InheritedStyle& parentStyle); diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 2d4ac0530d..0d4d20c472 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -17,7 +17,9 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "pagx/LayerBuilder.h" +#include #include "pagx/PAGXSVGParser.h" +#include "tgfx/core/Data.h" #include "tgfx/core/Font.h" #include "tgfx/core/Image.h" #include "tgfx/core/Path.h" @@ -47,6 +49,82 @@ namespace pagx { +// Decode base64 encoded string to binary data. +static std::shared_ptr Base64Decode(const std::string& encodedString) { + static const std::array decodingTable = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, + 64, 64, 64, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 64, 64, 64, 64, 64, 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64}; + + size_t inputLength = encodedString.size(); + if (inputLength % 4 != 0) { + return nullptr; + } + + size_t outputLength = inputLength / 4 * 3; + if (inputLength >= 1 && encodedString[inputLength - 1] == '=') { + outputLength--; + } + if (inputLength >= 2 && encodedString[inputLength - 2] == '=') { + outputLength--; + } + + auto output = static_cast(malloc(outputLength)); + if (!output) { + return nullptr; + } + + for (size_t i = 0, j = 0; i < inputLength;) { + auto a = encodedString[i] <= 127 ? decodingTable[encodedString[i++]] : 64; + auto b = encodedString[i] <= 127 ? decodingTable[encodedString[i++]] : 64; + auto c = encodedString[i] <= 127 ? decodingTable[encodedString[i++]] : 64; + auto d = encodedString[i] <= 127 ? decodingTable[encodedString[i++]] : 64; + + uint32_t triple = (a << 3 * 6) + (b << 2 * 6) + (c << 1 * 6) + (d << 0 * 6); + + if (j < outputLength) { + output[j++] = (triple >> 2 * 8) & 0xFF; + } + if (j < outputLength) { + output[j++] = (triple >> 1 * 8) & 0xFF; + } + if (j < outputLength) { + output[j++] = (triple >> 0 * 8) & 0xFF; + } + } + + return tgfx::Data::MakeAdopted(output, outputLength, tgfx::Data::FreeProc); +} + +// Decode a data URI (e.g., "data:image/png;base64,...") to an Image. +static std::shared_ptr ImageFromDataURI(const std::string& dataURI) { + if (dataURI.find("data:") != 0) { + return nullptr; + } + + auto commaPos = dataURI.find(','); + if (commaPos == std::string::npos) { + return nullptr; + } + + auto header = dataURI.substr(0, commaPos); + auto base64Data = dataURI.substr(commaPos + 1); + + if (header.find(";base64") == std::string::npos) { + return nullptr; + } + + auto data = Base64Decode(base64Data); + if (!data) { + return nullptr; + } + + return tgfx::Image::MakeFromEncoded(data); +} + // Type converters from pagx to tgfx static tgfx::Point ToTGFX(const Point& p) { return tgfx::Point::Make(p.x, p.y); @@ -338,6 +416,10 @@ class LayerBuilderImpl { auto grad = static_cast(node); return convertRadialGradient(grad); } + case NodeType::ImagePattern: { + auto pattern = static_cast(node); + return convertImagePattern(pattern); + } default: return nullptr; } @@ -378,6 +460,50 @@ class LayerBuilderImpl { return tgfx::Gradient::MakeRadial(ToTGFX(node->center), node->radius, colors, positions); } + std::shared_ptr convertImagePattern(const ImagePatternNode* node) { + if (!node || node->image.empty()) { + return nullptr; + } + + // Load image from data URI or file path. + std::shared_ptr image = nullptr; + if (node->image.find("data:") == 0) { + image = ImageFromDataURI(node->image); + } else { + // Try as file path. + std::string imagePath = node->image; + if (!_options.basePath.empty() && imagePath[0] != '/') { + imagePath = _options.basePath + imagePath; + } + image = tgfx::Image::MakeFromFile(imagePath); + } + + if (!image) { + return nullptr; + } + + // Convert tile modes. + auto tileModeX = tgfx::TileMode::Clamp; + auto tileModeY = tgfx::TileMode::Clamp; + if (node->tileModeX == TileMode::Repeat) { + tileModeX = tgfx::TileMode::Repeat; + } else if (node->tileModeX == TileMode::Mirror) { + tileModeX = tgfx::TileMode::Mirror; + } + if (node->tileModeY == TileMode::Repeat) { + tileModeY = tgfx::TileMode::Repeat; + } else if (node->tileModeY == TileMode::Mirror) { + tileModeY = tgfx::TileMode::Mirror; + } + + auto pattern = tgfx::ImagePattern::Make(image, tileModeX, tileModeY); + if (pattern && !node->matrix.isIdentity()) { + pattern->setMatrix(ToTGFX(node->matrix)); + } + + return pattern; + } + std::shared_ptr convertTrimPath(const TrimPathNode* node) { auto trim = std::make_shared(); trim->setStart(node->start); From e197652afe372c66bac9ad53ee57d76c02fe08a7 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 22:01:12 +0800 Subject: [PATCH 022/678] Fix stroke dashes rendering and ImagePattern matrix for PAGX. --- pagx/src/svg/PAGXSVGParser.cpp | 89 +++++++++++++++++++++++----------- pagx/src/tgfx/LayerBuilder.cpp | 4 ++ 2 files changed, 66 insertions(+), 27 deletions(-) diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index c6d5096127..62865cb7ed 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -557,9 +557,20 @@ std::unique_ptr SVGParserImpl::convertPattern( float patternWidth = parseLength(getAttribute(element, "width"), 1.0f); float patternHeight = parseLength(getAttribute(element, "height"), 1.0f); - // Check patternContentUnits - objectBoundingBox means content coordinates are 0-1 ratios. - std::string contentUnits = getAttribute(element, "patternContentUnits", "userSpaceOnUse"); - bool isObjectBoundingBox = (contentUnits == "objectBoundingBox"); + // Check patternUnits - determines how pattern x/y/width/height are interpreted. + // Default is objectBoundingBox, meaning values are relative to the shape bounds. + std::string patternUnitsStr = getAttribute(element, "patternUnits", "objectBoundingBox"); + bool patternUnitsOBB = (patternUnitsStr == "objectBoundingBox"); + + // Check patternContentUnits - determines how pattern content coordinates are interpreted. + // Default is userSpaceOnUse, meaning content uses absolute coordinates. + std::string contentUnitsStr = getAttribute(element, "patternContentUnits", "userSpaceOnUse"); + bool contentUnitsOBB = (contentUnitsStr == "objectBoundingBox"); + + // Calculate the actual tile size in user space. + // When patternUnits is objectBoundingBox, pattern dimensions are 0-1 ratios of shape bounds. + float tileWidth = patternUnitsOBB ? patternWidth * shapeBounds.width : patternWidth; + float tileHeight = patternUnitsOBB ? patternHeight * shapeBounds.height : patternHeight; // Look for image reference inside the pattern. auto child = element->getFirstChild(); @@ -581,7 +592,7 @@ std::unique_ptr SVGParserImpl::convertPattern( pattern->image = imageHref; - // Get image dimensions from SVG. + // Get image display dimensions from SVG (these are the dimensions in pattern content space). float imageWidth = parseLength(getAttribute(imgIt->second, "width"), 1.0f); float imageHeight = parseLength(getAttribute(imgIt->second, "height"), 1.0f); @@ -589,20 +600,36 @@ std::unique_ptr SVGParserImpl::convertPattern( std::string useTransform = getAttribute(child, "transform"); Matrix useMatrix = useTransform.empty() ? Matrix::Identity() : parseTransform(useTransform); - if (isObjectBoundingBox) { - // For objectBoundingBox, the pattern content is in 0-1 space. - // The use transform (e.g., scale(0.005)) maps image to 0-1 space. - // We need to scale from image space to shape bounds. - // Final scale: imageSize * useScale * shapeBounds - float scaleX = imageWidth * useMatrix.a * shapeBounds.width; - float scaleY = imageHeight * useMatrix.d * shapeBounds.height; - pattern->matrix = Matrix::Scale(scaleX, scaleY); + // Calculate the image's actual size in user space (considering content units and transform). + float imageSizeInUserSpaceX = 0; + float imageSizeInUserSpaceY = 0; + if (contentUnitsOBB) { + // When patternContentUnits is objectBoundingBox, image dimensions are 0-1 ratios. + // Apply use transform (e.g., scale(0.005)) to map image to content space, + // then scale by shape bounds to get user space size. + imageSizeInUserSpaceX = imageWidth * useMatrix.a * shapeBounds.width; + imageSizeInUserSpaceY = imageHeight * useMatrix.d * shapeBounds.height; } else { - // For userSpaceOnUse, use the transform directly. - float scaleX = imageWidth * useMatrix.a; - float scaleY = imageHeight * useMatrix.d; - pattern->matrix = Matrix::Scale(scaleX, scaleY); + // When patternContentUnits is userSpaceOnUse, image dimensions are in user space. + imageSizeInUserSpaceX = imageWidth * useMatrix.a; + imageSizeInUserSpaceY = imageHeight * useMatrix.d; } + + // The ImagePattern shader tiles the original image pixels. + // We need to scale the image so it renders at the correct size within the tile. + // Since tgfx ImagePattern uses the image's original pixel dimensions as the base, + // the matrix should scale the image to match imageSizeInUserSpace. + // Note: imageWidth here is the SVG display size, which equals original pixel size + // when the image is embedded at 1:1 scale. + float scaleX = imageSizeInUserSpaceX / imageWidth; + float scaleY = imageSizeInUserSpaceY / imageHeight; + + // PAGX ImagePattern coordinates are relative to the geometry's local origin (0,0). + // SVG pattern with objectBoundingBox is relative to the shape's bounding box. + // We need to translate the pattern to align with the shape bounds. + // Matrix multiplication order: translate first, then scale (right to left). + pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y) * + Matrix::Scale(scaleX, scaleY); } } else if (child->name == "image") { // Direct image element inside pattern. @@ -615,12 +642,13 @@ std::unique_ptr SVGParserImpl::convertPattern( float imageWidth = parseLength(getAttribute(child, "width"), 1.0f); float imageHeight = parseLength(getAttribute(child, "height"), 1.0f); - if (isObjectBoundingBox) { - // Scale image to shape bounds. - pattern->matrix = Matrix::Scale(imageWidth * shapeBounds.width, - imageHeight * shapeBounds.height); + if (contentUnitsOBB) { + // Image dimensions are 0-1 ratios, scale by shape bounds. + pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y) * + Matrix::Scale(shapeBounds.width, shapeBounds.height); } else { - pattern->matrix = Matrix::Scale(imageWidth, imageHeight); + // Image dimensions are absolute, translate to shape bounds origin. + pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y); } } child = child->getNextSibling(); @@ -753,12 +781,19 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, std::string dashArray = getAttribute(element, "stroke-dasharray"); if (!dashArray.empty() && dashArray != "none") { - std::istringstream iss(dashArray); - float val = 0; - char sep = 0; - while (iss >> val) { - strokeNode->dashes.push_back(val); - iss >> sep; + // Parse dash array values, which may contain units (e.g., "2px,2px" or "2,2"). + // Use parseLength to handle both numeric values and values with units. + std::string token; + for (size_t i = 0; i <= dashArray.size(); i++) { + char c = (i < dashArray.size()) ? dashArray[i] : ','; + if (c == ',' || c == ' ' || c == '\t' || c == '\n' || c == '\r') { + if (!token.empty()) { + strokeNode->dashes.push_back(parseLength(token, _viewBoxWidth)); + token.clear(); + } + } else { + token += c; + } } } diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 0d4d20c472..3b74187c3e 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -394,6 +394,10 @@ class LayerBuilderImpl { stroke->setLineCap(ToTGFX(node->cap)); stroke->setLineJoin(ToTGFX(node->join)); stroke->setMiterLimit(node->miterLimit); + if (!node->dashes.empty()) { + stroke->setDashes(node->dashes); + stroke->setDashOffset(node->dashOffset); + } return stroke; } From 58dc4bfa06c9cbb61ce0d7e2a6b997321873f6af Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 22:11:52 +0800 Subject: [PATCH 023/678] Fix PAGX viewer build by updating include paths and LayerBuilder API usage. --- pagx/viewer/CMakeLists.txt | 3 +++ pagx/viewer/src/PAGXView.cpp | 15 ++++++--------- pagx/viewer/src/PAGXView.h | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pagx/viewer/CMakeLists.txt b/pagx/viewer/CMakeLists.txt index e2dda6e3c9..103cc2f4a9 100644 --- a/pagx/viewer/CMakeLists.txt +++ b/pagx/viewer/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.13) project(PAGXViewer) +# Include vendor tools from libpag root +include(${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/vendor_tools/vendor.cmake) + set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_CXX_STANDARD 17) diff --git a/pagx/viewer/src/PAGXView.cpp b/pagx/viewer/src/PAGXView.cpp index 69e606623e..f0437f346f 100644 --- a/pagx/viewer/src/PAGXView.cpp +++ b/pagx/viewer/src/PAGXView.cpp @@ -19,15 +19,15 @@ #include "PAGXView.h" #include #include "GridBackground.h" -#include "pagx/layers/TextLayouter.h" #include "tgfx/core/Data.h" -#include "tgfx/core/Stream.h" #include "tgfx/core/Typeface.h" using namespace emscripten; namespace pagx { +static std::vector> fallbackTypefaces; + static std::shared_ptr GetDataFromEmscripten(const val& emscriptenData) { if (emscriptenData.isUndefined()) { return nullptr; @@ -54,7 +54,7 @@ PAGXView::PAGXView(const std::string& canvasID) : canvasID(canvasID) { } void PAGXView::registerFonts(const val& fontVal, const val& emojiFontVal) { - std::vector> fallbackTypefaces; + fallbackTypefaces.clear(); auto fontData = GetDataFromEmscripten(fontVal); if (fontData) { auto typeface = tgfx::Typeface::MakeFromData(fontData, 0); @@ -69,7 +69,6 @@ void PAGXView::registerFonts(const val& fontVal, const val& emojiFontVal) { fallbackTypefaces.push_back(std::move(typeface)); } } - TextLayouter::SetFallbackTypefaces(fallbackTypefaces); } void PAGXView::loadPAGX(const val& pagxData) { @@ -77,11 +76,9 @@ void PAGXView::loadPAGX(const val& pagxData) { if (!data) { return; } - auto stream = tgfx::Stream::MakeFromData(data); - if (!stream) { - return; - } - auto content = pagx::LayerBuilder::FromStream(*stream); + LayerBuilder::Options options; + options.fallbackTypefaces = fallbackTypefaces; + auto content = pagx::LayerBuilder::FromData(data->bytes(), data->size(), options); if (!content.root) { return; } diff --git a/pagx/viewer/src/PAGXView.h b/pagx/viewer/src/PAGXView.h index d59d9e1e3c..389afb9aba 100644 --- a/pagx/viewer/src/PAGXView.h +++ b/pagx/viewer/src/PAGXView.h @@ -22,7 +22,7 @@ #include "tgfx/gpu/Recording.h" #include "tgfx/gpu/opengl/webgl/WebGLWindow.h" #include "tgfx/layers/DisplayList.h" -#include "pagx/layers/LayerBuilder.h" +#include "pagx/LayerBuilder.h" namespace pagx { From fb3e19fd58d24f5f56319c7cf5a0cbe91501faa6 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 22:24:25 +0800 Subject: [PATCH 024/678] Fix SVG viewBox to viewport transform in PAGX conversion - Add viewBox transform calculation to apply proper scale and translation when viewBox dimensions differ from viewport (width/height) dimensions - Fix parseViewBox bug that incorrectly consumed digits when skipping separators, causing "0 0 96 96" to parse as [0, 96, 6] instead of [0, 0, 96, 96] This fixes the rendering offset issue in complex3_pagx test case where the SVG had viewport=128x128px but viewBox=96x96. --- pagx/src/svg/PAGXSVGParser.cpp | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 62865cb7ed..843958712b 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -128,6 +128,30 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr auto rootLayer = std::make_unique(); rootLayer->name = "root"; + // Apply viewBox transform if the viewBox differs from the viewport dimensions. + // Default preserveAspectRatio is "xMidYMid meet": uniform scale, centered. + if (viewBox.size() >= 4) { + float viewBoxX = viewBox[0]; + float viewBoxY = viewBox[1]; + float viewBoxW = viewBox[2]; + float viewBoxH = viewBox[3]; + + if (viewBoxW > 0 && viewBoxH > 0 && + (viewBoxX != 0 || viewBoxY != 0 || viewBoxW != width || viewBoxH != height)) { + // Calculate uniform scale (meet behavior: fit inside viewport). + float scaleX = width / viewBoxW; + float scaleY = height / viewBoxH; + float scale = std::min(scaleX, scaleY); + + // Calculate translation to center content (xMidYMid). + float translateX = (width - viewBoxW * scale) / 2.0f - viewBoxX * scale; + float translateY = (height - viewBoxH * scale) / 2.0f - viewBoxY * scale; + + // Build the transform matrix: scale then translate. + rootLayer->matrix = Matrix::Translate(translateX, translateY) * Matrix::Scale(scale, scale); + } + } + // Compute initial inherited style from the root element. InheritedStyle rootStyle = {}; rootStyle = computeInheritedStyle(root, rootStyle); @@ -1225,8 +1249,6 @@ std::vector SVGParserImpl::parseViewBox(const std::string& value) { float num = 0; while (iss >> num) { result.push_back(num); - char c = 0; - iss >> c; // Skip separator. } return result; From 596e6adf4e488d4703de214fa45b3de9d5015c48 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 22:38:59 +0800 Subject: [PATCH 025/678] Add SVG text element and mask and filter and gradientTransform support to PAGX SVG parser. --- pagx/src/svg/PAGXSVGParser.cpp | 213 ++++++++++++++++++++++++++++--- pagx/src/svg/SVGParserInternal.h | 6 + 2 files changed, 201 insertions(+), 18 deletions(-) diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 843958712b..9b76be3bae 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -124,12 +124,9 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr child = child->getNextSibling(); } - // Second pass: convert elements to a root layer. - auto rootLayer = std::make_unique(); - rootLayer->name = "root"; - - // Apply viewBox transform if the viewBox differs from the viewport dimensions. - // Default preserveAspectRatio is "xMidYMid meet": uniform scale, centered. + // Check if we need a viewBox transform. + bool needsViewBoxTransform = false; + Matrix viewBoxMatrix = Matrix::Identity(); if (viewBox.size() >= 4) { float viewBoxX = viewBox[0]; float viewBoxY = viewBox[1]; @@ -148,7 +145,8 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr float translateY = (height - viewBoxH * scale) / 2.0f - viewBoxY * scale; // Build the transform matrix: scale then translate. - rootLayer->matrix = Matrix::Translate(translateX, translateY) * Matrix::Scale(scale, scale); + viewBoxMatrix = Matrix::Translate(translateX, translateY) * Matrix::Scale(scale, scale); + needsViewBoxTransform = true; } } @@ -156,18 +154,40 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr InheritedStyle rootStyle = {}; rootStyle = computeInheritedStyle(root, rootStyle); + // Collect converted layers. + std::vector> convertedLayers; child = root->getFirstChild(); while (child) { if (child->name != "defs") { auto layer = convertToLayer(child, rootStyle); if (layer) { - rootLayer->children.push_back(std::move(layer)); + convertedLayers.push_back(std::move(layer)); } } child = child->getNextSibling(); } - _document->layers.push_back(std::move(rootLayer)); + // Add collected mask layers (invisible, used as mask references). + for (auto& maskLayer : _maskLayers) { + convertedLayers.insert(convertedLayers.begin(), std::move(maskLayer)); + } + _maskLayers.clear(); + + // If viewBox transform is needed, wrap in a root layer with the transform. + // Otherwise, add layers directly to document (no root wrapper). + if (needsViewBoxTransform) { + auto rootLayer = std::make_unique(); + rootLayer->matrix = viewBoxMatrix; + for (auto& layer : convertedLayers) { + rootLayer->children.push_back(std::move(layer)); + } + _document->layers.push_back(std::move(rootLayer)); + } else { + for (auto& layer : convertedLayers) { + _document->layers.push_back(std::move(layer)); + } + } + return _document; } @@ -256,6 +276,32 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrvisible = false; } + // Handle mask attribute. + std::string maskAttr = getAttribute(element, "mask"); + if (!maskAttr.empty() && maskAttr != "none") { + std::string maskId = resolveUrl(maskAttr); + auto maskIt = _defs.find(maskId); + if (maskIt != _defs.end()) { + // Convert mask element to a mask layer. + auto maskLayer = convertMaskElement(maskIt->second, inheritedStyle); + if (maskLayer) { + layer->mask = "#" + maskLayer->id; + // Add mask layer as invisible layer to the document. + _maskLayers.push_back(std::move(maskLayer)); + } + } + } + + // Handle filter attribute. + std::string filterAttr = getAttribute(element, "filter"); + if (!filterAttr.empty() && filterAttr != "none") { + std::string filterId = resolveUrl(filterAttr); + auto filterIt = _defs.find(filterId); + if (filterIt != _defs.end()) { + convertFilterElement(filterIt->second, layer->filters); + } + } + // Convert contents. if (tag == "g" || tag == "svg") { // Group: convert children as child layers. @@ -278,6 +324,17 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, std::vector>& contents, const InheritedStyle& inheritedStyle) { + const auto& tag = element->name; + + // Handle text element specially - it returns a GroupNode with TextSpan. + if (tag == "text") { + auto textGroup = convertText(element, inheritedStyle); + if (textGroup) { + contents.push_back(std::move(textGroup)); + } + return; + } + auto shapeElement = convertElement(element); if (shapeElement) { contents.push_back(std::move(shapeElement)); @@ -519,10 +576,42 @@ std::unique_ptr SVGParserImpl::convertLinearGradient( auto gradient = std::make_unique(); gradient->id = getAttribute(element, "id"); - gradient->startPoint.x = parseLength(getAttribute(element, "x1", "0%"), 1.0f); - gradient->startPoint.y = parseLength(getAttribute(element, "y1", "0%"), 1.0f); - gradient->endPoint.x = parseLength(getAttribute(element, "x2", "100%"), 1.0f); - gradient->endPoint.y = parseLength(getAttribute(element, "y2", "0%"), 1.0f); + + // Check gradientUnits - determines how gradient coordinates are interpreted. + // Default is objectBoundingBox, meaning values are 0-1 ratios of the shape bounds. + std::string gradientUnits = getAttribute(element, "gradientUnits", "objectBoundingBox"); + bool useOBB = (gradientUnits == "objectBoundingBox"); + + // Parse gradient coordinates. + float x1 = parseLength(getAttribute(element, "x1", "0%"), 1.0f); + float y1 = parseLength(getAttribute(element, "y1", "0%"), 1.0f); + float x2 = parseLength(getAttribute(element, "x2", "100%"), 1.0f); + float y2 = parseLength(getAttribute(element, "y2", "0%"), 1.0f); + + // Parse gradientTransform. + std::string gradientTransform = getAttribute(element, "gradientTransform"); + Matrix transformMatrix = gradientTransform.empty() ? Matrix::Identity() + : parseTransform(gradientTransform); + + if (useOBB) { + // For objectBoundingBox, coordinates are normalized 0-1. + // Apply gradient transform to normalized points. + Point start = {x1, y1}; + Point end = {x2, y2}; + start = transformMatrix.mapPoint(start); + end = transformMatrix.mapPoint(end); + gradient->startPoint = start; + gradient->endPoint = end; + } else { + // For userSpaceOnUse, coordinates are in user space. + // Apply gradient transform to user space points. + Point start = {x1, y1}; + Point end = {x2, y2}; + start = transformMatrix.mapPoint(start); + end = transformMatrix.mapPoint(end); + gradient->startPoint = start; + gradient->endPoint = end; + } // Parse stops. auto child = element->getFirstChild(); @@ -546,9 +635,44 @@ std::unique_ptr SVGParserImpl::convertRadialGradient( auto gradient = std::make_unique(); gradient->id = getAttribute(element, "id"); - gradient->center.x = parseLength(getAttribute(element, "cx", "50%"), 1.0f); - gradient->center.y = parseLength(getAttribute(element, "cy", "50%"), 1.0f); - gradient->radius = parseLength(getAttribute(element, "r", "50%"), 1.0f); + + // Check gradientUnits - determines how gradient coordinates are interpreted. + std::string gradientUnits = getAttribute(element, "gradientUnits", "objectBoundingBox"); + bool useOBB = (gradientUnits == "objectBoundingBox"); + + // Parse gradient coordinates. + float cx = parseLength(getAttribute(element, "cx", "50%"), 1.0f); + float cy = parseLength(getAttribute(element, "cy", "50%"), 1.0f); + float r = parseLength(getAttribute(element, "r", "50%"), 1.0f); + + // Parse gradientTransform. + std::string gradientTransform = getAttribute(element, "gradientTransform"); + Matrix transformMatrix = gradientTransform.empty() ? Matrix::Identity() + : parseTransform(gradientTransform); + + if (useOBB || !gradientTransform.empty()) { + // Apply gradientTransform to center point. + Point center = {cx, cy}; + center = transformMatrix.mapPoint(center); + gradient->center = center; + + // For radius, we need to account for scaling in the transform. + // Use the average of X and Y scale factors. + float scaleX = std::sqrt(transformMatrix.a * transformMatrix.a + + transformMatrix.b * transformMatrix.b); + float scaleY = std::sqrt(transformMatrix.c * transformMatrix.c + + transformMatrix.d * transformMatrix.d); + gradient->radius = r * (scaleX + scaleY) / 2.0f; + + // Store the matrix for non-uniform scaling (rotation, skew, etc.). + if (!transformMatrix.isIdentity()) { + gradient->matrix = transformMatrix; + } + } else { + gradient->center.x = cx; + gradient->center.y = cy; + gradient->radius = r; + } // Parse stops. auto child = element->getFirstChild(); @@ -705,9 +829,9 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } else if (fill.find("url(") == 0) { auto fillNode = std::make_unique(); std::string refId = resolveUrl(fill); - fillNode->color = "#" + refId; // Try to inline the gradient or pattern. + // Don't set fillNode->color when using colorSource. auto it = _defs.find(refId); if (it != _defs.end()) { if (it->second->name == "linearGradient") { @@ -757,8 +881,8 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, if (stroke.find("url(") == 0) { std::string refId = resolveUrl(stroke); - strokeNode->color = "#" + refId; + // Don't set strokeNode->color when using colorSource. auto it = _defs.find(refId); if (it != _defs.end()) { if (it->second->name == "linearGradient") { @@ -1303,4 +1427,57 @@ std::string SVGParserImpl::resolveUrl(const std::string& url) { return url; } +std::unique_ptr SVGParserImpl::convertMaskElement( + const std::shared_ptr& maskElement, const InheritedStyle& parentStyle) { + auto maskLayer = std::make_unique(); + maskLayer->id = getAttribute(maskElement, "id"); + maskLayer->name = maskLayer->id; + maskLayer->visible = false; + + // Parse mask contents. + auto child = maskElement->getFirstChild(); + while (child) { + if (child->name == "rect" || child->name == "circle" || child->name == "ellipse" || + child->name == "path" || child->name == "polygon" || child->name == "polyline") { + InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); + convertChildren(child, maskLayer->contents, inheritedStyle); + } else if (child->name == "g") { + // Handle group inside mask. + InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); + auto groupChild = child->getFirstChild(); + while (groupChild) { + convertChildren(groupChild, maskLayer->contents, inheritedStyle); + groupChild = groupChild->getNextSibling(); + } + } + child = child->getNextSibling(); + } + + return maskLayer; +} + +void SVGParserImpl::convertFilterElement( + const std::shared_ptr& filterElement, + std::vector>& filters) { + // Parse filter children to find effect elements. + auto child = filterElement->getFirstChild(); + while (child) { + if (child->name == "feGaussianBlur") { + auto blurFilter = std::make_unique(); + std::string stdDeviation = getAttribute(child, "stdDeviation", "0"); + // stdDeviation can be one value (both X and Y) or two values (X Y). + std::istringstream iss(stdDeviation); + float devX = 0, devY = 0; + iss >> devX; + if (!(iss >> devY)) { + devY = devX; + } + blurFilter->blurrinessX = devX; + blurFilter->blurrinessY = devY; + filters.push_back(std::move(blurFilter)); + } + child = child->getNextSibling(); + } +} + } // namespace pagx diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index a8d19fcfbb..7c863bca6e 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -80,6 +80,11 @@ class SVGParserImpl { std::unique_ptr convertPattern(const std::shared_ptr& element, const Rect& shapeBounds); + std::unique_ptr convertMaskElement(const std::shared_ptr& maskElement, + const InheritedStyle& parentStyle); + void convertFilterElement(const std::shared_ptr& filterElement, + std::vector>& filters); + void addFillStroke(const std::shared_ptr& element, std::vector>& contents, const InheritedStyle& inheritedStyle); @@ -104,6 +109,7 @@ class SVGParserImpl { PAGXSVGParser::Options _options = {}; std::shared_ptr _document = nullptr; std::unordered_map> _defs = {}; + std::vector> _maskLayers = {}; float _viewBoxWidth = 0; float _viewBoxHeight = 0; }; From b0f0ac653e82e022040902eb1dad257a3983cc19 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 22:46:41 +0800 Subject: [PATCH 026/678] Improve PAGX SVG parser output format - Extract image resources to Resources section and use #id reference format in ImagePattern (conforming to PAGX spec) - Don't set Fill/Stroke color attribute when using colorSource (gradient/pattern) - Add image resource deduplication (same source reuses same resource ID) - Add layer merging for adjacent same-geometry shapes with Fill+Stroke - Remove root layer wrapper when no viewBox transform is needed Note: Layer merging and root wrapper removal need further debugging. --- pagx/src/svg/PAGXSVGParser.cpp | 165 ++++++++++++++++++++++++++++++- pagx/src/svg/SVGParserInternal.h | 10 ++ 2 files changed, 173 insertions(+), 2 deletions(-) diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 9b76be3bae..628dddc76a 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -173,6 +173,9 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr } _maskLayers.clear(); + // Merge adjacent layers with the same geometry (optimize Fill + Stroke into one Layer). + mergeAdjacentLayers(convertedLayers); + // If viewBox transform is needed, wrap in a root layer with the transform. // Otherwise, add layers directly to document (no root wrapper). if (needsViewBoxTransform) { @@ -738,7 +741,9 @@ std::unique_ptr SVGParserImpl::convertPattern( imageHref = getAttribute(imgIt->second, "href"); } - pattern->image = imageHref; + // Register the image resource and use the reference ID. + std::string resourceId = registerImageResource(imageHref); + pattern->image = "#" + resourceId; // Get image display dimensions from SVG (these are the dimensions in pattern content space). float imageWidth = parseLength(getAttribute(imgIt->second, "width"), 1.0f); @@ -785,7 +790,10 @@ std::unique_ptr SVGParserImpl::convertPattern( if (imageHref.empty()) { imageHref = getAttribute(child, "href"); } - pattern->image = imageHref; + + // Register the image resource and use the reference ID. + std::string resourceId = registerImageResource(imageHref); + pattern->image = "#" + resourceId; float imageWidth = parseLength(getAttribute(child, "width"), 1.0f); float imageHeight = parseLength(getAttribute(child, "height"), 1.0f); @@ -1427,6 +1435,159 @@ std::string SVGParserImpl::resolveUrl(const std::string& url) { return url; } +std::string SVGParserImpl::registerImageResource(const std::string& imageSource) { + if (imageSource.empty()) { + return ""; + } + + // Check if this image source has already been registered. + auto it = _imageSourceToId.find(imageSource); + if (it != _imageSourceToId.end()) { + return it->second; + } + + // Generate a new image ID. + std::string imageId = "image" + std::to_string(_nextImageId++); + + // Create and add the image resource to the document. + auto imageNode = std::make_unique(); + imageNode->id = imageId; + imageNode->source = imageSource; + _document->resources.push_back(std::move(imageNode)); + + // Cache the mapping. + _imageSourceToId[imageSource] = imageId; + + return imageId; +} + +// Helper function to check if two VectorElement nodes are the same geometry. +static bool isSameGeometry(const VectorElementNode* a, const VectorElementNode* b) { + if (!a || !b || a->type() != b->type()) { + return false; + } + + switch (a->type()) { + case NodeType::Rectangle: { + auto rectA = static_cast(a); + auto rectB = static_cast(b); + return rectA->center.x == rectB->center.x && rectA->center.y == rectB->center.y && + rectA->size.width == rectB->size.width && rectA->size.height == rectB->size.height && + rectA->roundness == rectB->roundness; + } + case NodeType::Ellipse: { + auto ellipseA = static_cast(a); + auto ellipseB = static_cast(b); + return ellipseA->center.x == ellipseB->center.x && ellipseA->center.y == ellipseB->center.y && + ellipseA->size.width == ellipseB->size.width && + ellipseA->size.height == ellipseB->size.height; + } + case NodeType::Path: { + auto pathA = static_cast(a); + auto pathB = static_cast(b); + return pathA->data.toSVGString() == pathB->data.toSVGString(); + } + default: + return false; + } +} + +// Check if a layer is a simple shape layer (contains exactly one geometry and one Fill or Stroke). +static bool isSimpleShapeLayer(const LayerNode* layer, const VectorElementNode*& outGeometry, + const VectorElementNode*& outPainter) { + if (!layer || layer->contents.size() != 2) { + return false; + } + if (!layer->children.empty() || !layer->filters.empty() || !layer->styles.empty()) { + return false; + } + if (!layer->matrix.isIdentity() || layer->alpha != 1.0f) { + return false; + } + + const auto* first = layer->contents[0].get(); + const auto* second = layer->contents[1].get(); + + // Check if first is geometry and second is painter. + bool firstIsGeometry = (first->type() == NodeType::Rectangle || + first->type() == NodeType::Ellipse || first->type() == NodeType::Path); + bool secondIsPainter = + (second->type() == NodeType::Fill || second->type() == NodeType::Stroke); + + if (firstIsGeometry && secondIsPainter) { + outGeometry = first; + outPainter = second; + return true; + } + return false; +} + +void SVGParserImpl::mergeAdjacentLayers(std::vector>& layers) { + if (layers.size() < 2) { + return; + } + + std::vector> merged; + size_t i = 0; + + while (i < layers.size()) { + const VectorElementNode* geomA = nullptr; + const VectorElementNode* painterA = nullptr; + + if (isSimpleShapeLayer(layers[i].get(), geomA, painterA)) { + // Check if the next layer has the same geometry. + if (i + 1 < layers.size()) { + const VectorElementNode* geomB = nullptr; + const VectorElementNode* painterB = nullptr; + + if (isSimpleShapeLayer(layers[i + 1].get(), geomB, painterB) && + isSameGeometry(geomA, geomB)) { + // Merge: one has Fill, the other has Stroke. + bool aHasFill = (painterA->type() == NodeType::Fill); + bool bHasFill = (painterB->type() == NodeType::Fill); + + if (aHasFill != bHasFill) { + // Create merged layer. + auto mergedLayer = std::make_unique(); + + // Keep geometry from first layer. + auto geomClone = layers[i]->contents[0]->clone(); + mergedLayer->contents.push_back( + std::unique_ptr(static_cast(geomClone.release()))); + + // Add Fill first, then Stroke (standard order). + if (aHasFill) { + auto fillClone = layers[i]->contents[1]->clone(); + mergedLayer->contents.push_back( + std::unique_ptr(static_cast(fillClone.release()))); + auto strokeClone = layers[i + 1]->contents[1]->clone(); + mergedLayer->contents.push_back( + std::unique_ptr(static_cast(strokeClone.release()))); + } else { + auto fillClone = layers[i + 1]->contents[1]->clone(); + mergedLayer->contents.push_back( + std::unique_ptr(static_cast(fillClone.release()))); + auto strokeClone = layers[i]->contents[1]->clone(); + mergedLayer->contents.push_back( + std::unique_ptr(static_cast(strokeClone.release()))); + } + + merged.push_back(std::move(mergedLayer)); + i += 2; // Skip both layers. + continue; + } + } + } + } + + // No merge, keep the layer as is. + merged.push_back(std::move(layers[i])); + i++; + } + + layers = std::move(merged); +} + std::unique_ptr SVGParserImpl::convertMaskElement( const std::shared_ptr& maskElement, const InheritedStyle& parentStyle) { auto maskLayer = std::make_unique(); diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 7c863bca6e..1b9df53769 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -106,10 +106,20 @@ class SVGParserImpl { std::string getAttribute(const std::shared_ptr& node, const std::string& name, const std::string& defaultValue = "") const; + // Register an image resource and return its reference ID (e.g., "#image0"). + // If the image source (data URI or path) has already been registered, returns the existing ID. + std::string registerImageResource(const std::string& imageSource); + + // Merge adjacent layers that have the same shape geometry. + // This optimizes the output by combining Fill and Stroke for identical shapes into one Layer. + void mergeAdjacentLayers(std::vector>& layers); + PAGXSVGParser::Options _options = {}; std::shared_ptr _document = nullptr; std::unordered_map> _defs = {}; std::vector> _maskLayers = {}; + std::unordered_map _imageSourceToId = {}; // Maps image source to resource ID. + int _nextImageId = 0; float _viewBoxWidth = 0; float _viewBoxHeight = 0; }; From 684a07e29c676e713e74cccee075bdc0740a2a0b Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 09:28:55 +0800 Subject: [PATCH 027/678] Fix ImagePattern not rendering when using resource reference format. --- pagx/src/tgfx/LayerBuilder.cpp | 35 ++++++++++++++++++++++++++++++++-- pagx/viewer/server.js | 2 +- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 3b74187c3e..f132f8a844 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -193,6 +193,9 @@ class LayerBuilderImpl { } PAGXContent build(const PAGXDocument& document) { + // Cache resources for later lookup. + _resources = &document.resources; + PAGXContent content; content.width = document.width; content.height = document.height; @@ -207,6 +210,7 @@ class LayerBuilderImpl { } content.root = rootLayer; + _resources = nullptr; return content; } @@ -469,9 +473,13 @@ class LayerBuilderImpl { return nullptr; } - // Load image from data URI or file path. + // Load image from data URI, resource reference, or file path. std::shared_ptr image = nullptr; - if (node->image.find("data:") == 0) { + if (node->image.find("#") == 0) { + // Resource reference (e.g., "#image0") - look up in document resources. + std::string resourceId = node->image.substr(1); + image = findImageResource(resourceId); + } else if (node->image.find("data:") == 0) { image = ImageFromDataURI(node->image); } else { // Try as file path. @@ -508,6 +516,28 @@ class LayerBuilderImpl { return pattern; } + std::shared_ptr findImageResource(const std::string& resourceId) { + if (!_resources) { + return nullptr; + } + for (const auto& resource : *_resources) { + if (resource->type() == NodeType::Image) { + auto imageNode = static_cast(resource.get()); + if (imageNode->id == resourceId) { + if (imageNode->source.find("data:") == 0) { + return ImageFromDataURI(imageNode->source); + } else { + std::string imagePath = imageNode->source; + if (!_options.basePath.empty() && imagePath[0] != '/') { + imagePath = _options.basePath + imagePath; + } + return tgfx::Image::MakeFromFile(imagePath); + } + } + } + } + return nullptr; + } std::shared_ptr convertTrimPath(const TrimPathNode* node) { auto trim = std::make_shared(); trim->setStart(node->start); @@ -624,6 +654,7 @@ class LayerBuilderImpl { } LayerBuilder::Options _options = {}; + const std::vector>* _resources = nullptr; }; // Public API implementation diff --git a/pagx/viewer/server.js b/pagx/viewer/server.js index ce09a7e43f..57abc5f4da 100644 --- a/pagx/viewer/server.js +++ b/pagx/viewer/server.js @@ -45,7 +45,7 @@ app.get('/', (req, res) => { res.redirect('/index.html'); }); -const port = 8082; +const port = 8080; app.listen(port, () => { const url = `http://localhost:${port}/`; const start = (process.platform === 'darwin' ? 'open' : 'start'); From 2bf70b64ab03165dea9b01bdd5e77c0fcf6753f4 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 09:35:35 +0800 Subject: [PATCH 028/678] Align PAGX spec default values with tgfx implementation. --- pagx/docs/pagx_spec.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index eb54ec2d28..bfd786dfe0 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -846,7 +846,7 @@ VectorElement 按**文档顺序**依次处理,文档中靠前的元素先处 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `center` | point | 0,0 | 中心点 | -| `size` | size | 0,0 | 尺寸 "width,height" | +| `size` | size | 100,100 | 尺寸 "width,height" | | `roundness` | float | 0 | 圆角半径 | | `reversed` | bool | false | 反转路径方向 | @@ -875,7 +875,7 @@ rect.bottom = center.y + size.height / 2 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `center` | point | 0,0 | 中心点 | -| `size` | size | 0,0 | 尺寸 "width,height" | +| `size` | size | 100,100 | 尺寸 "width,height" | | `reversed` | bool | false | 反转路径方向 | **计算规则**: @@ -1055,7 +1055,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `color` | color/idref | - | 颜色值或颜色源引用 | -| `width` | float | 1 | 描边宽度 | +| `width` | float | 2 | 描边宽度 | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1) | | `cap` | LineCap | butt | 线帽样式(见下方) | @@ -1155,7 +1155,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `radius` | float | 0 | 圆角半径 | +| `radius` | float | 10 | 圆角半径 | **处理规则**: - 只影响尖角(非平滑连接的顶点) @@ -1272,7 +1272,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `anchorPoint` | point | 0.5,0.5 | 锚点(归一化) | +| `anchorPoint` | point | 0,0 | 锚点(归一化) | | `position` | point | 0,0 | 位置偏移 | | `rotation` | float | 0 | 旋转 | | `scale` | point | 1,1 | 缩放 | From f596c0deb2752cff3348194cc591427886effcf7 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 09:39:49 +0800 Subject: [PATCH 029/678] Fix TextModifier anchorPoint description from normalized to offset. --- pagx/docs/pagx_spec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index bfd786dfe0..1e96d3c9a9 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1265,14 +1265,14 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 对选定范围内的字形应用变换和样式覆盖。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `anchorPoint` | point | 0,0 | 锚点(归一化) | +| `anchorPoint` | point | 0,0 | 锚点偏移 | | `position` | point | 0,0 | 位置偏移 | | `rotation` | float | 0 | 旋转 | | `scale` | point | 1,1 | 缩放 | From 3d75a62a24aa3d4d1223180b0ed6234d3aa62c6c Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 09:41:08 +0800 Subject: [PATCH 030/678] Add default color value for Fill and Stroke. --- pagx/docs/pagx_spec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 1e96d3c9a9..8d6dbba3e5 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1014,7 +1014,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color/idref | - | 颜色值或颜色源引用 | +| `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1) | | `fillRule` | FillRule | winding | 填充规则(见下方) | @@ -1054,7 +1054,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color/idref | - | 颜色值或颜色源引用 | +| `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | | `width` | float | 2 | 描边宽度 | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1) | From d3e4102ad0fc4c86ac7587b626dd90b3d223b9e7 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 10:01:51 +0800 Subject: [PATCH 031/678] Change Stroke default width from 2 to 1 to align with SVG specification. --- pagx/docs/pagx_spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 8d6dbba3e5..a11651f6a1 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1055,7 +1055,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | -| `width` | float | 2 | 描边宽度 | +| `width` | float | 1 | 描边宽度 | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1) | | `cap` | LineCap | butt | 线帽样式(见下方) | From d6376e63846a7d5c864bee79cbb020a77af60ba1 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 10:08:25 +0800 Subject: [PATCH 032/678] Align PAGXNode default values with spec document for Rectangle and Ellipse size and RoundCorner radius and TextModifier anchorPoint. --- pagx/include/pagx/PAGXNode.h | 8 ++++---- pagx/src/PAGXXMLWriter.cpp | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index b6e7567b4f..8fc6d0b125 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -280,7 +280,7 @@ class VectorElementNode : public PAGXNode {}; */ struct RectangleNode : public VectorElementNode { Point center = {}; - Size size = {}; + Size size = {100, 100}; float roundness = 0; bool reversed = false; @@ -298,7 +298,7 @@ struct RectangleNode : public VectorElementNode { */ struct EllipseNode : public VectorElementNode { Point center = {}; - Size size = {}; + Size size = {100, 100}; bool reversed = false; NodeType type() const override { @@ -474,7 +474,7 @@ struct TrimPathNode : public VectorElementNode { * Round corner modifier. */ struct RoundCornerNode : public VectorElementNode { - float radius = 0; + float radius = 10; NodeType type() const override { return NodeType::RoundCorner; @@ -533,7 +533,7 @@ struct RangeSelectorNode : public PAGXNode { * Text modifier. */ struct TextModifierNode : public VectorElementNode { - Point anchorPoint = {0.5f, 0.5f}; + Point anchorPoint = {}; Point position = {}; float rotation = 0; Point scale = {1, 1}; diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index 3701160f29..e95244ae18 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -307,7 +307,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { if (rect->center.x != 0 || rect->center.y != 0) { xml.addAttribute("center", pointToString(rect->center)); } - if (rect->size.width != 0 || rect->size.height != 0) { + if (rect->size.width != 100 || rect->size.height != 100) { xml.addAttribute("size", sizeToString(rect->size)); } xml.addAttribute("roundness", rect->roundness); @@ -321,7 +321,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { if (ellipse->center.x != 0 || ellipse->center.y != 0) { xml.addAttribute("center", pointToString(ellipse->center)); } - if (ellipse->size.width != 0 || ellipse->size.height != 0) { + if (ellipse->size.width != 100 || ellipse->size.height != 100) { xml.addAttribute("size", sizeToString(ellipse->size)); } xml.addAttribute("reversed", ellipse->reversed); @@ -446,7 +446,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::RoundCorner: { auto round = static_cast(node); xml.openElement("RoundCorner"); - xml.addAttribute("radius", round->radius); + xml.addAttribute("radius", round->radius, 10.0f); xml.closeElementSelfClosing(); break; } @@ -462,7 +462,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::TextModifier: { auto modifier = static_cast(node); xml.openElement("TextModifier"); - if (modifier->anchorPoint.x != 0.5f || modifier->anchorPoint.y != 0.5f) { + if (modifier->anchorPoint.x != 0 || modifier->anchorPoint.y != 0) { xml.addAttribute("anchorPoint", pointToString(modifier->anchorPoint)); } if (modifier->position.x != 0 || modifier->position.y != 0) { From 8a84678352c6d04ee8d68709a22c6c65e8d62e35 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 10:15:15 +0800 Subject: [PATCH 033/678] Add textAnchor attribute to TextSpan for text horizontal alignment. --- pagx/docs/pagx_spec.md | 14 ++++++++++++-- pagx/include/pagx/PAGXNode.h | 1 + pagx/include/pagx/PAGXTypes.h | 12 ++++++++++++ pagx/src/PAGXTypes.cpp | 5 +++++ pagx/src/PAGXXMLParser.cpp | 1 + pagx/src/PAGXXMLWriter.cpp | 3 +++ pagx/src/svg/PAGXSVGParser.cpp | 10 ++++++++++ pagx/src/tgfx/LayerBuilder.cpp | 14 +++++++++++++- 8 files changed, 57 insertions(+), 3 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index a11651f6a1..d82ab1099c 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -959,7 +959,7 @@ y = center.y + outerRadius * sin(angle) 文本片段提供文本内容的几何形状。一个 TextSpan 经过塑形后会产生**字形列表**(多个字形),而非单一 Path。 ```xml - + Hello World ``` @@ -974,12 +974,22 @@ y = center.y + outerRadius * sin(angle) | `fontStyle` | enum | normal | normal 或 italic | | `tracking` | float | 0 | 字距 | | `baselineShift` | float | 0 | 基线偏移 | +| `textAnchor` | TextAnchor | start | 文本锚点(见下方) | + +**TextAnchor(文本锚点)**: + +| 值 | 说明 | +|------|------| +| `start` | 文本从 x 位置开始(默认) | +| `middle` | 文本以 x 位置为中心 | +| `end` | 文本以 x 位置为结束 | **处理流程**: 1. 根据 `font`、`fontSize`、`fontWeight`、`fontStyle` 查找字体 2. 应用 `tracking`(字距调整) 3. 将文本塑形(shaping)为字形列表 -4. 按 `x`、`y` 位置放置 +4. 根据 `textAnchor` 计算水平偏移 +5. 按 `x`、`y` 位置和偏移放置 **字体回退**:当指定字体不可用时,按平台默认字体回退链选择替代字体。 diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index 8fc6d0b125..ad06294a11 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -361,6 +361,7 @@ struct TextSpanNode : public VectorElementNode { FontStyle fontStyle = FontStyle::Normal; float tracking = 0; float baselineShift = 0; + TextAnchor textAnchor = TextAnchor::Start; std::string text = {}; NodeType type() const override { diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h index bc73918ca7..691dffe7a9 100644 --- a/pagx/include/pagx/PAGXTypes.h +++ b/pagx/include/pagx/PAGXTypes.h @@ -437,6 +437,15 @@ enum class RepeaterOrder { AboveOriginal }; +/** + * Text anchor for horizontal alignment. + */ +enum class TextAnchor { + Start, + Middle, + End +}; + //============================================================================== // Enum string conversion utilities //============================================================================== @@ -504,4 +513,7 @@ SelectorMode SelectorModeFromString(const std::string& str); std::string RepeaterOrderToString(RepeaterOrder order); RepeaterOrder RepeaterOrderFromString(const std::string& str); +std::string TextAnchorToString(TextAnchor anchor); +TextAnchor TextAnchorFromString(const std::string& str); + } // namespace pagx diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp index 448d7be042..10edfb69c7 100644 --- a/pagx/src/PAGXTypes.cpp +++ b/pagx/src/PAGXTypes.cpp @@ -351,6 +351,11 @@ DEFINE_ENUM_CONVERSION(RepeaterOrder, {RepeaterOrder::BelowOriginal, "belowOriginal"}, {RepeaterOrder::AboveOriginal, "aboveOriginal"}) +DEFINE_ENUM_CONVERSION(TextAnchor, + {TextAnchor::Start, "start"}, + {TextAnchor::Middle, "middle"}, + {TextAnchor::End, "end"}) + #undef DEFINE_ENUM_CONVERSION } // namespace pagx diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 5d46a5062b..028eb235a4 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -586,6 +586,7 @@ std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) textSpan->fontStyle = FontStyleFromString(getAttribute(node, "fontStyle", "normal")); textSpan->tracking = getFloatAttribute(node, "tracking", 0); textSpan->baselineShift = getFloatAttribute(node, "baselineShift", 0); + textSpan->textAnchor = TextAnchorFromString(getAttribute(node, "textAnchor", "start")); textSpan->text = node->text; return textSpan; } diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index e95244ae18..5a82514d1f 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -368,6 +368,9 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { } xml.addAttribute("tracking", text->tracking); xml.addAttribute("baselineShift", text->baselineShift); + if (text->textAnchor != TextAnchor::Start) { + xml.addAttribute("textAnchor", TextAnchorToString(text->textAnchor)); + } xml.closeElementStart(); xml.addTextContent(text->text); xml.closeElement(); diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 628dddc76a..c14efca137 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -506,6 +506,15 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptrgetFirstChild(); @@ -521,6 +530,7 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptrx = x; textSpan->y = y; textSpan->text = textContent; + textSpan->textAnchor = textAnchor; std::string fontFamily = getAttribute(element, "font-family"); if (!fontFamily.empty()) { diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index f132f8a844..2bbe9af6c0 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -336,7 +336,6 @@ class LayerBuilderImpl { std::shared_ptr convertTextSpan(const TextSpanNode* node) { auto textSpan = std::make_shared(); - textSpan->setPosition(tgfx::Point::Make(node->x, node->y)); std::shared_ptr typeface = nullptr; if (!node->font.empty() && !_options.fallbackTypefaces.empty()) { @@ -350,12 +349,25 @@ class LayerBuilderImpl { if (!typeface && !_options.fallbackTypefaces.empty()) { typeface = _options.fallbackTypefaces[0]; } + + float xOffset = 0; if (typeface && !node->text.empty()) { auto font = tgfx::Font(typeface, node->fontSize); auto textBlob = tgfx::TextBlob::MakeFrom(node->text, font); textSpan->setTextBlob(textBlob); + + // Apply text-anchor offset based on text width. + if (textBlob && node->textAnchor != TextAnchor::Start) { + auto bounds = textBlob->getTightBounds(); + if (node->textAnchor == TextAnchor::Middle) { + xOffset = -bounds.width() * 0.5f; + } else if (node->textAnchor == TextAnchor::End) { + xOffset = -bounds.width(); + } + } } + textSpan->setPosition(tgfx::Point::Make(node->x + xOffset, node->y)); return textSpan; } From a2fbe4d36bc2b3f7df618044ff143b16d970d52e Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 10:17:39 +0800 Subject: [PATCH 034/678] Only write id attribute for nodes that are referenced by other nodes. --- pagx/src/PAGXXMLWriter.cpp | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index 5a82514d1f..734eb3cc65 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -136,7 +136,7 @@ class XMLBuilder { } }; -static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node); +static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool writeId = true); static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node); static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node); static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node); @@ -181,12 +181,14 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& s } } -static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { +static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool writeId) { switch (node->type()) { case NodeType::SolidColor: { auto solid = static_cast(node); xml.openElement("SolidColor"); - xml.addAttribute("id", solid->id); + if (writeId) { + xml.addAttribute("id", solid->id); + } xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); xml.closeElementSelfClosing(); break; @@ -194,7 +196,9 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { case NodeType::LinearGradient: { auto grad = static_cast(node); xml.openElement("LinearGradient"); - xml.addAttribute("id", grad->id); + if (writeId) { + xml.addAttribute("id", grad->id); + } if (grad->startPoint.x != 0 || grad->startPoint.y != 0) { xml.addAttribute("startPoint", pointToString(grad->startPoint)); } @@ -216,7 +220,9 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { case NodeType::RadialGradient: { auto grad = static_cast(node); xml.openElement("RadialGradient"); - xml.addAttribute("id", grad->id); + if (writeId) { + xml.addAttribute("id", grad->id); + } if (grad->center.x != 0 || grad->center.y != 0) { xml.addAttribute("center", pointToString(grad->center)); } @@ -236,7 +242,9 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { case NodeType::ConicGradient: { auto grad = static_cast(node); xml.openElement("ConicGradient"); - xml.addAttribute("id", grad->id); + if (writeId) { + xml.addAttribute("id", grad->id); + } if (grad->center.x != 0 || grad->center.y != 0) { xml.addAttribute("center", pointToString(grad->center)); } @@ -257,7 +265,9 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { case NodeType::DiamondGradient: { auto grad = static_cast(node); xml.openElement("DiamondGradient"); - xml.addAttribute("id", grad->id); + if (writeId) { + xml.addAttribute("id", grad->id); + } if (grad->center.x != 0 || grad->center.y != 0) { xml.addAttribute("center", pointToString(grad->center)); } @@ -277,7 +287,9 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node) { case NodeType::ImagePattern: { auto pattern = static_cast(node); xml.openElement("ImagePattern"); - xml.addAttribute("id", pattern->id); + if (writeId) { + xml.addAttribute("id", pattern->id); + } xml.addAttribute("image", pattern->image); if (pattern->tileModeX != TileMode::Clamp) { xml.addAttribute("tileModeX", TileModeToString(pattern->tileModeX)); @@ -392,7 +404,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { } if (fill->colorSource) { xml.closeElementStart(); - writeColorSource(xml, fill->colorSource.get()); + writeColorSource(xml, fill->colorSource.get(), false); xml.closeElement(); } else { xml.closeElementSelfClosing(); @@ -427,7 +439,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { } if (stroke->colorSource) { xml.closeElementStart(); - writeColorSource(xml, stroke->colorSource.get()); + writeColorSource(xml, stroke->colorSource.get(), false); xml.closeElement(); } else { xml.closeElementSelfClosing(); @@ -754,7 +766,9 @@ static void writeResource(XMLBuilder& xml, const ResourceNode* node) { static void writeLayer(XMLBuilder& xml, const LayerNode* node) { xml.openElement("Layer"); - xml.addAttribute("id", node->id); + if (!node->id.empty()) { + xml.addAttribute("id", node->id); + } xml.addAttribute("name", node->name); xml.addAttribute("visible", node->visible, true); xml.addAttribute("alpha", node->alpha, 1.0f); From f5dc589fc7d3930c1ba7256630a89f65ec069ff9 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 10:20:43 +0800 Subject: [PATCH 035/678] Always write required attributes even when their values equal zero. --- pagx/src/PAGXXMLWriter.cpp | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index 734eb3cc65..a2738881e6 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -45,6 +45,14 @@ class XMLBuilder { } } + void addRequiredAttribute(const std::string& name, float value) { + buffer << " " << name << "=\"" << formatFloat(value) << "\""; + } + + void addRequiredAttribute(const std::string& name, const std::string& value) { + buffer << " " << name << "=\"" << escapeXML(value) << "\""; + } + void addAttribute(const std::string& name, int value, int defaultValue = 0) { if (value != defaultValue) { buffer << " " << name << "=\"" << value << "\""; @@ -175,8 +183,8 @@ static std::string floatListToString(const std::vector& values) { static void writeColorStops(XMLBuilder& xml, const std::vector& stops) { for (const auto& stop : stops) { xml.openElement("ColorStop"); - xml.addAttribute("offset", stop.offset); - xml.addAttribute("color", stop.color.toHexString(stop.color.alpha < 1.0f)); + xml.addRequiredAttribute("offset", stop.offset); + xml.addRequiredAttribute("color", stop.color.toHexString(stop.color.alpha < 1.0f)); xml.closeElementSelfClosing(); } } @@ -202,9 +210,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool if (grad->startPoint.x != 0 || grad->startPoint.y != 0) { xml.addAttribute("startPoint", pointToString(grad->startPoint)); } - if (grad->endPoint.x != 0 || grad->endPoint.y != 0) { - xml.addAttribute("endPoint", pointToString(grad->endPoint)); - } + xml.addRequiredAttribute("endPoint", pointToString(grad->endPoint)); if (!grad->matrix.isIdentity()) { xml.addAttribute("matrix", grad->matrix.toString()); } @@ -226,7 +232,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool if (grad->center.x != 0 || grad->center.y != 0) { xml.addAttribute("center", pointToString(grad->center)); } - xml.addAttribute("radius", grad->radius); + xml.addRequiredAttribute("radius", grad->radius); if (!grad->matrix.isIdentity()) { xml.addAttribute("matrix", grad->matrix.toString()); } @@ -271,7 +277,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool if (grad->center.x != 0 || grad->center.y != 0) { xml.addAttribute("center", pointToString(grad->center)); } - xml.addAttribute("halfDiagonal", grad->halfDiagonal); + xml.addRequiredAttribute("halfDiagonal", grad->halfDiagonal); if (!grad->matrix.isIdentity()) { xml.addAttribute("matrix", grad->matrix.toString()); } @@ -542,8 +548,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::TextLayout: { auto layout = static_cast(node); xml.openElement("TextLayout"); - xml.addAttribute("width", layout->width); - xml.addAttribute("height", layout->height); + xml.addRequiredAttribute("width", layout->width); + xml.addRequiredAttribute("height", layout->height); if (layout->textAlign != TextAlign::Left) { xml.addAttribute("align", TextAlignToString(layout->textAlign)); } @@ -669,8 +675,8 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node) { case NodeType::BlurFilter: { auto filter = static_cast(node); xml.openElement("BlurFilter"); - xml.addAttribute("blurrinessX", filter->blurrinessX); - xml.addAttribute("blurrinessY", filter->blurrinessY); + xml.addRequiredAttribute("blurrinessX", filter->blurrinessX); + xml.addRequiredAttribute("blurrinessY", filter->blurrinessY); if (filter->tileMode != TileMode::Decal) { xml.addAttribute("tileMode", TileModeToString(filter->tileMode)); } @@ -738,8 +744,8 @@ static void writeResource(XMLBuilder& xml, const ResourceNode* node) { auto comp = static_cast(node); xml.openElement("Composition"); xml.addAttribute("id", comp->id); - xml.addAttribute("width", comp->width); - xml.addAttribute("height", comp->height); + xml.addRequiredAttribute("width", static_cast(comp->width)); + xml.addRequiredAttribute("height", static_cast(comp->height)); if (comp->layers.empty()) { xml.closeElementSelfClosing(); } else { From be28bda43a56681137c6466674d6c415fe8cf971 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 10:22:28 +0800 Subject: [PATCH 036/678] Add PathData resource type for path data sharing and reference. --- pagx/docs/pagx_spec.md | 59 +++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index d82ab1099c..ea8eabcbc8 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -136,7 +136,20 @@ PAGX 支持多种颜色格式: **示例**:`"M 0 0 L 100 0 L 100 100 Z"` -### 2.8 图片(Image) +### 2.8 PathData(路径数据资源) + +PathData 定义可在文档中引用的路径数据,支持路径复用。 + +```xml + +``` + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `id` | string | 是 | 唯一标识 | +| `data` | string | 是 | SVG 路径数据(语法见 2.7 节) | + +### 2.9 Image(图片) 图片资源定义可在文档中引用的位图数据。 @@ -155,7 +168,7 @@ PAGX 支持多种颜色格式: **支持格式**:PNG、JPEG、WebP、GIF -### 2.9 颜色源(Color Source) +### 2.10 颜色源(Color Source) 颜色源定义可用于渲染的颜色,支持两种定义方式: @@ -167,7 +180,7 @@ PAGX 支持多种颜色格式: - 被多处引用的颜色源应定义在 Resources 中以便复用 - ImagePattern 使用 objectBoundingBox 时(tile 尺寸依赖形状尺寸),通常需要内联定义,因为不同形状需要不同的 matrix -#### 2.9.1 SolidColor(纯色) +#### 2.10.1 SolidColor(纯色) ```xml @@ -178,7 +191,7 @@ PAGX 支持多种颜色格式: | `id` | string | 是(Resources 中) | 唯一标识 | | `color` | color | 是 | 颜色值 | -#### 2.9.2 LinearGradient(线性渐变) +#### 2.10.2 LinearGradient(线性渐变) 线性渐变沿起点到终点的方向插值。 @@ -198,7 +211,7 @@ PAGX 支持多种颜色格式: **计算**:对于点 P,其颜色由 P 在起点-终点连线上的投影位置决定。 -#### 2.9.3 RadialGradient(径向渐变) +#### 2.10.3 RadialGradient(径向渐变) 径向渐变从中心向外辐射。 @@ -218,7 +231,7 @@ PAGX 支持多种颜色格式: **计算**:对于点 P,其颜色由 `distance(P, center) / radius` 决定。 -#### 2.9.4 ConicGradient(锥形渐变) +#### 2.10.4 ConicGradient(锥形渐变) 锥形渐变(也称扫描渐变)沿圆周方向插值。 @@ -239,7 +252,7 @@ PAGX 支持多种颜色格式: **计算**:对于点 P,其颜色由 `atan2(P.y - center.y, P.x - center.x)` 在 `[startAngle, endAngle]` 范围内的比例决定。 -#### 2.9.5 DiamondGradient(菱形渐变) +#### 2.10.5 DiamondGradient(菱形渐变) 菱形渐变从中心向四角辐射。 @@ -259,7 +272,7 @@ PAGX 支持多种颜色格式: **计算**:对于点 P,其颜色由曼哈顿距离 `(|P.x - center.x| + |P.y - center.y|) / halfDiagonal` 决定。 -#### 2.9.6 ColorStop(渐变色标) +#### 2.10.6 ColorStop(渐变色标) ```xml @@ -280,7 +293,7 @@ PAGX 支持多种颜色格式: - 如果没有 `offset = 1` 的色标,使用最后一个色标的颜色填充 - **渐变变换**:`matrix` 属性对渐变坐标系应用变换 -#### 2.9.8 颜色源坐标系统 +#### 2.10.7 颜色源坐标系统 所有颜色源(渐变、图案)的坐标系是**相对于几何元素的局部坐标系原点**。 @@ -309,7 +322,7 @@ PAGX 支持多种颜色格式: - 对该图层应用 `scale(2, 2)` 变换:矩形变为 200×200,渐变也随之放大,视觉效果保持一致 - 直接将 Rectangle 的 size 改为 200,200:矩形变为 200×200,但渐变坐标不变,只覆盖矩形的左半部分 -#### 2.9.7 ImagePattern(图片图案) +#### 2.10.8 ImagePattern(图片图案) 图片图案使用图片作为颜色源。 @@ -396,11 +409,12 @@ PAGX 使用标准的 2D 笛卡尔坐标系: ### 3.3 Resources(资源区) -`` 定义可复用的资源,包括图片、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `#id` 形式引用。 +`` 定义可复用的资源,包括图片、路径数据、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `#id` 形式引用。 ```xml + @@ -413,11 +427,15 @@ PAGX 使用标准的 2D 笛卡尔坐标系: #### 3.3.1 Image(图片) -图片资源定义见 2.8 节。 +图片资源定义见 2.9 节。 -#### 3.3.2 颜色源 +#### 3.3.2 PathData(路径数据) -Resources 中可定义以下颜色源类型(详见 2.9 节): +路径数据资源定义见 2.8 节。可被 Path 元素和 TextPath 修改器引用。 + +#### 3.3.3 颜色源 + +Resources 中可定义以下颜色源类型(详见 2.10 节): - `SolidColor`:纯色 - `LinearGradient`:线性渐变 @@ -426,7 +444,7 @@ Resources 中可定义以下颜色源类型(详见 2.9 节): - `DiamondGradient`:菱形渐变 - `ImagePattern`:图片图案 -#### 3.3.3 Composition(合成) +#### 3.3.4 Composition(合成) 合成用于内容复用(类似 After Effects 的 Pre-comp)。 @@ -455,6 +473,7 @@ PAGX 文档采用层级结构组织内容: ← 根元素(定义画布尺寸) ├── ← 资源区(可选,定义可复用资源) │ ├── ← 图片资源 +│ ├── ← 路径数据资源 │ ├── ← 纯色定义 │ ├── ← 渐变定义 │ ├── ← 图片图案定义 @@ -943,15 +962,19 @@ y = center.y + outerRadius * sin(angle) #### 5.2.4 Path(路径) -使用 SVG 路径语法定义任意形状。 +使用 SVG 路径语法定义任意形状,支持内联数据或引用 Resources 中定义的 PathData。 ```xml + + + + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `data` | string | "" | SVG 路径数据(语法见 2.7 节) | +| `data` | string/idref | "" | SVG 路径数据(语法见 2.7 节)或 PathData 引用 "#id" | | `reversed` | bool | false | 反转路径方向 | #### 5.2.5 TextSpan(文本片段) @@ -1388,7 +1411,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `path` | idref | - | 路径引用 "#id" | +| `path` | idref | - | PathData 资源引用 "#id"(见 2.8 节) | | `align` | TextPathAlign | start | 对齐模式(见下方) | | `firstMargin` | float | 0 | 起始边距 | | `lastMargin` | float | 0 | 结束边距 | From 3f49de9e3f385672b3034dfeaa471af6866fef3e Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 10:29:29 +0800 Subject: [PATCH 037/678] Unify attribute table format in spec with consistent required field notation. --- pagx/docs/pagx_spec.md | 138 ++++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 64 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index ea8eabcbc8..d7fd4cca86 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -47,7 +47,17 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 | 默认单位 | 像素(无需标注) | `width="100"` | | 角度单位 | 度 | `rotation="45"` | -### 2.2 基本数值类型 +### 2.2 属性表格约定 + +本规范中的属性表格统一使用"默认值"列描述属性的必填性: + +| 默认值格式 | 含义 | +|------------|------| +| `(必填)` | 属性必须指定,没有默认值 | +| `(Resources 中必填)` | 仅在 Resources 中定义时必填,内联使用时可省略 | +| 具体值(如 `0`、`true`、`normal`) | 属性可选,未指定时使用该默认值 | + +### 2.3 基本数值类型 | 类型 | 说明 | 示例 | |------|------|------| @@ -58,7 +68,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 | `enum` | 枚举值 | `normal`、`multiply` | | `idref` | ID 引用 | `#gradientId`、`#maskLayer` | -### 2.3 点(Point) +### 2.4 点(Point) 点使用逗号分隔的两个浮点数表示: @@ -68,7 +78,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 **示例**:`"100,200"`、`"0.5,0.5"`、`"-50,100"` -### 2.4 矩形(Rect) +### 2.5 矩形(Rect) 矩形使用逗号分隔的四个浮点数表示: @@ -78,7 +88,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 **示例**:`"0,0,100,100"`、`"10,20,200,150"` -### 2.5 变换矩阵(Matrix) +### 2.6 变换矩阵(Matrix) #### 2D 变换矩阵 @@ -105,7 +115,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 "m00,m10,m20,m30,m01,m11,m21,m31,m02,m12,m22,m32,m03,m13,m23,m33" ``` -### 2.6 颜色(Color) +### 2.7 颜色(Color) PAGX 支持多种颜色格式: @@ -117,7 +127,7 @@ PAGX 支持多种颜色格式: | 色域 | `color(display-p3 1 0 0)` | 广色域颜色 | | 引用 | `#resourceId` | 引用 Resources 中定义的颜色源 | -### 2.7 路径数据(Path Data) +### 2.8 路径数据(Path Data) 路径数据使用 SVG 路径语法,支持以下命令: @@ -136,7 +146,7 @@ PAGX 支持多种颜色格式: **示例**:`"M 0 0 L 100 0 L 100 100 Z"` -### 2.8 PathData(路径数据资源) +### 2.9 PathData(路径数据资源) PathData 定义可在文档中引用的路径数据,支持路径复用。 @@ -144,12 +154,12 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 ``` -| 属性 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `id` | string | 是 | 唯一标识 | -| `data` | string | 是 | SVG 路径数据(语法见 2.7 节) | +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | (必填) | 唯一标识 | +| `data` | string | (必填) | SVG 路径数据(语法见 2.11 节) | -### 2.9 Image(图片) +### 2.10 Image(图片) 图片资源定义可在文档中引用的位图数据。 @@ -161,14 +171,14 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 ``` -| 属性 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `id` | string | 是 | 唯一标识 | -| `source` | string | 是 | 文件路径或数据 URI | +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | (必填) | 唯一标识 | +| `source` | string | (必填) | 文件路径或数据 URI | **支持格式**:PNG、JPEG、WebP、GIF -### 2.10 颜色源(Color Source) +### 2.11 颜色源(Color Source) 颜色源定义可用于渲染的颜色,支持两种定义方式: @@ -180,18 +190,18 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 - 被多处引用的颜色源应定义在 Resources 中以便复用 - ImagePattern 使用 objectBoundingBox 时(tile 尺寸依赖形状尺寸),通常需要内联定义,因为不同形状需要不同的 matrix -#### 2.10.1 SolidColor(纯色) +#### 2.11.1 SolidColor(纯色) ```xml ``` -| 属性 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `id` | string | 是(Resources 中) | 唯一标识 | -| `color` | color | 是 | 颜色值 | +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | (Resources 中必填) | 唯一标识 | +| `color` | color | (必填) | 颜色值 | -#### 2.10.2 LinearGradient(线性渐变) +#### 2.11.2 LinearGradient(线性渐变) 线性渐变沿起点到终点的方向插值。 @@ -204,14 +214,14 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | +| `id` | string | (Resources 中必填) | 唯一标识 | | `startPoint` | point | 0,0 | 起点 | -| `endPoint` | point | - | 终点 | +| `endPoint` | point | (必填) | 终点 | | `matrix` | string | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由 P 在起点-终点连线上的投影位置决定。 -#### 2.10.3 RadialGradient(径向渐变) +#### 2.11.3 RadialGradient(径向渐变) 径向渐变从中心向外辐射。 @@ -224,14 +234,14 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | +| `id` | string | (Resources 中必填) | 唯一标识 | | `center` | point | 0,0 | 中心点 | -| `radius` | float | - | 渐变半径 | +| `radius` | float | (必填) | 渐变半径 | | `matrix` | string | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由 `distance(P, center) / radius` 决定。 -#### 2.10.4 ConicGradient(锥形渐变) +#### 2.11.4 ConicGradient(锥形渐变) 锥形渐变(也称扫描渐变)沿圆周方向插值。 @@ -244,7 +254,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | +| `id` | string | (Resources 中必填) | 唯一标识 | | `center` | point | 0,0 | 中心点 | | `startAngle` | float | 0 | 起始角度 | | `endAngle` | float | 360 | 结束角度 | @@ -252,7 +262,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 **计算**:对于点 P,其颜色由 `atan2(P.y - center.y, P.x - center.x)` 在 `[startAngle, endAngle]` 范围内的比例决定。 -#### 2.10.5 DiamondGradient(菱形渐变) +#### 2.11.5 DiamondGradient(菱形渐变) 菱形渐变从中心向四角辐射。 @@ -265,23 +275,23 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | +| `id` | string | (Resources 中必填) | 唯一标识 | | `center` | point | 0,0 | 中心点 | -| `halfDiagonal` | float | - | 半对角线长度 | +| `halfDiagonal` | float | (必填) | 半对角线长度 | | `matrix` | string | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由曼哈顿距离 `(|P.x - center.x| + |P.y - center.y|) / halfDiagonal` 决定。 -#### 2.10.6 ColorStop(渐变色标) +#### 2.11.6 ColorStop(渐变色标) ```xml ``` -| 属性 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `offset` | float | 是 | 位置 0.0~1.0 | -| `color` | color | 是 | 色标颜色 | +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `offset` | float | (必填) | 位置 0.0~1.0 | +| `color` | color | (必填) | 色标颜色 | **渐变通用规则**: @@ -293,7 +303,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 - 如果没有 `offset = 1` 的色标,使用最后一个色标的颜色填充 - **渐变变换**:`matrix` 属性对渐变坐标系应用变换 -#### 2.10.7 颜色源坐标系统 +#### 2.11.7 颜色源坐标系统 所有颜色源(渐变、图案)的坐标系是**相对于几何元素的局部坐标系原点**。 @@ -322,7 +332,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 - 对该图层应用 `scale(2, 2)` 变换:矩形变为 200×200,渐变也随之放大,视觉效果保持一致 - 直接将 Rectangle 的 size 改为 200,200:矩形变为 200×200,但渐变坐标不变,只覆盖矩形的左半部分 -#### 2.10.8 ImagePattern(图片图案) +#### 2.11.8 ImagePattern(图片图案) 图片图案使用图片作为颜色源。 @@ -332,8 +342,8 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | -| `image` | idref | - | 图片引用 "#id" | +| `id` | string | (Resources 中必填) | 唯一标识 | +| `image` | idref | (必填) | 图片引用 "#id" | | `tileModeX` | TileMode | clamp | X 方向平铺模式(见下方) | | `tileModeY` | TileMode | clamp | Y 方向平铺模式(见下方) | | `sampling` | SamplingMode | linear | 采样模式(见下方) | @@ -399,11 +409,11 @@ PAGX 使用标准的 2D 笛卡尔坐标系: ``` -| 属性 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `version` | string | 是 | 格式版本 | -| `width` | float | 是 | 画布宽度 | -| `height` | float | 是 | 画布高度 | +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `version` | string | (必填) | 格式版本 | +| `width` | float | (必填) | 画布宽度 | +| `height` | float | (必填) | 画布高度 | **图层渲染顺序**:图层按文档顺序依次渲染,文档中靠前的图层先渲染(位于下方),靠后的图层后渲染(位于上方)。 @@ -427,15 +437,15 @@ PAGX 使用标准的 2D 笛卡尔坐标系: #### 3.3.1 Image(图片) -图片资源定义见 2.9 节。 +图片资源定义见 2.11 节。 #### 3.3.2 PathData(路径数据) -路径数据资源定义见 2.8 节。可被 Path 元素和 TextPath 修改器引用。 +路径数据资源定义见 2.11 节。可被 Path 元素和 TextPath 修改器引用。 #### 3.3.3 颜色源 -Resources 中可定义以下颜色源类型(详见 2.10 节): +Resources 中可定义以下颜色源类型(详见 2.11 节): - `SolidColor`:纯色 - `LinearGradient`:线性渐变 @@ -459,11 +469,11 @@ Resources 中可定义以下颜色源类型(详见 2.10 节): ``` -| 属性 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `id` | string | 是 | 唯一标识 | -| `width` | float | 是 | 合成宽度 | -| `height` | float | 是 | 合成高度 | +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | string | (必填) | 唯一标识 | +| `width` | float | (必填) | 合成宽度 | +| `height` | float | (必填) | 合成高度 | ### 3.4 文档层级结构 @@ -685,8 +695,8 @@ PAGX 文档采用层级结构组织内容: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `blurrinessX` | float | - | X 模糊半径 | -| `blurrinessY` | float | - | Y 模糊半径 | +| `blurrinessX` | float | (必填) | X 模糊半径 | +| `blurrinessY` | float | (必填) | Y 模糊半径 | | `tileMode` | TileMode | decal | 平铺模式(见 4.2.3) | #### 4.3.2 DropShadowFilter(投影阴影滤镜) @@ -717,7 +727,7 @@ PAGX 文档采用层级结构组织内容: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color | - | 混合颜色 | +| `color` | color | (必填) | 混合颜色 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1) | #### 4.3.5 ColorMatrixFilter(颜色矩阵滤镜) @@ -726,7 +736,7 @@ PAGX 文档采用层级结构组织内容: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `matrix` | string | - | 4x5 颜色矩阵(20 个逗号分隔的浮点数) | +| `matrix` | string | (必填) | 4x5 颜色矩阵(20 个逗号分隔的浮点数) | **矩阵格式**(20 个值,行优先): ``` @@ -974,7 +984,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `data` | string/idref | "" | SVG 路径数据(语法见 2.7 节)或 PathData 引用 "#id" | +| `data` | string/idref | "" | SVG 路径数据(语法见 2.11 节)或 PathData 引用 "#id" | | `reversed` | bool | false | 反转路径方向 | #### 5.2.5 TextSpan(文本片段) @@ -991,7 +1001,7 @@ y = center.y + outerRadius * sin(angle) |------|------|--------|------| | `x` | float | 0 | X 位置 | | `y` | float | 0 | Y 位置 | -| `font` | string | - | 字体族 | +| `font` | string | (必填) | 字体族 | | `fontSize` | float | 12 | 字号 | | `fontWeight` | int | 400 | 字重(100-900) | | `fontStyle` | enum | normal | normal 或 italic | @@ -1411,7 +1421,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `path` | idref | - | PathData 资源引用 "#id"(见 2.8 节) | +| `path` | idref | (必填) | PathData 资源引用 "#id"(见 2.11 节) | | `align` | TextPathAlign | start | 对齐模式(见下方) | | `firstMargin` | float | 0 | 起始边距 | | `lastMargin` | float | 0 | 结束边距 | @@ -1463,8 +1473,8 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `width` | float | - | 文本框宽度 | -| `height` | float | - | 文本框高度 | +| `width` | float | (必填) | 文本框宽度 | +| `height` | float | (必填) | 文本框高度 | | `align` | TextAlign | left | 水平对齐(见下方) | | `verticalAlign` | VerticalAlign | top | 垂直对齐(见下方) | | `lineHeight` | float | 1.2 | 行高倍数 | From 64962deb8b4983fb9ecc664cfd7a7a1f54f6fe6b Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 11:27:14 +0800 Subject: [PATCH 038/678] Rename Placement enum to LayerPlacement for clarity. --- pagx/docs/pagx_spec.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index d7fd4cca86..5a17b16c4a 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -421,6 +421,8 @@ PAGX 使用标准的 2D 笛卡尔坐标系: `` 定义可复用的资源,包括图片、路径数据、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `#id` 形式引用。 +**元素位置**:Resources 元素可放置在根元素内的任意位置。为便于阅读和理解,推荐在可能的情况下,将被引用的元素定义在引用它的元素之前,并将所有资源集中在靠近文件顶部的单个 Resources 元素内。解析器必须支持前向引用,即元素可以引用在文档后面定义的资源或图层。 + ```xml @@ -1061,7 +1063,7 @@ y = center.y + outerRadius * sin(angle) | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1) | | `fillRule` | FillRule | winding | 填充规则(见下方) | -| `placement` | Placement | background | 绘制位置(见 5.3.3) | +| `placement` | LayerPlacement | background | 绘制位置(见 5.3.3) | **FillRule(填充规则)**: @@ -1107,7 +1109,7 @@ y = center.y + outerRadius * sin(angle) | `dashes` | string | - | 虚线模式 "d1,d2,..." | | `dashOffset` | float | 0 | 虚线偏移 | | `align` | StrokeAlign | center | 描边对齐(见下方) | -| `placement` | Placement | background | 绘制位置(见 5.3.3) | +| `placement` | LayerPlacement | background | 绘制位置(见 5.3.3) | **LineCap(线帽样式)**: @@ -1141,7 +1143,7 @@ y = center.y + outerRadius * sin(angle) - `dashes`:定义虚线段长度序列,如 `"5,3"` 表示 5px 实线 + 3px 空白 - `dashOffset`:虚线起始偏移量 -#### 5.3.3 Placement(绘制位置) +#### 5.3.3 LayerPlacement(绘制位置) Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: From c7193d710f79a8f97b278628fd29d8c8da2e216f Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 11:27:42 +0800 Subject: [PATCH 039/678] Update tgfx to f96be663690a908ec7a59481dd65b5cedd99854d. --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index b20b19af92..dbca07a4fb 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "ced4fe464a8459e4efda8608927660e16bd009bb", + "commit": "f96be663690a908ec7a59481dd65b5cedd99854d", "dir": "third_party/tgfx" }, { From 215e007f041982f3d32b2eeea53fc6c2729e3ec4 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 11:30:19 +0800 Subject: [PATCH 040/678] Extend findLayer to search within Composition resources for forward reference support. --- pagx/src/PAGXDocument.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp index 18ccdf040c..a08e4fce6d 100644 --- a/pagx/src/PAGXDocument.cpp +++ b/pagx/src/PAGXDocument.cpp @@ -87,7 +87,22 @@ ResourceNode* PAGXDocument::findResource(const std::string& id) const { } LayerNode* PAGXDocument::findLayer(const std::string& id) const { - return findLayerRecursive(layers, id); + // First search in top-level layers + auto found = findLayerRecursive(layers, id); + if (found) { + return found; + } + // Then search in Composition resources + for (const auto& resource : resources) { + if (resource->type() == NodeType::Composition) { + auto comp = static_cast(resource.get()); + found = findLayerRecursive(comp->layers, id); + if (found) { + return found; + } + } + } + return nullptr; } void PAGXDocument::rebuildResourceMap() const { From f637bc3b1c4f1195b66630e5adc819ca60776b60 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 11:38:31 +0800 Subject: [PATCH 041/678] Extract PathData to Resources and deduplicate ColorSource references in XML writer. --- pagx/include/pagx/PAGXNode.h | 16 + pagx/src/PAGXXMLParser.cpp | 10 + pagx/src/PAGXXMLParser.h | 1 + pagx/src/PAGXXMLWriter.cpp | 610 +++++++++++++++++++++++++++++++++-- 4 files changed, 609 insertions(+), 28 deletions(-) diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index ad06294a11..3368d95a57 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -88,6 +88,7 @@ enum class NodeType { // Resources Image, + PathData, Composition, // Layer @@ -848,6 +849,21 @@ struct ImageNode : public ResourceNode { } }; +/** + * PathData resource - stores reusable path data. + */ +struct PathDataNode : public ResourceNode { + std::string data = {}; // SVG path data string + + NodeType type() const override { + return NodeType::PathData; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + /** * Composition resource. */ diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 028eb235a4..5897953506 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -304,6 +304,9 @@ std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) if (node->tag == "Image") { return parseImage(node); } + if (node->tag == "PathData") { + return parsePathData(node); + } if (node->tag == "SolidColor") { auto solidColor = parseSolidColor(node); auto resource = std::make_unique(); @@ -896,6 +899,13 @@ std::unique_ptr PAGXXMLParser::parseImage(const XMLNode* node) { return image; } +std::unique_ptr PAGXXMLParser::parsePathData(const XMLNode* node) { + auto pathData = std::make_unique(); + pathData->id = getAttribute(node, "id"); + pathData->data = getAttribute(node, "data"); + return pathData; +} + std::unique_ptr PAGXXMLParser::parseComposition(const XMLNode* node) { auto comp = std::make_unique(); comp->id = getAttribute(node, "id"); diff --git a/pagx/src/PAGXXMLParser.h b/pagx/src/PAGXXMLParser.h index 3d402005fa..cec690247c 100644 --- a/pagx/src/PAGXXMLParser.h +++ b/pagx/src/PAGXXMLParser.h @@ -88,6 +88,7 @@ class PAGXXMLParser { static ColorStopNode parseColorStop(const XMLNode* node); static std::unique_ptr parseImage(const XMLNode* node); + static std::unique_ptr parsePathData(const XMLNode* node); static std::unique_ptr parseComposition(const XMLNode* node); static std::unique_ptr parseDropShadowStyle(const XMLNode* node); diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index a2738881e6..ac8209a351 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -18,9 +18,15 @@ #include "PAGXXMLWriter.h" #include +#include +#include namespace pagx { +//============================================================================== +// XMLBuilder - XML generation helper +//============================================================================== + class XMLBuilder { public: void appendDeclaration() { @@ -144,12 +150,9 @@ class XMLBuilder { } }; -static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool writeId = true); -static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node); -static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node); -static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node); -static void writeResource(XMLBuilder& xml, const ResourceNode* node); -static void writeLayer(XMLBuilder& xml, const LayerNode* node); +//============================================================================== +// Helper functions for converting types to strings +//============================================================================== static std::string pointToString(const Point& p) { std::ostringstream oss = {}; @@ -180,6 +183,229 @@ static std::string floatListToString(const std::vector& values) { return oss.str(); } +//============================================================================== +// ColorSource serialization helper +//============================================================================== + +static std::string colorSourceToKey(const ColorSourceNode* node) { + if (!node) { + return ""; + } + std::ostringstream oss = {}; + switch (node->type()) { + case NodeType::SolidColor: { + auto solid = static_cast(node); + oss << "SolidColor:" << solid->color.toHexString(true); + break; + } + case NodeType::LinearGradient: { + auto grad = static_cast(node); + oss << "LinearGradient:" << grad->startPoint.x << "," << grad->startPoint.y << ":" + << grad->endPoint.x << "," << grad->endPoint.y << ":" << grad->matrix.toString() << ":"; + for (const auto& stop : grad->colorStops) { + oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; + } + break; + } + case NodeType::RadialGradient: { + auto grad = static_cast(node); + oss << "RadialGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->radius + << ":" << grad->matrix.toString() << ":"; + for (const auto& stop : grad->colorStops) { + oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; + } + break; + } + case NodeType::ConicGradient: { + auto grad = static_cast(node); + oss << "ConicGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->startAngle + << ":" << grad->endAngle << ":" << grad->matrix.toString() << ":"; + for (const auto& stop : grad->colorStops) { + oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; + } + break; + } + case NodeType::DiamondGradient: { + auto grad = static_cast(node); + oss << "DiamondGradient:" << grad->center.x << "," << grad->center.y << ":" + << grad->halfDiagonal << ":" << grad->matrix.toString() << ":"; + for (const auto& stop : grad->colorStops) { + oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; + } + break; + } + case NodeType::ImagePattern: { + auto pattern = static_cast(node); + oss << "ImagePattern:" << pattern->image << ":" << static_cast(pattern->tileModeX) << ":" + << static_cast(pattern->tileModeY) << ":" << static_cast(pattern->sampling) + << ":" << pattern->matrix.toString(); + break; + } + default: + break; + } + return oss.str(); +} + +//============================================================================== +// ResourceContext - tracks extracted resources and reference counts +//============================================================================== + +class ResourceContext { + public: + // PathData: SVG string -> resource id + std::unordered_map pathDataMap = {}; + + // ColorSource: serialized key -> (resource id, reference count) + std::unordered_map> colorSourceMap = {}; + + // All extracted PathData resources (ordered) + std::vector> pathDataResources = {}; // id -> svg string + + // All extracted ColorSource resources (ordered) + std::vector> colorSourceResources = {}; + + int nextPathId = 1; + int nextColorId = 1; + + // First pass: collect and count all resources + void collectFromDocument(const PAGXDocument& doc) { + for (const auto& layer : doc.layers) { + collectFromLayer(layer.get()); + } + for (const auto& resource : doc.resources) { + if (resource->type() == NodeType::Composition) { + auto comp = static_cast(resource.get()); + for (const auto& layer : comp->layers) { + collectFromLayer(layer.get()); + } + } + } + } + + // Get or create PathData resource id + std::string getPathDataId(const std::string& svgString) { + auto it = pathDataMap.find(svgString); + if (it != pathDataMap.end()) { + return it->second; + } + std::string id = "path" + std::to_string(nextPathId++); + pathDataMap[svgString] = id; + pathDataResources.emplace_back(id, svgString); + return id; + } + + // Register ColorSource usage (for counting) + void registerColorSource(const ColorSourceNode* node) { + if (!node) { + return; + } + std::string key = colorSourceToKey(node); + if (key.empty()) { + return; + } + auto it = colorSourceMap.find(key); + if (it != colorSourceMap.end()) { + it->second.second++; // Increment reference count + } else { + colorSourceMap[key] = {"", 1}; // Will assign id later if needed + } + } + + // Finalize: assign ids to ColorSources with multiple references + void finalizeColorSources() { + for (auto& [key, value] : colorSourceMap) { + if (value.second > 1) { + value.first = "color" + std::to_string(nextColorId++); + } + } + } + + // Check if ColorSource should be extracted to Resources + bool shouldExtractColorSource(const ColorSourceNode* node) const { + if (!node) { + return false; + } + std::string key = colorSourceToKey(node); + auto it = colorSourceMap.find(key); + return it != colorSourceMap.end() && it->second.second > 1; + } + + // Get ColorSource resource id (empty if should inline) + std::string getColorSourceId(const ColorSourceNode* node) const { + if (!node) { + return ""; + } + std::string key = colorSourceToKey(node); + auto it = colorSourceMap.find(key); + if (it != colorSourceMap.end() && it->second.second > 1) { + return it->second.first; + } + return ""; + } + + private: + void collectFromLayer(const LayerNode* layer) { + for (const auto& element : layer->contents) { + collectFromVectorElement(element.get()); + } + for (const auto& child : layer->children) { + collectFromLayer(child.get()); + } + } + + void collectFromVectorElement(const VectorElementNode* element) { + switch (element->type()) { + case NodeType::Path: { + auto path = static_cast(element); + if (!path->data.isEmpty()) { + getPathDataId(path->data.toSVGString()); + } + break; + } + case NodeType::Fill: { + auto fill = static_cast(element); + if (fill->colorSource) { + registerColorSource(fill->colorSource.get()); + } + break; + } + case NodeType::Stroke: { + auto stroke = static_cast(element); + if (stroke->colorSource) { + registerColorSource(stroke->colorSource.get()); + } + break; + } + case NodeType::Group: { + auto group = static_cast(element); + for (const auto& child : group->elements) { + collectFromVectorElement(child.get()); + } + break; + } + default: + break; + } + } +}; + +//============================================================================== +// Forward declarations +//============================================================================== + +static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool writeId); +static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, + const ResourceContext& ctx); +static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node); +static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node); +static void writeResource(XMLBuilder& xml, const ResourceNode* node, const ResourceContext& ctx); +static void writeLayer(XMLBuilder& xml, const LayerNode* node, const ResourceContext& ctx); + +//============================================================================== +// ColorStop and ColorSource writing +//============================================================================== + static void writeColorStops(XMLBuilder& xml, const std::vector& stops) { for (const auto& stop : stops) { xml.openElement("ColorStop"); @@ -194,7 +420,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool case NodeType::SolidColor: { auto solid = static_cast(node); xml.openElement("SolidColor"); - if (writeId) { + if (writeId && !solid->id.empty()) { xml.addAttribute("id", solid->id); } xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); @@ -204,7 +430,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool case NodeType::LinearGradient: { auto grad = static_cast(node); xml.openElement("LinearGradient"); - if (writeId) { + if (writeId && !grad->id.empty()) { xml.addAttribute("id", grad->id); } if (grad->startPoint.x != 0 || grad->startPoint.y != 0) { @@ -226,7 +452,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool case NodeType::RadialGradient: { auto grad = static_cast(node); xml.openElement("RadialGradient"); - if (writeId) { + if (writeId && !grad->id.empty()) { xml.addAttribute("id", grad->id); } if (grad->center.x != 0 || grad->center.y != 0) { @@ -248,7 +474,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool case NodeType::ConicGradient: { auto grad = static_cast(node); xml.openElement("ConicGradient"); - if (writeId) { + if (writeId && !grad->id.empty()) { xml.addAttribute("id", grad->id); } if (grad->center.x != 0 || grad->center.y != 0) { @@ -271,7 +497,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool case NodeType::DiamondGradient: { auto grad = static_cast(node); xml.openElement("DiamondGradient"); - if (writeId) { + if (writeId && !grad->id.empty()) { xml.addAttribute("id", grad->id); } if (grad->center.x != 0 || grad->center.y != 0) { @@ -293,7 +519,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool case NodeType::ImagePattern: { auto pattern = static_cast(node); xml.openElement("ImagePattern"); - if (writeId) { + if (writeId && !pattern->id.empty()) { xml.addAttribute("id", pattern->id); } xml.addAttribute("image", pattern->image); @@ -317,7 +543,130 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool } } -static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { +// Write ColorSource with assigned id (for Resources section) +static void writeColorSourceWithId(XMLBuilder& xml, const ColorSourceNode* node, + const std::string& id) { + switch (node->type()) { + case NodeType::SolidColor: { + auto solid = static_cast(node); + xml.openElement("SolidColor"); + xml.addAttribute("id", id); + xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); + xml.closeElementSelfClosing(); + break; + } + case NodeType::LinearGradient: { + auto grad = static_cast(node); + xml.openElement("LinearGradient"); + xml.addAttribute("id", id); + if (grad->startPoint.x != 0 || grad->startPoint.y != 0) { + xml.addAttribute("startPoint", pointToString(grad->startPoint)); + } + xml.addRequiredAttribute("endPoint", pointToString(grad->endPoint)); + if (!grad->matrix.isIdentity()) { + xml.addAttribute("matrix", grad->matrix.toString()); + } + if (grad->colorStops.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + writeColorStops(xml, grad->colorStops); + xml.closeElement(); + } + break; + } + case NodeType::RadialGradient: { + auto grad = static_cast(node); + xml.openElement("RadialGradient"); + xml.addAttribute("id", id); + if (grad->center.x != 0 || grad->center.y != 0) { + xml.addAttribute("center", pointToString(grad->center)); + } + xml.addRequiredAttribute("radius", grad->radius); + if (!grad->matrix.isIdentity()) { + xml.addAttribute("matrix", grad->matrix.toString()); + } + if (grad->colorStops.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + writeColorStops(xml, grad->colorStops); + xml.closeElement(); + } + break; + } + case NodeType::ConicGradient: { + auto grad = static_cast(node); + xml.openElement("ConicGradient"); + xml.addAttribute("id", id); + if (grad->center.x != 0 || grad->center.y != 0) { + xml.addAttribute("center", pointToString(grad->center)); + } + xml.addAttribute("startAngle", grad->startAngle); + xml.addAttribute("endAngle", grad->endAngle, 360.0f); + if (!grad->matrix.isIdentity()) { + xml.addAttribute("matrix", grad->matrix.toString()); + } + if (grad->colorStops.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + writeColorStops(xml, grad->colorStops); + xml.closeElement(); + } + break; + } + case NodeType::DiamondGradient: { + auto grad = static_cast(node); + xml.openElement("DiamondGradient"); + xml.addAttribute("id", id); + if (grad->center.x != 0 || grad->center.y != 0) { + xml.addAttribute("center", pointToString(grad->center)); + } + xml.addRequiredAttribute("halfDiagonal", grad->halfDiagonal); + if (!grad->matrix.isIdentity()) { + xml.addAttribute("matrix", grad->matrix.toString()); + } + if (grad->colorStops.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + writeColorStops(xml, grad->colorStops); + xml.closeElement(); + } + break; + } + case NodeType::ImagePattern: { + auto pattern = static_cast(node); + xml.openElement("ImagePattern"); + xml.addAttribute("id", id); + xml.addAttribute("image", pattern->image); + if (pattern->tileModeX != TileMode::Clamp) { + xml.addAttribute("tileModeX", TileModeToString(pattern->tileModeX)); + } + if (pattern->tileModeY != TileMode::Clamp) { + xml.addAttribute("tileModeY", TileModeToString(pattern->tileModeY)); + } + if (pattern->sampling != SamplingMode::Linear) { + xml.addAttribute("sampling", SamplingModeToString(pattern->sampling)); + } + if (!pattern->matrix.isIdentity()) { + xml.addAttribute("matrix", pattern->matrix.toString()); + } + xml.closeElementSelfClosing(); + break; + } + default: + break; + } +} + +//============================================================================== +// VectorElement writing +//============================================================================== + +static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, + const ResourceContext& ctx) { switch (node->type()) { case NodeType::Rectangle: { auto rect = static_cast(node); @@ -367,7 +716,15 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { auto path = static_cast(node); xml.openElement("Path"); if (!path->data.isEmpty()) { - xml.addAttribute("data", path->data.toSVGString()); + // Always reference PathData from Resources + std::string svgStr = path->data.toSVGString(); + auto it = ctx.pathDataMap.find(svgStr); + if (it != ctx.pathDataMap.end()) { + xml.addAttribute("data", "#" + it->second); + } else { + // Fallback to inline if not found (shouldn't happen) + xml.addAttribute("data", svgStr); + } } xml.addAttribute("reversed", path->reversed); xml.closeElementSelfClosing(); @@ -397,7 +754,17 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::Fill: { auto fill = static_cast(node); xml.openElement("Fill"); - xml.addAttribute("color", fill->color); + // Check if ColorSource should be referenced or inlined + if (fill->colorSource) { + std::string colorId = ctx.getColorSourceId(fill->colorSource.get()); + if (!colorId.empty()) { + // Reference the ColorSource from Resources + xml.addAttribute("color", "#" + colorId); + } + // If colorId is empty, we'll inline it below + } else { + xml.addAttribute("color", fill->color); + } xml.addAttribute("alpha", fill->alpha, 1.0f); if (fill->blendMode != BlendMode::Normal) { xml.addAttribute("blendMode", BlendModeToString(fill->blendMode)); @@ -408,7 +775,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { if (fill->placement != Placement::Background) { xml.addAttribute("placement", PlacementToString(fill->placement)); } - if (fill->colorSource) { + // Inline ColorSource only if not extracted to Resources + if (fill->colorSource && ctx.getColorSourceId(fill->colorSource.get()).empty()) { xml.closeElementStart(); writeColorSource(xml, fill->colorSource.get(), false); xml.closeElement(); @@ -420,7 +788,17 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { case NodeType::Stroke: { auto stroke = static_cast(node); xml.openElement("Stroke"); - xml.addAttribute("color", stroke->color); + // Check if ColorSource should be referenced or inlined + if (stroke->colorSource) { + std::string colorId = ctx.getColorSourceId(stroke->colorSource.get()); + if (!colorId.empty()) { + // Reference the ColorSource from Resources + xml.addAttribute("color", "#" + colorId); + } + // If colorId is empty, we'll inline it below + } else { + xml.addAttribute("color", stroke->color); + } xml.addAttribute("width", stroke->strokeWidth, 1.0f); xml.addAttribute("alpha", stroke->alpha, 1.0f); if (stroke->blendMode != BlendMode::Normal) { @@ -443,7 +821,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { if (stroke->placement != Placement::Background) { xml.addAttribute("placement", PlacementToString(stroke->placement)); } - if (stroke->colorSource) { + // Inline ColorSource only if not extracted to Resources + if (stroke->colorSource && ctx.getColorSourceId(stroke->colorSource.get()).empty()) { xml.closeElementStart(); writeColorSource(xml, stroke->colorSource.get(), false); xml.closeElement(); @@ -609,7 +988,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { } else { xml.closeElementStart(); for (const auto& element : group->elements) { - writeVectorElement(xml, element.get()); + writeVectorElement(xml, element.get(), ctx); } xml.closeElement(); } @@ -620,6 +999,10 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node) { } } +//============================================================================== +// LayerStyle writing +//============================================================================== + static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node) { switch (node->type()) { case NodeType::DropShadowStyle: { @@ -670,6 +1053,10 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node) { } } +//============================================================================== +// LayerFilter writing +//============================================================================== + static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node) { switch (node->type()) { case NodeType::BlurFilter: { @@ -730,7 +1117,11 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node) { } } -static void writeResource(XMLBuilder& xml, const ResourceNode* node) { +//============================================================================== +// Resource writing +//============================================================================== + +static void writeResource(XMLBuilder& xml, const ResourceNode* node, const ResourceContext& ctx) { switch (node->type()) { case NodeType::Image: { auto image = static_cast(node); @@ -740,6 +1131,14 @@ static void writeResource(XMLBuilder& xml, const ResourceNode* node) { xml.closeElementSelfClosing(); break; } + case NodeType::PathData: { + auto pathData = static_cast(node); + xml.openElement("PathData"); + xml.addAttribute("id", pathData->id); + xml.addAttribute("data", pathData->data); + xml.closeElementSelfClosing(); + break; + } case NodeType::Composition: { auto comp = static_cast(node); xml.openElement("Composition"); @@ -751,7 +1150,7 @@ static void writeResource(XMLBuilder& xml, const ResourceNode* node) { } else { xml.closeElementStart(); for (const auto& layer : comp->layers) { - writeLayer(xml, layer.get()); + writeLayer(xml, layer.get(), ctx); } xml.closeElement(); } @@ -763,14 +1162,18 @@ static void writeResource(XMLBuilder& xml, const ResourceNode* node) { case NodeType::ConicGradient: case NodeType::DiamondGradient: case NodeType::ImagePattern: - writeColorSource(xml, static_cast(node)); + writeColorSource(xml, static_cast(node), true); break; default: break; } } -static void writeLayer(XMLBuilder& xml, const LayerNode* node) { +//============================================================================== +// Layer writing +//============================================================================== + +static void writeLayer(XMLBuilder& xml, const LayerNode* node, const ResourceContext& ctx) { xml.openElement("Layer"); if (!node->id.empty()) { xml.addAttribute("id", node->id); @@ -816,7 +1219,7 @@ static void writeLayer(XMLBuilder& xml, const LayerNode* node) { xml.openElement("contents"); xml.closeElementStart(); for (const auto& element : node->contents) { - writeVectorElement(xml, element.get()); + writeVectorElement(xml, element.get(), ctx); } xml.closeElement(); } @@ -840,13 +1243,141 @@ static void writeLayer(XMLBuilder& xml, const LayerNode* node) { } for (const auto& child : node->children) { - writeLayer(xml, child.get()); + writeLayer(xml, child.get(), ctx); } xml.closeElement(); } +//============================================================================== +// Main Write function +//============================================================================== + std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { + // First pass: collect resources and count references + ResourceContext ctx = {}; + ctx.collectFromDocument(doc); + ctx.finalizeColorSources(); + + // Build ColorSource resources list (only those with multiple references) + // We need to store pointers to actual ColorSource nodes for writing + std::unordered_map colorSourceByKey = {}; + for (const auto& layer : doc.layers) { + std::function collectColorSources = [&](const LayerNode* layer) { + for (const auto& element : layer->contents) { + if (element->type() == NodeType::Fill) { + auto fill = static_cast(element.get()); + if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { + std::string key = colorSourceToKey(fill->colorSource.get()); + if (colorSourceByKey.find(key) == colorSourceByKey.end()) { + colorSourceByKey[key] = fill->colorSource.get(); + } + } + } else if (element->type() == NodeType::Stroke) { + auto stroke = static_cast(element.get()); + if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { + std::string key = colorSourceToKey(stroke->colorSource.get()); + if (colorSourceByKey.find(key) == colorSourceByKey.end()) { + colorSourceByKey[key] = stroke->colorSource.get(); + } + } + } else if (element->type() == NodeType::Group) { + auto group = static_cast(element.get()); + std::function collectFromGroup = [&](const GroupNode* g) { + for (const auto& child : g->elements) { + if (child->type() == NodeType::Fill) { + auto fill = static_cast(child.get()); + if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { + std::string key = colorSourceToKey(fill->colorSource.get()); + if (colorSourceByKey.find(key) == colorSourceByKey.end()) { + colorSourceByKey[key] = fill->colorSource.get(); + } + } + } else if (child->type() == NodeType::Stroke) { + auto stroke = static_cast(child.get()); + if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { + std::string key = colorSourceToKey(stroke->colorSource.get()); + if (colorSourceByKey.find(key) == colorSourceByKey.end()) { + colorSourceByKey[key] = stroke->colorSource.get(); + } + } + } else if (child->type() == NodeType::Group) { + collectFromGroup(static_cast(child.get())); + } + } + }; + collectFromGroup(group); + } + } + for (const auto& child : layer->children) { + collectColorSources(child.get()); + } + }; + collectColorSources(layer.get()); + } + + // Also collect from Compositions + for (const auto& resource : doc.resources) { + if (resource->type() == NodeType::Composition) { + auto comp = static_cast(resource.get()); + std::function collectColorSources = [&](const LayerNode* layer) { + for (const auto& element : layer->contents) { + if (element->type() == NodeType::Fill) { + auto fill = static_cast(element.get()); + if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { + std::string key = colorSourceToKey(fill->colorSource.get()); + if (colorSourceByKey.find(key) == colorSourceByKey.end()) { + colorSourceByKey[key] = fill->colorSource.get(); + } + } + } else if (element->type() == NodeType::Stroke) { + auto stroke = static_cast(element.get()); + if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { + std::string key = colorSourceToKey(stroke->colorSource.get()); + if (colorSourceByKey.find(key) == colorSourceByKey.end()) { + colorSourceByKey[key] = stroke->colorSource.get(); + } + } + } else if (element->type() == NodeType::Group) { + auto group = static_cast(element.get()); + std::function collectFromGroup = [&](const GroupNode* g) { + for (const auto& child : g->elements) { + if (child->type() == NodeType::Fill) { + auto fill = static_cast(child.get()); + if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { + std::string key = colorSourceToKey(fill->colorSource.get()); + if (colorSourceByKey.find(key) == colorSourceByKey.end()) { + colorSourceByKey[key] = fill->colorSource.get(); + } + } + } else if (child->type() == NodeType::Stroke) { + auto stroke = static_cast(child.get()); + if (stroke->colorSource && + ctx.shouldExtractColorSource(stroke->colorSource.get())) { + std::string key = colorSourceToKey(stroke->colorSource.get()); + if (colorSourceByKey.find(key) == colorSourceByKey.end()) { + colorSourceByKey[key] = stroke->colorSource.get(); + } + } + } else if (child->type() == NodeType::Group) { + collectFromGroup(static_cast(child.get())); + } + } + }; + collectFromGroup(group); + } + } + for (const auto& child : layer->children) { + collectColorSources(child.get()); + } + }; + for (const auto& layer : comp->layers) { + collectColorSources(layer.get()); + } + } + } + + // Second pass: generate XML XMLBuilder xml = {}; xml.appendDeclaration(); @@ -856,17 +1387,40 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { xml.addAttribute("height", doc.height); xml.closeElementStart(); - if (!doc.resources.empty()) { + // Write Resources section + bool hasResources = !ctx.pathDataResources.empty() || !colorSourceByKey.empty() || + !doc.resources.empty(); + if (hasResources) { xml.openElement("Resources"); xml.closeElementStart(); + + // Write PathData resources + for (const auto& [id, svgString] : ctx.pathDataResources) { + xml.openElement("PathData"); + xml.addAttribute("id", id); + xml.addAttribute("data", svgString); + xml.closeElementSelfClosing(); + } + + // Write ColorSource resources (those with multiple references) + for (const auto& [key, node] : colorSourceByKey) { + std::string id = ctx.getColorSourceId(node); + if (!id.empty()) { + writeColorSourceWithId(xml, node, id); + } + } + + // Write original resources (Image, Composition, etc.) for (const auto& resource : doc.resources) { - writeResource(xml, resource.get()); + writeResource(xml, resource.get(), ctx); } + xml.closeElement(); } + // Write Layers for (const auto& layer : doc.layers) { - writeLayer(xml, layer.get()); + writeLayer(xml, layer.get(), ctx); } xml.closeElement(); From f6134b6a7124ed233bfeca3430f121de461a88bf Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 11:53:10 +0800 Subject: [PATCH 042/678] Refactor PAGX spec with appendixes for quick reference and fix cross-section references. --- pagx/docs/pagx_spec.md | 661 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 617 insertions(+), 44 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 5a17b16c4a..20b4c9269a 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -31,6 +31,13 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 3. **图层系统**:定义图层及其相关特性(样式、滤镜、遮罩) 4. **矢量元素系统**:定义图层内容的矢量元素及其处理模型 +**附录**(方便速查): + +- **附录 A**:枚举类型汇总 +- **附录 B**:节点定义速查 +- **附录 C**:节点分类与包含关系 +- **附录 D**:完整示例 + --- ## 2. Basic Data Types(基础数据类型) @@ -157,7 +164,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `id` | string | (必填) | 唯一标识 | -| `data` | string | (必填) | SVG 路径数据(语法见 2.11 节) | +| `data` | string | (必填) | SVG 路径数据(语法见 2.8 节) | ### 2.10 Image(图片) @@ -421,7 +428,7 @@ PAGX 使用标准的 2D 笛卡尔坐标系: `` 定义可复用的资源,包括图片、路径数据、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `#id` 形式引用。 -**元素位置**:Resources 元素可放置在根元素内的任意位置。为便于阅读和理解,推荐在可能的情况下,将被引用的元素定义在引用它的元素之前,并将所有资源集中在靠近文件顶部的单个 Resources 元素内。解析器必须支持前向引用,即元素可以引用在文档后面定义的资源或图层。 +**元素位置**:Resources 元素可放置在根元素内的任意位置,对位置没有限制。解析器必须支持元素引用在文档后面定义的资源或图层(即前向引用)。 ```xml @@ -437,26 +444,14 @@ PAGX 使用标准的 2D 笛卡尔坐标系: ``` -#### 3.3.1 Image(图片) - -图片资源定义见 2.11 节。 - -#### 3.3.2 PathData(路径数据) - -路径数据资源定义见 2.11 节。可被 Path 元素和 TextPath 修改器引用。 - -#### 3.3.3 颜色源 - -Resources 中可定义以下颜色源类型(详见 2.11 节): +**支持的资源类型**: -- `SolidColor`:纯色 -- `LinearGradient`:线性渐变 -- `RadialGradient`:径向渐变 -- `ConicGradient`:锥形渐变 -- `DiamondGradient`:菱形渐变 -- `ImagePattern`:图片图案 +- `Image`:图片资源(见 2.10 节) +- `PathData`:路径数据(见 2.9 节),可被 Path 元素和 TextPath 修改器引用 +- 颜色源(见 2.11 节):`SolidColor`、`LinearGradient`、`RadialGradient`、`ConicGradient`、`DiamondGradient`、`ImagePattern` +- `Composition`:合成(见下方) -#### 3.3.4 Composition(合成) +#### 3.3.1 Composition(合成) 合成用于内容复用(类似 After Effects 的 Pre-comp)。 @@ -560,9 +555,17 @@ PAGX 文档采用层级结构组织内容: | `excludeChildEffectsInLayerStyle` | bool | false | 图层样式是否排除子图层效果 | | `scrollRect` | string | - | 滚动裁剪区域 "x,y,w,h" | | `mask` | idref | - | 遮罩图层引用 "#id" | -| `maskType` | MaskType | alpha | 遮罩类型(见 4.4.2) | +| `maskType` | MaskType | alpha | 遮罩类型(见下方) | | `composition` | idref | - | 合成引用 "#id" | +**MaskType(遮罩类型)**: + +| 值 | 说明 | +|------|------| +| `alpha` | Alpha 遮罩:使用遮罩的 alpha 通道 | +| `luminance` | 亮度遮罩:使用遮罩的亮度值 | +| `contour` | 轮廓遮罩:使用遮罩的轮廓进行裁剪 | + **BlendMode(混合模式)**: 混合模式定义源颜色(S)如何与目标颜色(D)组合。 @@ -665,16 +668,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | 0 | X 模糊半径 | | `blurrinessY` | float | 0 | Y 模糊半径 | -| `tileMode` | TileMode | mirror | 平铺模式(见下方) | - -**TileMode(平铺模式)**: - -| 值 | 说明 | -|------|------| -| `clamp` | 钳制:超出边界使用边缘像素颜色 | -| `repeat` | 重复:平铺图片 | -| `mirror` | 镜像:交替翻转平铺 | -| `decal` | 贴花:超出边界为透明 | +| `tileMode` | TileMode | mirror | 平铺模式(见 2.11.8) | **渲染步骤**: 1. 获取图层边界下方的背景内容 @@ -699,7 +693,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | (必填) | X 模糊半径 | | `blurrinessY` | float | (必填) | Y 模糊半径 | -| `tileMode` | TileMode | decal | 平铺模式(见 4.2.3) | +| `tileMode` | TileMode | decal | 平铺模式(见 2.11.8) | #### 4.3.2 DropShadowFilter(投影阴影滤镜) @@ -775,15 +769,7 @@ PAGX 文档采用层级结构组织内容: ... ``` -**遮罩类型**: - -**MaskType(遮罩类型)**: - -| 值 | 说明 | -|------|------| -| `alpha` | Alpha 遮罩:使用遮罩的 alpha 通道 | -| `luminance` | 亮度遮罩:使用遮罩的亮度值 | -| `contour` | 轮廓遮罩:使用遮罩的轮廓进行裁剪 | +**遮罩类型**:MaskType 定义见 4.1 节。 **遮罩规则**: - 遮罩图层自身不渲染(`visible` 属性被忽略) @@ -986,7 +972,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `data` | string/idref | "" | SVG 路径数据(语法见 2.11 节)或 PathData 引用 "#id" | +| `data` | string/idref | "" | SVG 路径数据(语法见 2.8 节)或 PathData 资源引用 "#id"(见 2.9 节) | | `reversed` | bool | false | 反转路径方向 | #### 5.2.5 TextSpan(文本片段) @@ -1423,7 +1409,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `path` | idref | (必填) | PathData 资源引用 "#id"(见 2.11 节) | +| `path` | idref | (必填) | PathData 资源引用 "#id"(见 2.9 节) | | `align` | TextPathAlign | start | 对齐模式(见下方) | | `firstMargin` | float | 0 | 起始边距 | | `lastMargin` | float | 0 | 结束边距 | @@ -1745,7 +1731,494 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: --- -## Appendix A. Complete Example(完整示例) +## Appendix A. Enumerations(枚举类型汇总) + +本附录汇总规范中所有枚举类型,方便速查。 + +### A.1 图层相关 + +| 枚举 | 值 | 定义位置 | +|------|------|----------| +| **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter` | 4.1 | +| **MaskType** | `alpha`, `luminance`, `contour` | 4.1 | +| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | 2.11.8 | +| **SamplingMode** | `nearest`, `linear`, `mipmap` | 2.11.8 | + +### A.2 绘制器相关 + +| 枚举 | 值 | 定义位置 | +|------|------|----------| +| **FillRule** | `winding`, `evenOdd` | 5.3.1 | +| **LineCap** | `butt`, `round`, `square` | 5.3.2 | +| **LineJoin** | `miter`, `round`, `bevel` | 5.3.2 | +| **StrokeAlign** | `center`, `inside`, `outside` | 5.3.2 | +| **LayerPlacement** | `background`, `foreground` | 5.3.3 | + +### A.3 几何元素相关 + +| 枚举 | 值 | 定义位置 | +|------|------|----------| +| **PolystarType** | `polygon`, `star` | 5.2.3 | +| **TextAnchor** | `start`, `middle`, `end` | 5.2.5 | + +### A.4 修改器相关 + +| 枚举 | 值 | 定义位置 | +|------|------|----------| +| **TrimType** | `separate`, `continuous` | 5.4.1 | +| **MergePathOp** | `append`, `union`, `intersect`, `xor`, `difference` | 5.4.3 | +| **SelectorUnit** | `index`, `percentage` | 5.5.4 | +| **SelectorShape** | `square`, `rampUp`, `rampDown`, `triangle`, `round`, `smooth` | 5.5.4 | +| **SelectorMode** | `add`, `subtract`, `intersect`, `min`, `max`, `difference` | 5.5.4 | +| **TextPathAlign** | `start`, `center`, `end` | 5.5.5 | +| **TextAlign** | `left`, `center`, `right`, `justify` | 5.5.6 | +| **VerticalAlign** | `top`, `center`, `bottom` | 5.5.6 | +| **Overflow** | `clip`, `visible`, `ellipsis` | 5.5.6 | +| **RepeaterOrder** | `belowOriginal`, `aboveOriginal` | 5.6 | + +--- + +## Appendix B. Node Reference(节点定义速查) + +本附录列出所有节点的属性定义,省略详细说明。 + +### B.1 文档结构节点 + +#### pagx +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `version` | string | (必填) | +| `width` | float | (必填) | +| `height` | float | (必填) | + +#### Composition +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | (必填) | +| `width` | float | (必填) | +| `height` | float | (必填) | + +### B.2 资源节点 + +#### Image +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | (必填) | +| `source` | string | (必填) | + +#### PathData +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | (必填) | +| `data` | string | (必填) | + +#### SolidColor +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | (Resources 中必填) | +| `color` | color | (必填) | + +#### LinearGradient +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | (Resources 中必填) | +| `startPoint` | point | 0,0 | +| `endPoint` | point | (必填) | +| `matrix` | string | 单位矩阵 | + +#### RadialGradient +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | (Resources 中必填) | +| `center` | point | 0,0 | +| `radius` | float | (必填) | +| `matrix` | string | 单位矩阵 | + +#### ConicGradient +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | (Resources 中必填) | +| `center` | point | 0,0 | +| `startAngle` | float | 0 | +| `endAngle` | float | 360 | +| `matrix` | string | 单位矩阵 | + +#### DiamondGradient +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | (Resources 中必填) | +| `center` | point | 0,0 | +| `halfDiagonal` | float | (必填) | +| `matrix` | string | 单位矩阵 | + +#### ColorStop +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `offset` | float | (必填) | +| `color` | color | (必填) | + +#### ImagePattern +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | (Resources 中必填) | +| `image` | idref | (必填) | +| `tileModeX` | TileMode | clamp | +| `tileModeY` | TileMode | clamp | +| `sampling` | SamplingMode | linear | +| `matrix` | string | 单位矩阵 | + +### B.3 图层节点 + +#### Layer +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `id` | string | - | +| `name` | string | "" | +| `visible` | bool | true | +| `alpha` | float | 1 | +| `blendMode` | BlendMode | normal | +| `x` | float | 0 | +| `y` | float | 0 | +| `matrix` | string | 单位矩阵 | +| `matrix3D` | string | - | +| `preserve3D` | bool | false | +| `antiAlias` | bool | true | +| `groupOpacity` | bool | false | +| `passThroughBackground` | bool | true | +| `excludeChildEffectsInLayerStyle` | bool | false | +| `scrollRect` | string | - | +| `mask` | idref | - | +| `maskType` | MaskType | alpha | +| `composition` | idref | - | + +### B.4 图层样式节点 + +#### DropShadowStyle +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `offsetX` | float | 0 | +| `offsetY` | float | 0 | +| `blurrinessX` | float | 0 | +| `blurrinessY` | float | 0 | +| `color` | color | #000000 | +| `showBehindLayer` | bool | true | +| `blendMode` | BlendMode | normal | + +#### InnerShadowStyle +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `offsetX` | float | 0 | +| `offsetY` | float | 0 | +| `blurrinessX` | float | 0 | +| `blurrinessY` | float | 0 | +| `color` | color | #000000 | +| `blendMode` | BlendMode | normal | + +#### BackgroundBlurStyle +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `blurrinessX` | float | 0 | +| `blurrinessY` | float | 0 | +| `tileMode` | TileMode | mirror | +| `blendMode` | BlendMode | normal | + +### B.5 滤镜节点 + +#### BlurFilter +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `blurrinessX` | float | (必填) | +| `blurrinessY` | float | (必填) | +| `tileMode` | TileMode | decal | + +#### DropShadowFilter +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `offsetX` | float | 0 | +| `offsetY` | float | 0 | +| `blurrinessX` | float | 0 | +| `blurrinessY` | float | 0 | +| `color` | color | #000000 | +| `shadowOnly` | bool | false | + +#### InnerShadowFilter +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `offsetX` | float | 0 | +| `offsetY` | float | 0 | +| `blurrinessX` | float | 0 | +| `blurrinessY` | float | 0 | +| `color` | color | #000000 | +| `shadowOnly` | bool | false | + +#### BlendFilter +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `color` | color | (必填) | +| `blendMode` | BlendMode | normal | + +#### ColorMatrixFilter +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `matrix` | string | (必填) | + +### B.6 几何元素节点 + +#### Rectangle +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `center` | point | 0,0 | +| `size` | size | 100,100 | +| `roundness` | float | 0 | +| `reversed` | bool | false | + +#### Ellipse +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `center` | point | 0,0 | +| `size` | size | 100,100 | +| `reversed` | bool | false | + +#### Polystar +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `center` | point | 0,0 | +| `polystarType` | PolystarType | star | +| `pointCount` | float | 5 | +| `outerRadius` | float | 100 | +| `innerRadius` | float | 50 | +| `rotation` | float | 0 | +| `outerRoundness` | float | 0 | +| `innerRoundness` | float | 0 | +| `reversed` | bool | false | + +#### Path +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `data` | string/idref | "" | +| `reversed` | bool | false | + +#### TextSpan +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `x` | float | 0 | +| `y` | float | 0 | +| `font` | string | (必填) | +| `fontSize` | float | 12 | +| `fontWeight` | int | 400 | +| `fontStyle` | enum | normal | +| `tracking` | float | 0 | +| `baselineShift` | float | 0 | +| `textAnchor` | TextAnchor | start | + +### B.7 绘制器节点 + +#### Fill +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `color` | color/idref | #000000 | +| `alpha` | float | 1 | +| `blendMode` | BlendMode | normal | +| `fillRule` | FillRule | winding | +| `placement` | LayerPlacement | background | + +#### Stroke +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `color` | color/idref | #000000 | +| `width` | float | 1 | +| `alpha` | float | 1 | +| `blendMode` | BlendMode | normal | +| `cap` | LineCap | butt | +| `join` | LineJoin | miter | +| `miterLimit` | float | 4 | +| `dashes` | string | - | +| `dashOffset` | float | 0 | +| `align` | StrokeAlign | center | +| `placement` | LayerPlacement | background | + +### B.8 形状修改器节点 + +#### TrimPath +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `start` | float | 0 | +| `end` | float | 1 | +| `offset` | float | 0 | +| `type` | TrimType | separate | + +#### RoundCorner +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `radius` | float | 10 | + +#### MergePath +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `mode` | MergePathOp | append | + +### B.9 文本修改器节点 + +#### TextModifier +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `anchorPoint` | point | 0,0 | +| `position` | point | 0,0 | +| `rotation` | float | 0 | +| `scale` | point | 1,1 | +| `skew` | float | 0 | +| `skewAxis` | float | 0 | +| `alpha` | float | 1 | +| `fillColor` | color | - | +| `strokeColor` | color | - | +| `strokeWidth` | float | - | + +#### RangeSelector +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `start` | float | 0 | +| `end` | float | 1 | +| `offset` | float | 0 | +| `unit` | SelectorUnit | percentage | +| `shape` | SelectorShape | square | +| `easeIn` | float | 0 | +| `easeOut` | float | 0 | +| `mode` | SelectorMode | add | +| `weight` | float | 1 | +| `randomizeOrder` | bool | false | +| `randomSeed` | int | 0 | + +#### TextPath +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `path` | idref | (必填) | +| `align` | TextPathAlign | start | +| `firstMargin` | float | 0 | +| `lastMargin` | float | 0 | +| `perpendicularToPath` | bool | true | +| `reversed` | bool | false | +| `forceAlignment` | bool | false | + +#### TextLayout +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `width` | float | (必填) | +| `height` | float | (必填) | +| `align` | TextAlign | left | +| `verticalAlign` | VerticalAlign | top | +| `lineHeight` | float | 1.2 | +| `indent` | float | 0 | +| `overflow` | Overflow | clip | + +### B.10 其他节点 + +#### Repeater +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `copies` | float | 3 | +| `offset` | float | 0 | +| `order` | RepeaterOrder | belowOriginal | +| `anchorPoint` | point | 0,0 | +| `position` | point | 100,100 | +| `rotation` | float | 0 | +| `scale` | point | 1,1 | +| `startAlpha` | float | 1 | +| `endAlpha` | float | 1 | + +#### Group +| 属性 | 类型 | 默认值 | +|------|------|--------| +| `name` | string | "" | +| `anchorPoint` | point | 0,0 | +| `position` | point | 0,0 | +| `rotation` | float | 0 | +| `scale` | point | 1,1 | +| `skew` | float | 0 | +| `skewAxis` | float | 0 | +| `alpha` | float | 1 | + +--- + +## Appendix C. Node Hierarchy(节点分类与包含关系) + +本附录描述节点的分类和嵌套规则。 + +### C.1 节点分类 + +| 分类 | 节点 | +|------|------| +| **文档根** | `pagx` | +| **资源** | `Resources`, `Image`, `PathData`, `SolidColor`, `LinearGradient`, `RadialGradient`, `ConicGradient`, `DiamondGradient`, `ColorStop`, `ImagePattern`, `Composition` | +| **图层** | `Layer` | +| **图层样式** | `DropShadowStyle`, `InnerShadowStyle`, `BackgroundBlurStyle` | +| **滤镜** | `BlurFilter`, `DropShadowFilter`, `InnerShadowFilter`, `BlendFilter`, `ColorMatrixFilter` | +| **几何元素** | `Rectangle`, `Ellipse`, `Polystar`, `Path`, `TextSpan` | +| **绘制器** | `Fill`, `Stroke` | +| **形状修改器** | `TrimPath`, `RoundCorner`, `MergePath` | +| **文本修改器** | `TextModifier`, `RangeSelector`, `TextPath`, `TextLayout` | +| **其他** | `Repeater`, `Group` | + +### C.2 包含关系 + +``` +pagx +├── Resources +│ ├── Image +│ ├── PathData +│ ├── SolidColor +│ ├── LinearGradient → ColorStop* +│ ├── RadialGradient → ColorStop* +│ ├── ConicGradient → ColorStop* +│ ├── DiamondGradient → ColorStop* +│ ├── ImagePattern +│ └── Composition → Layer* +│ +└── Layer* + ├── contents + │ └── VectorElement*(见下方) + ├── styles + │ ├── DropShadowStyle + │ ├── InnerShadowStyle + │ └── BackgroundBlurStyle + ├── filters + │ ├── BlurFilter + │ ├── DropShadowFilter + │ ├── InnerShadowFilter + │ ├── BlendFilter + │ └── ColorMatrixFilter + └── Layer*(子图层) +``` + +### C.3 VectorElement 包含关系 + +`contents` 和 `Group` 可包含以下 VectorElement: + +``` +contents / Group +├── Rectangle +├── Ellipse +├── Polystar +├── Path +├── TextSpan +├── Fill(可内嵌颜色源) +│ └── SolidColor / LinearGradient / RadialGradient / ConicGradient / DiamondGradient / ImagePattern +├── Stroke(可内嵌颜色源) +│ └── SolidColor / LinearGradient / RadialGradient / ConicGradient / DiamondGradient / ImagePattern +├── TrimPath +├── RoundCorner +├── MergePath +├── TextModifier → RangeSelector* +├── TextPath +├── TextLayout +├── Repeater +└── Group*(递归) +``` + +--- + +## Appendix D. Examples(示例) + +### D.1 Complete Example(完整示例) ```xml @@ -1813,3 +2286,103 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ``` + +### D.2 Feature Examples(功能示例) + +以下示例省略外层 ``、``、`` 等样板代码,仅展示核心片段。 + +#### D.2.1 多重填充/描边 + +```xml + + + + + + + + + + + + +``` + +#### D.2.2 TrimPath 路径裁剪 + +```xml + + + + + + + + + + + +``` + +#### D.2.3 Repeater 阵列效果 + +```xml + + + + + + + + + + + +``` + +#### D.2.4 TextModifier 逐字变换 + +```xml + + + + + + + + + + + + + + + + + +``` + +#### D.2.5 TextLayout 富文本排版 + +```xml + + This is + bold + and + italic + text in a paragraph that will automatically wrap to fit the container width. + + + +``` + +#### D.2.6 TextPath 沿路径文本 + +```xml + + + + + + +``` From 598505cd51e44366877354906168a2ece98c4d0b Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 11:58:01 +0800 Subject: [PATCH 043/678] Remove redundant Resources-required constraint from attribute tables. --- pagx/docs/pagx_spec.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 20b4c9269a..eebcf100b9 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -61,8 +61,8 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 | 默认值格式 | 含义 | |------------|------| | `(必填)` | 属性必须指定,没有默认值 | -| `(Resources 中必填)` | 仅在 Resources 中定义时必填,内联使用时可省略 | | 具体值(如 `0`、`true`、`normal`) | 属性可选,未指定时使用该默认值 | +| `-` | 属性可选,未指定时不生效 | ### 2.3 基本数值类型 @@ -205,7 +205,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | (Resources 中必填) | 唯一标识 | +| `id` | string | - | 唯一标识 | | `color` | color | (必填) | 颜色值 | #### 2.11.2 LinearGradient(线性渐变) @@ -221,7 +221,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | (Resources 中必填) | 唯一标识 | +| `id` | string | - | 唯一标识 | | `startPoint` | point | 0,0 | 起点 | | `endPoint` | point | (必填) | 终点 | | `matrix` | string | 单位矩阵 | 变换矩阵 | @@ -241,7 +241,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | (Resources 中必填) | 唯一标识 | +| `id` | string | - | 唯一标识 | | `center` | point | 0,0 | 中心点 | | `radius` | float | (必填) | 渐变半径 | | `matrix` | string | 单位矩阵 | 变换矩阵 | @@ -261,7 +261,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | (Resources 中必填) | 唯一标识 | +| `id` | string | - | 唯一标识 | | `center` | point | 0,0 | 中心点 | | `startAngle` | float | 0 | 起始角度 | | `endAngle` | float | 360 | 结束角度 | @@ -282,7 +282,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | (Resources 中必填) | 唯一标识 | +| `id` | string | - | 唯一标识 | | `center` | point | 0,0 | 中心点 | | `halfDiagonal` | float | (必填) | 半对角线长度 | | `matrix` | string | 单位矩阵 | 变换矩阵 | @@ -349,7 +349,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | (Resources 中必填) | 唯一标识 | +| `id` | string | - | 唯一标识 | | `image` | idref | (必填) | 图片引用 "#id" | | `tileModeX` | TileMode | clamp | X 方向平铺模式(见下方) | | `tileModeY` | TileMode | clamp | Y 方向平铺模式(见下方) | @@ -1815,13 +1815,13 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### SolidColor | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | (Resources 中必填) | +| `id` | string | - | | `color` | color | (必填) | #### LinearGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | (Resources 中必填) | +| `id` | string | - | | `startPoint` | point | 0,0 | | `endPoint` | point | (必填) | | `matrix` | string | 单位矩阵 | @@ -1829,7 +1829,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### RadialGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | (Resources 中必填) | +| `id` | string | - | | `center` | point | 0,0 | | `radius` | float | (必填) | | `matrix` | string | 单位矩阵 | @@ -1837,7 +1837,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### ConicGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | (Resources 中必填) | +| `id` | string | - | | `center` | point | 0,0 | | `startAngle` | float | 0 | | `endAngle` | float | 360 | @@ -1846,7 +1846,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### DiamondGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | (Resources 中必填) | +| `id` | string | - | | `center` | point | 0,0 | | `halfDiagonal` | float | (必填) | | `matrix` | string | 单位矩阵 | @@ -1860,7 +1860,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### ImagePattern | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | (Resources 中必填) | +| `id` | string | - | | `image` | idref | (必填) | | `tileModeX` | TileMode | clamp | | `tileModeY` | TileMode | clamp | From 7edefd911931bcc6510e024801b5db21b12ac5c8 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:03:16 +0800 Subject: [PATCH 044/678] Define id as a universal attribute applicable to any element. --- pagx/docs/pagx_spec.md | 84 +++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index eebcf100b9..81e124fb73 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -64,7 +64,15 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 | 具体值(如 `0`、`true`、`normal`) | 属性可选,未指定时使用该默认值 | | `-` | 属性可选,未指定时不生效 | -### 2.3 基本数值类型 +### 2.3 通用属性 + +以下属性可用于任意元素,不在各节点的属性表中重复列出: + +| 属性 | 类型 | 说明 | +|------|------|------| +| `id` | string | 唯一标识符,用于被其他元素引用(如遮罩、颜色源)。在文档中必须唯一,不能为空或包含空白字符 | + +### 2.4 基本数值类型 | 类型 | 说明 | 示例 | |------|------|------| @@ -75,7 +83,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 | `enum` | 枚举值 | `normal`、`multiply` | | `idref` | ID 引用 | `#gradientId`、`#maskLayer` | -### 2.4 点(Point) +### 2.5 点(Point) 点使用逗号分隔的两个浮点数表示: @@ -85,7 +93,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 **示例**:`"100,200"`、`"0.5,0.5"`、`"-50,100"` -### 2.5 矩形(Rect) +### 2.6 矩形(Rect) 矩形使用逗号分隔的四个浮点数表示: @@ -95,7 +103,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 **示例**:`"0,0,100,100"`、`"10,20,200,150"` -### 2.6 变换矩阵(Matrix) +### 2.7 变换矩阵(Matrix) #### 2D 变换矩阵 @@ -122,7 +130,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 "m00,m10,m20,m30,m01,m11,m21,m31,m02,m12,m22,m32,m03,m13,m23,m33" ``` -### 2.7 颜色(Color) +### 2.8 颜色(Color) PAGX 支持多种颜色格式: @@ -134,7 +142,7 @@ PAGX 支持多种颜色格式: | 色域 | `color(display-p3 1 0 0)` | 广色域颜色 | | 引用 | `#resourceId` | 引用 Resources 中定义的颜色源 | -### 2.8 路径数据(Path Data) +### 2.9 路径数据(Path Data) 路径数据使用 SVG 路径语法,支持以下命令: @@ -153,7 +161,7 @@ PAGX 支持多种颜色格式: **示例**:`"M 0 0 L 100 0 L 100 100 Z"` -### 2.9 PathData(路径数据资源) +### 2.10 PathData(路径数据资源) PathData 定义可在文档中引用的路径数据,支持路径复用。 @@ -163,10 +171,9 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | (必填) | 唯一标识 | -| `data` | string | (必填) | SVG 路径数据(语法见 2.8 节) | +| `data` | string | (必填) | SVG 路径数据(语法见 2.9 节) | -### 2.10 Image(图片) +### 2.11 Image(图片) 图片资源定义可在文档中引用的位图数据。 @@ -180,12 +187,11 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | (必填) | 唯一标识 | | `source` | string | (必填) | 文件路径或数据 URI | **支持格式**:PNG、JPEG、WebP、GIF -### 2.11 颜色源(Color Source) +### 2.12 颜色源(Color Source) 颜色源定义可用于渲染的颜色,支持两种定义方式: @@ -197,7 +203,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 - 被多处引用的颜色源应定义在 Resources 中以便复用 - ImagePattern 使用 objectBoundingBox 时(tile 尺寸依赖形状尺寸),通常需要内联定义,因为不同形状需要不同的 matrix -#### 2.11.1 SolidColor(纯色) +#### 2.12.1 SolidColor(纯色) ```xml @@ -205,10 +211,9 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | | `color` | color | (必填) | 颜色值 | -#### 2.11.2 LinearGradient(线性渐变) +#### 2.12.2 LinearGradient(线性渐变) 线性渐变沿起点到终点的方向插值。 @@ -221,14 +226,13 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | | `startPoint` | point | 0,0 | 起点 | | `endPoint` | point | (必填) | 终点 | | `matrix` | string | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由 P 在起点-终点连线上的投影位置决定。 -#### 2.11.3 RadialGradient(径向渐变) +#### 2.12.3 RadialGradient(径向渐变) 径向渐变从中心向外辐射。 @@ -241,14 +245,13 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | | `center` | point | 0,0 | 中心点 | | `radius` | float | (必填) | 渐变半径 | | `matrix` | string | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由 `distance(P, center) / radius` 决定。 -#### 2.11.4 ConicGradient(锥形渐变) +#### 2.12.4 ConicGradient(锥形渐变) 锥形渐变(也称扫描渐变)沿圆周方向插值。 @@ -261,7 +264,6 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | | `center` | point | 0,0 | 中心点 | | `startAngle` | float | 0 | 起始角度 | | `endAngle` | float | 360 | 结束角度 | @@ -269,7 +271,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 **计算**:对于点 P,其颜色由 `atan2(P.y - center.y, P.x - center.x)` 在 `[startAngle, endAngle]` 范围内的比例决定。 -#### 2.11.5 DiamondGradient(菱形渐变) +#### 2.12.5 DiamondGradient(菱形渐变) 菱形渐变从中心向四角辐射。 @@ -282,14 +284,13 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | | `center` | point | 0,0 | 中心点 | | `halfDiagonal` | float | (必填) | 半对角线长度 | | `matrix` | string | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由曼哈顿距离 `(|P.x - center.x| + |P.y - center.y|) / halfDiagonal` 决定。 -#### 2.11.6 ColorStop(渐变色标) +#### 2.12.6 ColorStop(渐变色标) ```xml @@ -310,7 +311,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 - 如果没有 `offset = 1` 的色标,使用最后一个色标的颜色填充 - **渐变变换**:`matrix` 属性对渐变坐标系应用变换 -#### 2.11.7 颜色源坐标系统 +#### 2.12.7 颜色源坐标系统 所有颜色源(渐变、图案)的坐标系是**相对于几何元素的局部坐标系原点**。 @@ -339,7 +340,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 - 对该图层应用 `scale(2, 2)` 变换:矩形变为 200×200,渐变也随之放大,视觉效果保持一致 - 直接将 Rectangle 的 size 改为 200,200:矩形变为 200×200,但渐变坐标不变,只覆盖矩形的左半部分 -#### 2.11.8 ImagePattern(图片图案) +#### 2.12.8 ImagePattern(图片图案) 图片图案使用图片作为颜色源。 @@ -349,7 +350,6 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识 | | `image` | idref | (必填) | 图片引用 "#id" | | `tileModeX` | TileMode | clamp | X 方向平铺模式(见下方) | | `tileModeY` | TileMode | clamp | Y 方向平铺模式(见下方) | @@ -446,9 +446,9 @@ PAGX 使用标准的 2D 笛卡尔坐标系: **支持的资源类型**: -- `Image`:图片资源(见 2.10 节) -- `PathData`:路径数据(见 2.9 节),可被 Path 元素和 TextPath 修改器引用 -- 颜色源(见 2.11 节):`SolidColor`、`LinearGradient`、`RadialGradient`、`ConicGradient`、`DiamondGradient`、`ImagePattern` +- `Image`:图片资源(见 2.11 节) +- `PathData`:路径数据(见 2.10 节),可被 Path 元素和 TextPath 修改器引用 +- 颜色源(见 2.12 节):`SolidColor`、`LinearGradient`、`RadialGradient`、`ConicGradient`、`DiamondGradient`、`ImagePattern` - `Composition`:合成(见下方) #### 3.3.1 Composition(合成) @@ -468,7 +468,6 @@ PAGX 使用标准的 2D 笛卡尔坐标系: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | (必填) | 唯一标识 | | `width` | float | (必填) | 合成宽度 | | `height` | float | (必填) | 合成高度 | @@ -539,7 +538,6 @@ PAGX 文档采用层级结构组织内容: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `id` | string | - | 唯一标识,用于引用 | | `name` | string | "" | 显示名称 | | `visible` | bool | true | 是否可见 | | `alpha` | float | 1 | 透明度 0~1 | @@ -668,7 +666,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | 0 | X 模糊半径 | | `blurrinessY` | float | 0 | Y 模糊半径 | -| `tileMode` | TileMode | mirror | 平铺模式(见 2.11.8) | +| `tileMode` | TileMode | mirror | 平铺模式(见 2.12.8) | **渲染步骤**: 1. 获取图层边界下方的背景内容 @@ -693,7 +691,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | (必填) | X 模糊半径 | | `blurrinessY` | float | (必填) | Y 模糊半径 | -| `tileMode` | TileMode | decal | 平铺模式(见 2.11.8) | +| `tileMode` | TileMode | decal | 平铺模式(见 2.12.8) | #### 4.3.2 DropShadowFilter(投影阴影滤镜) @@ -972,7 +970,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `data` | string/idref | "" | SVG 路径数据(语法见 2.8 节)或 PathData 资源引用 "#id"(见 2.9 节) | +| `data` | string/idref | "" | SVG 路径数据(语法见 2.9 节)或 PathData 资源引用 "#id"(见 2.10 节) | | `reversed` | bool | false | 反转路径方向 | #### 5.2.5 TextSpan(文本片段) @@ -1409,7 +1407,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `path` | idref | (必填) | PathData 资源引用 "#id"(见 2.9 节) | +| `path` | idref | (必填) | PathData 资源引用 "#id"(见 2.10 节) | | `align` | TextPathAlign | start | 对齐模式(见下方) | | `firstMargin` | float | 0 | 起始边距 | | `lastMargin` | float | 0 | 结束边距 | @@ -1741,8 +1739,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: |------|------|----------| | **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter` | 4.1 | | **MaskType** | `alpha`, `luminance`, `contour` | 4.1 | -| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | 2.11.8 | -| **SamplingMode** | `nearest`, `linear`, `mipmap` | 2.11.8 | +| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | 2.12.8 | +| **SamplingMode** | `nearest`, `linear`, `mipmap` | 2.12.8 | ### A.2 绘制器相关 @@ -1782,6 +1780,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: 本附录列出所有节点的属性定义,省略详细说明。 +**注意**:`id` 属性为通用属性,可用于任意元素(见 2.3 节),各表中不再重复列出。 + ### B.1 文档结构节点 #### pagx @@ -1794,7 +1794,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### Composition | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | (必填) | | `width` | float | (必填) | | `height` | float | (必填) | @@ -1803,25 +1802,21 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### Image | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | (必填) | | `source` | string | (必填) | #### PathData | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | (必填) | | `data` | string | (必填) | #### SolidColor | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | - | | `color` | color | (必填) | #### LinearGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | - | | `startPoint` | point | 0,0 | | `endPoint` | point | (必填) | | `matrix` | string | 单位矩阵 | @@ -1829,7 +1824,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### RadialGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | - | | `center` | point | 0,0 | | `radius` | float | (必填) | | `matrix` | string | 单位矩阵 | @@ -1837,7 +1831,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### ConicGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | - | | `center` | point | 0,0 | | `startAngle` | float | 0 | | `endAngle` | float | 360 | @@ -1846,7 +1839,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### DiamondGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | - | | `center` | point | 0,0 | | `halfDiagonal` | float | (必填) | | `matrix` | string | 单位矩阵 | @@ -1860,7 +1852,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### ImagePattern | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | - | | `image` | idref | (必填) | | `tileModeX` | TileMode | clamp | | `tileModeY` | TileMode | clamp | @@ -1872,7 +1863,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### Layer | 属性 | 类型 | 默认值 | |------|------|--------| -| `id` | string | - | | `name` | string | "" | | `visible` | bool | true | | `alpha` | float | 1 | From 7fcc0e8d97db4aaa63a7a63b9806dd758257e1e7 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:09:31 +0800 Subject: [PATCH 045/678] Add data-* custom attributes to PAGX spec as universal attributes. --- pagx/docs/pagx_spec.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 81e124fb73..69674826a8 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -71,6 +71,22 @@ PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也 | 属性 | 类型 | 说明 | |------|------|------| | `id` | string | 唯一标识符,用于被其他元素引用(如遮罩、颜色源)。在文档中必须唯一,不能为空或包含空白字符 | +| `data-*` | string | 自定义数据属性,用于存储应用特定的私有数据。`*` 可替换为任意名称(如 `data-name`、`data-layer-id`),运行时忽略这些属性 | + +**自定义属性说明**: + +- 属性名必须以 `data-` 开头,后跟至少一个字符 +- 属性名只能包含小写字母、数字和连字符(`-`),不能以连字符结尾 +- 属性值为任意字符串,由创建该属性的应用自行解释 +- 运行时不处理这些属性,仅用于工具间传递元数据或存储调试信息 + +**示例**: + +```xml + + ... + +``` ### 2.4 基本数值类型 @@ -1780,7 +1796,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: 本附录列出所有节点的属性定义,省略详细说明。 -**注意**:`id` 属性为通用属性,可用于任意元素(见 2.3 节),各表中不再重复列出。 +**注意**:`id` 和 `data-*` 属性为通用属性,可用于任意元素(见 2.3 节),各表中不再重复列出。 ### B.1 文档结构节点 From fb7e4e0e2689dbdb69db6308befb8d2ef66818d2 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:14:17 +0800 Subject: [PATCH 046/678] Merge PathData sections and clarify inline vs shared usage in PAGX spec. --- pagx/docs/pagx_spec.md | 66 ++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 69674826a8..67abfe4f24 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -158,9 +158,19 @@ PAGX 支持多种颜色格式: | 色域 | `color(display-p3 1 0 0)` | 广色域颜色 | | 引用 | `#resourceId` | 引用 Resources 中定义的颜色源 | -### 2.9 路径数据(Path Data) +### 2.9 PathData(路径数据) -路径数据使用 SVG 路径语法,支持以下命令: +PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素和 TextPath 修改器引用。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `data` | string | (必填) | SVG 路径数据 | + +**路径命令**(SVG 路径语法): | 命令 | 参数 | 说明 | |------|------|------| @@ -175,21 +185,7 @@ PAGX 支持多种颜色格式: | A/a | rx ry rotation large-arc sweep x y | 椭圆弧 | | Z/z | - | 闭合路径 | -**示例**:`"M 0 0 L 100 0 L 100 100 Z"` - -### 2.10 PathData(路径数据资源) - -PathData 定义可在文档中引用的路径数据,支持路径复用。 - -```xml - -``` - -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `data` | string | (必填) | SVG 路径数据(语法见 2.9 节) | - -### 2.11 Image(图片) +### 2.10 Image(图片) 图片资源定义可在文档中引用的位图数据。 @@ -207,7 +203,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 **支持格式**:PNG、JPEG、WebP、GIF -### 2.12 颜色源(Color Source) +### 2.11 颜色源(Color Source) 颜色源定义可用于渲染的颜色,支持两种定义方式: @@ -219,7 +215,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 - 被多处引用的颜色源应定义在 Resources 中以便复用 - ImagePattern 使用 objectBoundingBox 时(tile 尺寸依赖形状尺寸),通常需要内联定义,因为不同形状需要不同的 matrix -#### 2.12.1 SolidColor(纯色) +#### 2.11.1 SolidColor(纯色) ```xml @@ -229,7 +225,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 |------|------|--------|------| | `color` | color | (必填) | 颜色值 | -#### 2.12.2 LinearGradient(线性渐变) +#### 2.11.2 LinearGradient(线性渐变) 线性渐变沿起点到终点的方向插值。 @@ -248,7 +244,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 **计算**:对于点 P,其颜色由 P 在起点-终点连线上的投影位置决定。 -#### 2.12.3 RadialGradient(径向渐变) +#### 2.11.3 RadialGradient(径向渐变) 径向渐变从中心向外辐射。 @@ -267,7 +263,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 **计算**:对于点 P,其颜色由 `distance(P, center) / radius` 决定。 -#### 2.12.4 ConicGradient(锥形渐变) +#### 2.11.4 ConicGradient(锥形渐变) 锥形渐变(也称扫描渐变)沿圆周方向插值。 @@ -287,7 +283,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 **计算**:对于点 P,其颜色由 `atan2(P.y - center.y, P.x - center.x)` 在 `[startAngle, endAngle]` 范围内的比例决定。 -#### 2.12.5 DiamondGradient(菱形渐变) +#### 2.11.5 DiamondGradient(菱形渐变) 菱形渐变从中心向四角辐射。 @@ -306,7 +302,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 **计算**:对于点 P,其颜色由曼哈顿距离 `(|P.x - center.x| + |P.y - center.y|) / halfDiagonal` 决定。 -#### 2.12.6 ColorStop(渐变色标) +#### 2.11.6 ColorStop(渐变色标) ```xml @@ -327,7 +323,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 - 如果没有 `offset = 1` 的色标,使用最后一个色标的颜色填充 - **渐变变换**:`matrix` 属性对渐变坐标系应用变换 -#### 2.12.7 颜色源坐标系统 +#### 2.11.7 颜色源坐标系统 所有颜色源(渐变、图案)的坐标系是**相对于几何元素的局部坐标系原点**。 @@ -356,7 +352,7 @@ PathData 定义可在文档中引用的路径数据,支持路径复用。 - 对该图层应用 `scale(2, 2)` 变换:矩形变为 200×200,渐变也随之放大,视觉效果保持一致 - 直接将 Rectangle 的 size 改为 200,200:矩形变为 200×200,但渐变坐标不变,只覆盖矩形的左半部分 -#### 2.12.8 ImagePattern(图片图案) +#### 2.11.8 ImagePattern(图片图案) 图片图案使用图片作为颜色源。 @@ -462,9 +458,9 @@ PAGX 使用标准的 2D 笛卡尔坐标系: **支持的资源类型**: -- `Image`:图片资源(见 2.11 节) -- `PathData`:路径数据(见 2.10 节),可被 Path 元素和 TextPath 修改器引用 -- 颜色源(见 2.12 节):`SolidColor`、`LinearGradient`、`RadialGradient`、`ConicGradient`、`DiamondGradient`、`ImagePattern` +- `Image`:图片资源(见 2.10 节) +- `PathData`:路径数据(见 2.9 节),可被 Path 元素和 TextPath 修改器引用 +- 颜色源(见 2.11 节):`SolidColor`、`LinearGradient`、`RadialGradient`、`ConicGradient`、`DiamondGradient`、`ImagePattern` - `Composition`:合成(见下方) #### 3.3.1 Composition(合成) @@ -682,7 +678,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | 0 | X 模糊半径 | | `blurrinessY` | float | 0 | Y 模糊半径 | -| `tileMode` | TileMode | mirror | 平铺模式(见 2.12.8) | +| `tileMode` | TileMode | mirror | 平铺模式(见 2.11.8) | **渲染步骤**: 1. 获取图层边界下方的背景内容 @@ -707,7 +703,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | (必填) | X 模糊半径 | | `blurrinessY` | float | (必填) | Y 模糊半径 | -| `tileMode` | TileMode | decal | 平铺模式(见 2.12.8) | +| `tileMode` | TileMode | decal | 平铺模式(见 2.11.8) | #### 4.3.2 DropShadowFilter(投影阴影滤镜) @@ -986,7 +982,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `data` | string/idref | "" | SVG 路径数据(语法见 2.9 节)或 PathData 资源引用 "#id"(见 2.10 节) | +| `data` | string/idref | "" | SVG 路径数据或 PathData 资源引用 "#id"(语法见 2.9 节) | | `reversed` | bool | false | 反转路径方向 | #### 5.2.5 TextSpan(文本片段) @@ -1423,7 +1419,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `path` | idref | (必填) | PathData 资源引用 "#id"(见 2.10 节) | +| `path` | idref | (必填) | PathData 资源引用 "#id"(见 2.9 节) | | `align` | TextPathAlign | start | 对齐模式(见下方) | | `firstMargin` | float | 0 | 起始边距 | | `lastMargin` | float | 0 | 结束边距 | @@ -1755,8 +1751,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: |------|------|----------| | **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter` | 4.1 | | **MaskType** | `alpha`, `luminance`, `contour` | 4.1 | -| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | 2.12.8 | -| **SamplingMode** | `nearest`, `linear`, `mipmap` | 2.12.8 | +| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | 2.11.8 | +| **SamplingMode** | `nearest`, `linear`, `mipmap` | 2.11.8 | ### A.2 绘制器相关 From 86b97eb2516ffb1fbfaba259f5686cdaad1d4ad7 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:20:06 +0800 Subject: [PATCH 047/678] Add external resource path resolution rules in PAGX spec. --- pagx/docs/pagx_spec.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 67abfe4f24..c31d3e902d 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -20,7 +20,7 @@ ### 1.2 文件结构 -PAGX 是纯 XML 文件(`.pagx`),可引用同目录下的外部资源,也支持 base64 数据 URI 内嵌图片。发布时可转换为内嵌所有资源的二进制 PAG 格式以优化加载性能。 +PAGX 是纯 XML 文件(`.pagx`),可引用外部资源(相对路径,基于 PAGX 文件所在目录),也支持 base64 数据 URI 内嵌图片。发布时可转换为内嵌所有资源的二进制 PAG 格式以优化加载性能。 ### 1.3 文档组织 @@ -192,9 +192,10 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 ```xml + - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -203,6 +204,13 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **支持格式**:PNG、JPEG、WebP、GIF +**路径解析规则**: + +- **相对路径**:相对于 PAGX 文件所在目录解析(如 `photo.png`、`assets/logo.png`) +- **数据 URI**:以 `data:` 开头,内嵌 base64 编码的图片数据 +- 路径分隔符统一使用 `/`(正斜杠),不支持 `\`(反斜杠) +- 不支持绝对路径和 `../` 父目录引用,所有外部资源必须位于 PAGX 文件所在目录或其子目录内 + ### 2.11 颜色源(Color Source) 颜色源定义可用于渲染的颜色,支持两种定义方式: From 50afaff9e141163de897aa4f2d8c383d3943a652 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:24:21 +0800 Subject: [PATCH 048/678] Add unified external resource reference section for images videos audio and fonts. --- pagx/docs/pagx_spec.md | 67 ++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index c31d3e902d..28199f0281 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -20,7 +20,7 @@ ### 1.2 文件结构 -PAGX 是纯 XML 文件(`.pagx`),可引用外部资源(相对路径,基于 PAGX 文件所在目录),也支持 base64 数据 URI 内嵌图片。发布时可转换为内嵌所有资源的二进制 PAG 格式以优化加载性能。 +PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视频、音频、字体等),也支持通过数据 URI 内嵌资源。发布时可转换为内嵌所有资源的二进制 PAG 格式以优化加载性能。 ### 1.3 文档组织 @@ -185,33 +185,42 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 | A/a | rx ry rotation large-arc sweep x y | 椭圆弧 | | Z/z | - | 闭合路径 | -### 2.10 Image(图片) +### 2.10 外部资源引用(External Resource Reference) -图片资源定义可在文档中引用的位图数据。 +外部资源通过相对路径或数据 URI 引用,适用于图片、视频、音频、字体等文件。 ```xml - + - - + + ``` -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `source` | string | (必填) | 文件路径或数据 URI | - -**支持格式**:PNG、JPEG、WebP、GIF - **路径解析规则**: -- **相对路径**:相对于 PAGX 文件所在目录解析(如 `photo.png`、`assets/logo.png`) -- **数据 URI**:以 `data:` 开头,内嵌 base64 编码的图片数据 +- **相对路径**:相对于 PAGX 文件所在目录解析 +- **数据 URI**:以 `data:` 开头,格式为 `data:;base64,` - 路径分隔符统一使用 `/`(正斜杠),不支持 `\`(反斜杠) - 不支持绝对路径和 `../` 父目录引用,所有外部资源必须位于 PAGX 文件所在目录或其子目录内 -### 2.11 颜色源(Color Source) +### 2.11 Image(图片) + +图片资源定义可在文档中引用的位图数据。 + +```xml + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `source` | string | (必填) | 文件路径或数据 URI(见 2.10 节) | + +**支持格式**:PNG、JPEG、WebP、GIF + +### 2.12 颜色源(Color Source) 颜色源定义可用于渲染的颜色,支持两种定义方式: @@ -223,7 +232,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 - 被多处引用的颜色源应定义在 Resources 中以便复用 - ImagePattern 使用 objectBoundingBox 时(tile 尺寸依赖形状尺寸),通常需要内联定义,因为不同形状需要不同的 matrix -#### 2.11.1 SolidColor(纯色) +#### 2.12.1 SolidColor(纯色) ```xml @@ -233,7 +242,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 |------|------|--------|------| | `color` | color | (必填) | 颜色值 | -#### 2.11.2 LinearGradient(线性渐变) +#### 2.12.2 LinearGradient(线性渐变) 线性渐变沿起点到终点的方向插值。 @@ -252,7 +261,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **计算**:对于点 P,其颜色由 P 在起点-终点连线上的投影位置决定。 -#### 2.11.3 RadialGradient(径向渐变) +#### 2.12.3 RadialGradient(径向渐变) 径向渐变从中心向外辐射。 @@ -271,7 +280,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **计算**:对于点 P,其颜色由 `distance(P, center) / radius` 决定。 -#### 2.11.4 ConicGradient(锥形渐变) +#### 2.12.4 ConicGradient(锥形渐变) 锥形渐变(也称扫描渐变)沿圆周方向插值。 @@ -291,7 +300,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **计算**:对于点 P,其颜色由 `atan2(P.y - center.y, P.x - center.x)` 在 `[startAngle, endAngle]` 范围内的比例决定。 -#### 2.11.5 DiamondGradient(菱形渐变) +#### 2.12.5 DiamondGradient(菱形渐变) 菱形渐变从中心向四角辐射。 @@ -310,7 +319,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **计算**:对于点 P,其颜色由曼哈顿距离 `(|P.x - center.x| + |P.y - center.y|) / halfDiagonal` 决定。 -#### 2.11.6 ColorStop(渐变色标) +#### 2.12.6 ColorStop(渐变色标) ```xml @@ -331,7 +340,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 - 如果没有 `offset = 1` 的色标,使用最后一个色标的颜色填充 - **渐变变换**:`matrix` 属性对渐变坐标系应用变换 -#### 2.11.7 颜色源坐标系统 +#### 2.12.7 颜色源坐标系统 所有颜色源(渐变、图案)的坐标系是**相对于几何元素的局部坐标系原点**。 @@ -360,7 +369,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 - 对该图层应用 `scale(2, 2)` 变换:矩形变为 200×200,渐变也随之放大,视觉效果保持一致 - 直接将 Rectangle 的 size 改为 200,200:矩形变为 200×200,但渐变坐标不变,只覆盖矩形的左半部分 -#### 2.11.8 ImagePattern(图片图案) +#### 2.12.8 ImagePattern(图片图案) 图片图案使用图片作为颜色源。 @@ -466,9 +475,9 @@ PAGX 使用标准的 2D 笛卡尔坐标系: **支持的资源类型**: -- `Image`:图片资源(见 2.10 节) +- `Image`:图片资源(见 2.11 节) - `PathData`:路径数据(见 2.9 节),可被 Path 元素和 TextPath 修改器引用 -- 颜色源(见 2.11 节):`SolidColor`、`LinearGradient`、`RadialGradient`、`ConicGradient`、`DiamondGradient`、`ImagePattern` +- 颜色源(见 2.12 节):`SolidColor`、`LinearGradient`、`RadialGradient`、`ConicGradient`、`DiamondGradient`、`ImagePattern` - `Composition`:合成(见下方) #### 3.3.1 Composition(合成) @@ -686,7 +695,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | 0 | X 模糊半径 | | `blurrinessY` | float | 0 | Y 模糊半径 | -| `tileMode` | TileMode | mirror | 平铺模式(见 2.11.8) | +| `tileMode` | TileMode | mirror | 平铺模式(见 2.12.8) | **渲染步骤**: 1. 获取图层边界下方的背景内容 @@ -711,7 +720,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | (必填) | X 模糊半径 | | `blurrinessY` | float | (必填) | Y 模糊半径 | -| `tileMode` | TileMode | decal | 平铺模式(见 2.11.8) | +| `tileMode` | TileMode | decal | 平铺模式(见 2.12.8) | #### 4.3.2 DropShadowFilter(投影阴影滤镜) @@ -1759,8 +1768,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: |------|------|----------| | **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter` | 4.1 | | **MaskType** | `alpha`, `luminance`, `contour` | 4.1 | -| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | 2.11.8 | -| **SamplingMode** | `nearest`, `linear`, `mipmap` | 2.11.8 | +| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | 2.12.8 | +| **SamplingMode** | `nearest`, `linear`, `mipmap` | 2.12.8 | ### A.2 绘制器相关 From 5afbfa157121b0bb826396e3f9f3d54bc915921d Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:35:56 +0800 Subject: [PATCH 049/678] Remove objectBoundingBox reference and clean up unnecessary id attributes from XML examples. --- pagx/docs/pagx_spec.md | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 28199f0281..042bca512e 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -191,11 +191,11 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 ```xml - - + + - + ``` **路径解析规则**: @@ -210,8 +210,8 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 图片资源定义可在文档中引用的位图数据。 ```xml - - + + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -227,15 +227,10 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 1. **共享定义**:在 `` 中预定义,通过 `#id` 引用。适用于**被多处引用**的颜色源。 2. **内联定义**:直接嵌套在 `` 或 `` 元素内部。适用于**仅使用一次**的颜色源,更简洁。 -**最佳实践**: -- 只使用一次的颜色源应内联定义,避免在 Resources 中增加不必要的条目 -- 被多处引用的颜色源应定义在 Resources 中以便复用 -- ImagePattern 使用 objectBoundingBox 时(tile 尺寸依赖形状尺寸),通常需要内联定义,因为不同形状需要不同的 matrix - #### 2.12.1 SolidColor(纯色) ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -247,7 +242,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 线性渐变沿起点到终点的方向插值。 ```xml - + @@ -266,7 +261,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 径向渐变从中心向外辐射。 ```xml - + @@ -285,7 +280,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 锥形渐变(也称扫描渐变)沿圆周方向插值。 ```xml - + @@ -305,7 +300,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 菱形渐变从中心向四角辐射。 ```xml - + @@ -353,10 +348,12 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **示例**:在 100×100 的区域内绘制一个从左到右的线性渐变: ```xml - - - - + + + + + + @@ -374,7 +371,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 图片图案使用图片作为颜色源。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -413,7 +410,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 ```xml - + ``` --- @@ -543,7 +540,7 @@ PAGX 文档采用层级结构组织内容: `` 是内容和子图层的基本容器。 ```xml - + From 0b4fffd492ec6a7bdb6d6732618d0e9c08d4a712 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:37:13 +0800 Subject: [PATCH 050/678] Allow absolute paths in external resource references in PAGX spec. --- pagx/docs/pagx_spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 042bca512e..86453e4d26 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -203,7 +203,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 - **相对路径**:相对于 PAGX 文件所在目录解析 - **数据 URI**:以 `data:` 开头,格式为 `data:;base64,` - 路径分隔符统一使用 `/`(正斜杠),不支持 `\`(反斜杠) -- 不支持绝对路径和 `../` 父目录引用,所有外部资源必须位于 PAGX 文件所在目录或其子目录内 +- 不支持 `../` 父目录引用,相对路径引用的外部资源必须位于 PAGX 文件所在目录或其子目录内 ### 2.11 Image(图片) From 40a3f11ea13b4cc292e4a40c5b1bb35e61427701 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:37:27 +0800 Subject: [PATCH 051/678] feat(pagx): preserve SVG id and data-* attributes, move Resources to end - Add customData field to LayerNode for storing data-* attributes - Parse and write data-* attributes (stored without "data-" prefix) - Generate unique IDs with "_" prefix to avoid collision with SVG IDs - Remove redundant id->name assignment in SVG parser - Move Resources section to end of PAGX file for better readability - Update spec to allow Resources at any position (require forward ref support) - Add test SVG files for customData and path deduplication verification --- pagx/include/pagx/PAGXNode.h | 5 +++ pagx/src/PAGXXMLParser.cpp | 7 +++ pagx/src/PAGXXMLWriter.cpp | 17 +++++--- pagx/src/svg/PAGXSVGParser.cpp | 54 ++++++++++++++++++++++-- pagx/src/svg/SVGParserInternal.h | 12 ++++++ resources/apitest/SVG/customData.svg | 15 +++++++ resources/apitest/SVG/duplicatePaths.svg | 15 +++++++ 7 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 resources/apitest/SVG/customData.svg create mode 100644 resources/apitest/SVG/duplicatePaths.svg diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index 3368d95a57..add74c75a0 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -20,6 +20,7 @@ #include #include +#include #include #include "pagx/PAGXTypes.h" #include "pagx/PathData.h" @@ -912,6 +913,9 @@ struct LayerNode : public PAGXNode { std::vector> filters = {}; std::vector> children = {}; + // Custom data from SVG data-* attributes (key without "data-" prefix) + std::unordered_map customData = {}; + NodeType type() const override { return NodeType::Layer; } @@ -953,6 +957,7 @@ struct LayerNode : public PAGXNode { node->children.push_back( std::unique_ptr(static_cast(child->clone().release()))); } + node->customData = customData; return node; } }; diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 5897953506..3bf2f8ed63 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -381,6 +381,13 @@ std::unique_ptr PAGXXMLParser::parseLayer(const XMLNode* node) { layer->maskType = MaskTypeFromString(getAttribute(node, "maskType", "alpha")); layer->composition = getAttribute(node, "composition"); + // Parse data-* custom attributes. + for (const auto& [key, value] : node->attributes) { + if (key.length() > 5 && key.substr(0, 5) == "data-") { + layer->customData[key.substr(5)] = value; + } + } + for (const auto& child : node->children) { if (child->tag == "contents") { parseContents(child.get(), layer.get()); diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index ac8209a351..831792cc63 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -1206,6 +1206,11 @@ static void writeLayer(XMLBuilder& xml, const LayerNode* node, const ResourceCon } xml.addAttribute("composition", node->composition); + // Write custom data as data-* attributes. + for (const auto& [key, value] : node->customData) { + xml.addAttribute("data-" + key, value); + } + bool hasChildren = !node->contents.empty() || !node->styles.empty() || !node->filters.empty() || !node->children.empty(); if (!hasChildren) { @@ -1387,7 +1392,12 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { xml.addAttribute("height", doc.height); xml.closeElementStart(); - // Write Resources section + // Write Layers first (for better readability) + for (const auto& layer : doc.layers) { + writeLayer(xml, layer.get(), ctx); + } + + // Write Resources section at the end bool hasResources = !ctx.pathDataResources.empty() || !colorSourceByKey.empty() || !doc.resources.empty(); if (hasResources) { @@ -1418,11 +1428,6 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { xml.closeElement(); } - // Write Layers - for (const auto& layer : doc.layers) { - writeLayer(xml, layer.get(), ctx); - } - xml.closeElement(); return xml.str(); diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index c14efca137..cab84e9e77 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -115,6 +115,9 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr _document = PAGXDocument::Make(width, height); + // Collect all IDs from the SVG to avoid conflicts when generating new IDs. + collectAllIds(root); + // First pass: collect defs. auto child = root->getFirstChild(); while (child) { @@ -257,7 +260,9 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrid = getAttribute(element, "id"); - layer->name = getAttribute(element, "id"); + + // Parse data-* custom attributes. + parseCustomData(element, layer.get()); std::string transform = getAttribute(element, "transform"); if (!transform.empty()) { @@ -1456,8 +1461,8 @@ std::string SVGParserImpl::registerImageResource(const std::string& imageSource) return it->second; } - // Generate a new image ID. - std::string imageId = "image" + std::to_string(_nextImageId++); + // Generate a unique image ID that doesn't conflict with existing SVG IDs. + std::string imageId = generateUniqueId("image"); // Create and add the image resource to the document. auto imageNode = std::make_unique(); @@ -1651,4 +1656,47 @@ void SVGParserImpl::convertFilterElement( } } +void SVGParserImpl::collectAllIds(const std::shared_ptr& node) { + if (!node) { + return; + } + + // Collect id from this node. + auto [found, id] = node->findAttribute("id"); + if (found && !id.empty()) { + _existingIds.insert(id); + } + + // Recursively collect from children. + auto child = node->getFirstChild(); + while (child) { + collectAllIds(child); + child = child->getNextSibling(); + } +} + +std::string SVGParserImpl::generateUniqueId(const std::string& prefix) { + std::string id; + do { + id = "_" + prefix + std::to_string(_nextGeneratedId++); + } while (_existingIds.count(id) > 0); + _existingIds.insert(id); + return id; +} + +void SVGParserImpl::parseCustomData(const std::shared_ptr& element, LayerNode* layer) { + if (!element || !layer) { + return; + } + + // Iterate through all attributes and find data-* ones. + for (const auto& attr : element->attributes) { + if (attr.name.length() > 5 && attr.name.substr(0, 5) == "data-") { + // Remove "data-" prefix and store in customData. + std::string key = attr.name.substr(5); + layer->customData[key] = attr.value; + } + } +} + } // namespace pagx diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 1b9df53769..e1edb2af7a 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include "pagx/PAGXDocument.h" #include "pagx/PAGXSVGParser.h" @@ -114,12 +115,23 @@ class SVGParserImpl { // This optimizes the output by combining Fill and Stroke for identical shapes into one Layer. void mergeAdjacentLayers(std::vector>& layers); + // Collect all IDs from the SVG document to avoid conflicts when generating new IDs. + void collectAllIds(const std::shared_ptr& node); + + // Generate a unique ID that doesn't conflict with existing SVG IDs. + std::string generateUniqueId(const std::string& prefix); + + // Parse data-* attributes from element and add to layer's customData. + void parseCustomData(const std::shared_ptr& element, LayerNode* layer); + PAGXSVGParser::Options _options = {}; std::shared_ptr _document = nullptr; std::unordered_map> _defs = {}; std::vector> _maskLayers = {}; std::unordered_map _imageSourceToId = {}; // Maps image source to resource ID. + std::unordered_set _existingIds = {}; // All IDs found in SVG to avoid conflicts. int _nextImageId = 0; + int _nextGeneratedId = 0; // Counter for generating unique IDs. float _viewBoxWidth = 0; float _viewBoxHeight = 0; }; diff --git a/resources/apitest/SVG/customData.svg b/resources/apitest/SVG/customData.svg new file mode 100644 index 0000000000..5119e99c6c --- /dev/null +++ b/resources/apitest/SVG/customData.svg @@ -0,0 +1,15 @@ + + + + + + + + Custom Data Test + + + diff --git a/resources/apitest/SVG/duplicatePaths.svg b/resources/apitest/SVG/duplicatePaths.svg new file mode 100644 index 0000000000..19822f2532 --- /dev/null +++ b/resources/apitest/SVG/duplicatePaths.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + From 953b30a64ce59914c1a512f8604b7923affe757d Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:38:11 +0800 Subject: [PATCH 052/678] Allow parent directory references in relative paths for external resources. --- pagx/docs/pagx_spec.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 86453e4d26..da8405d48f 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -200,10 +200,9 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **路径解析规则**: -- **相对路径**:相对于 PAGX 文件所在目录解析 +- **相对路径**:相对于 PAGX 文件所在目录解析,支持 `../` 引用父目录 - **数据 URI**:以 `data:` 开头,格式为 `data:;base64,` - 路径分隔符统一使用 `/`(正斜杠),不支持 `\`(反斜杠) -- 不支持 `../` 父目录引用,相对路径引用的外部资源必须位于 PAGX 文件所在目录或其子目录内 ### 2.11 Image(图片) From c36173b73886609afab27a21b8605b7816fdfd3b Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:40:02 +0800 Subject: [PATCH 053/678] Clarify that only base64 encoding is supported for data URIs. --- pagx/docs/pagx_spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index da8405d48f..bfc2052479 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -201,7 +201,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **路径解析规则**: - **相对路径**:相对于 PAGX 文件所在目录解析,支持 `../` 引用父目录 -- **数据 URI**:以 `data:` 开头,格式为 `data:;base64,` +- **数据 URI**:以 `data:` 开头,格式为 `data:;base64,`,仅支持 base64 编码 - 路径分隔符统一使用 `/`(正斜杠),不支持 `\`(反斜杠) ### 2.11 Image(图片) From 6055fdb63b130a73c642acdce3f7147769adb59e Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:47:11 +0800 Subject: [PATCH 054/678] Move Resources section after layers in all XML examples. --- pagx/docs/pagx_spec.md | 82 +++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index bfc2052479..1bbf246f04 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -435,9 +435,9 @@ PAGX 使用标准的 2D 笛卡尔坐标系: ```xml - ... ... ... + ... ``` @@ -502,30 +502,30 @@ PAGX 文档采用层级结构组织内容: ``` ← 根元素(定义画布尺寸) -├── ← 资源区(可选,定义可复用资源) -│ ├── ← 图片资源 -│ ├── ← 路径数据资源 -│ ├── ← 纯色定义 -│ ├── ← 渐变定义 -│ ├── ← 图片图案定义 -│ └── ← 合成定义 -│ └── ← 合成内的图层 +├── ← 图层(可多个) +│ ├── ← 矢量内容(VectorElement 系统) +│ │ ├── 几何元素 ← Rectangle、Ellipse、Path、TextSpan 等 +│ │ ├── 修改器 ← TrimPath、RoundCorner、TextModifier 等 +│ │ ├── 绘制器 ← Fill、Stroke +│ │ └── ← 矢量元素容器(可嵌套) +│ │ +│ ├── ← 图层样式 +│ │ └── ← 投影、内阴影等 +│ │ +│ ├── ← 滤镜 +│ │ └── ← 模糊、颜色矩阵等 +│ │ +│ └── ← 子图层(递归结构) +│ └── ... │ -└── ← 图层(可多个) - ├── ← 矢量内容(VectorElement 系统) - │ ├── 几何元素 ← Rectangle、Ellipse、Path、TextSpan 等 - │ ├── 修改器 ← TrimPath、RoundCorner、TextModifier 等 - │ ├── 绘制器 ← Fill、Stroke - │ └── ← 矢量元素容器(可嵌套) - │ - ├── ← 图层样式 - │ └── ← 投影、内阴影等 - │ - ├── ← 滤镜 - │ └── ← 模糊、颜色矩阵等 - │ - └── ← 子图层(递归结构) - └── ... +└── ← 资源区(可选,定义可复用资源) + ├── ← 图片资源 + ├── ← 路径数据资源 + ├── ← 纯色定义 + ├── ← 渐变定义 + ├── ← 图片图案定义 + └── ← 合成定义 + └── ← 合成内的图层 ``` --- @@ -2239,23 +2239,6 @@ contents / Group - - - - - - - - - - - - - - - - @@ -2299,6 +2282,23 @@ contents / Group + + + + + + + + + + + + + + + + ``` From 3d8366d7191f253d77451848204a01597536f391 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:50:11 +0800 Subject: [PATCH 055/678] Fix invalid XML syntax in specification examples by replacing ellipsis with complete code. --- pagx/docs/pagx_spec.md | 62 ++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 1bbf246f04..fcc0366a83 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -84,7 +84,10 @@ PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视 ```xml - ... + + + + ``` @@ -435,9 +438,9 @@ PAGX 使用标准的 2D 笛卡尔坐标系: ```xml - ... - ... - ... + + + ``` @@ -465,7 +468,7 @@ PAGX 使用标准的 2D 笛卡尔坐标系: - ... + ``` @@ -541,8 +544,8 @@ PAGX 文档采用层级结构组织内容: ```xml - - + + @@ -550,7 +553,12 @@ PAGX 文档采用层级结构组织内容: - ... + + + + + + ``` @@ -774,7 +782,10 @@ PAGX 文档采用层级结构组织内容: ```xml - ... + + + + ``` @@ -789,7 +800,12 @@ PAGX 文档采用层级结构组织内容: -... + + + + + + ``` **遮罩类型**:MaskType 定义见 4.1 节。 @@ -1245,9 +1261,9 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: **示例**: ```xml - + - + ``` @@ -1264,8 +1280,8 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: - - + + ``` @@ -1309,8 +1325,8 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: - - + + ``` @@ -1597,9 +1613,9 @@ alpha = lerp(startAlpha, endAlpha, t) ```xml - + - + ``` @@ -1664,10 +1680,10 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: **示例 1 - 基本隔离**: ```xml - + - + ``` @@ -1675,11 +1691,11 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ```xml - + - + @@ -1688,7 +1704,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: **示例 3 - 多个绘制器复用几何**: ```xml - + ``` From d9825d9ee17b6fb66e0a3036e70b7d4f6f96ae1f Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:53:50 +0800 Subject: [PATCH 056/678] Add plusDarker blend mode to PAGX specification and implementation. --- pagx/docs/pagx_spec.md | 3 ++- pagx/include/pagx/PAGXTypes.h | 3 ++- pagx/src/PAGXTypes.cpp | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index fcc0366a83..5b5e7db060 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -620,6 +620,7 @@ PAGX 文档采用层级结构组织内容: | `color` | D 的亮度 + S 的色相和饱和度 | 颜色 | | `luminosity` | S 的亮度 + D 的色相和饱和度 | 亮度 | | `plusLighter` | S + D | 相加(趋向白色) | +| `plusDarker` | S + D - 1 | 相加减一(趋向黑色) | #### 图层渲染流程 @@ -1778,7 +1779,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | 枚举 | 值 | 定义位置 | |------|------|----------| -| **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter` | 4.1 | +| **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter`, `plusDarker` | 4.1 | | **MaskType** | `alpha`, `luminance`, `contour` | 4.1 | | **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | 2.12.8 | | **SamplingMode** | `nearest`, `linear`, `mipmap` | 2.12.8 | diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h index 691dffe7a9..8a015e8084 100644 --- a/pagx/include/pagx/PAGXTypes.h +++ b/pagx/include/pagx/PAGXTypes.h @@ -251,7 +251,8 @@ enum class BlendMode { Saturation, Color, Luminosity, - PlusLighter + PlusLighter, + PlusDarker }; /** diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp index 10edfb69c7..778a074264 100644 --- a/pagx/src/PAGXTypes.cpp +++ b/pagx/src/PAGXTypes.cpp @@ -246,7 +246,8 @@ DEFINE_ENUM_CONVERSION(BlendMode, {BlendMode::Saturation, "saturation"}, {BlendMode::Color, "color"}, {BlendMode::Luminosity, "luminosity"}, - {BlendMode::PlusLighter, "plusLighter"}) + {BlendMode::PlusLighter, "plusLighter"}, + {BlendMode::PlusDarker, "plusDarker"}) DEFINE_ENUM_CONVERSION(LineCap, {LineCap::Butt, "butt"}, From 69f98a6fd3be3239c870d1f30191acdb3d009441 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 12:59:03 +0800 Subject: [PATCH 057/678] Add layer contour concept description to PAGX specification. --- pagx/docs/pagx_spec.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 5b5e7db060..db536713ae 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -635,6 +635,34 @@ PAGX 文档采用层级结构组织内容: **图层样式的参考内容**:图层样式计算时使用的参考内容包含背景内容和前景内容的完整形状。例如,当填充为背景、描边为前景时,描边会绘制在子图层之上,但投影阴影仍然基于包含填充和描边的完整形状计算。 +#### 图层轮廓(Layer Contour) + +**图层轮廓**是图层内容的形状信息,代表图层内容的外形边界。轮廓不包含实际的 RGBA 颜色数据,仅表示形状。图层轮廓主要用于: + +- **图层样式**:投影阴影、内阴影、背景模糊等效果基于轮廓计算 +- **遮罩**:`maskType="contour"` 使用遮罩图层的轮廓进行裁剪 + +**轮廓与可见内容的关系**: + +- **透明度为 0 的绘制器仍参与轮廓**:即使 Fill 或 Stroke 的 `alpha="0"`,其形状仍然会参与图层轮廓的构建。这意味着完全透明的内容虽然不可见,但仍会影响图层样式的效果范围 +- **几何元素必须有绘制器才能参与轮廓**:单独的几何元素(Rectangle、Ellipse 等)如果没有对应的 Fill 或 Stroke,则不会参与轮廓计算 + +**示例**:创建一个不可见但有阴影的图层: + +```xml + + + + + + + + + +``` + +上例中,矩形填充完全透明不可见,但投影阴影仍然会基于矩形的轮廓生成。 + ### 4.2 Layer Styles(图层样式) 图层样式在图层内容渲染完成后应用。 From 962457fc84a06faf4222bf5e8c62cc26455db6da Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 13:07:56 +0800 Subject: [PATCH 058/678] Simplify XML examples by removing default attribute values in PAGX specification. --- pagx/docs/pagx_spec.md | 88 ++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 47 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index db536713ae..9e0c458094 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -244,7 +244,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 线性渐变沿起点到终点的方向插值。 ```xml - + @@ -282,7 +282,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 锥形渐变(也称扫描渐变)沿圆周方向插值。 ```xml - + @@ -351,7 +351,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 ```xml - + @@ -463,7 +463,7 @@ PAGX 使用标准的 2D 笛卡尔坐标系: - + @@ -542,7 +542,7 @@ PAGX 文档采用层级结构组织内容: `` 是内容和子图层的基本容器。 ```xml - + @@ -669,9 +669,9 @@ PAGX 文档采用层级结构组织内容: ```xml - + - + ``` @@ -829,7 +829,7 @@ PAGX 文档采用层级结构组织内容: - + @@ -925,7 +925,7 @@ VectorElement 按**文档顺序**依次处理,文档中靠前的元素先处 矩形从中心点定义,支持统一圆角。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -954,7 +954,7 @@ rect.bottom = center.y + size.height / 2 椭圆从中心点定义。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -978,7 +978,7 @@ boundingRect.bottom = center.y + size.height / 2 支持正多边形和星形两种模式。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1032,10 +1032,10 @@ y = center.y + outerRadius * sin(angle) ```xml - + - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1048,7 +1048,7 @@ y = center.y + outerRadius * sin(angle) 文本片段提供文本内容的几何形状。一个 TextSpan 经过塑形后会产生**字形列表**(多个字形),而非单一 Path。 ```xml - + ``` @@ -1092,7 +1092,7 @@ y = center.y + outerRadius * sin(angle) ```xml - + @@ -1137,10 +1137,10 @@ y = center.y + outerRadius * sin(angle) ```xml - + - + @@ -1215,7 +1215,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 裁剪路径到指定的起止范围。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1353,7 +1353,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: ```xml - + @@ -1364,8 +1364,8 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 对选定范围内的字形应用变换和样式覆盖。 ```xml - - + + ``` @@ -1421,7 +1421,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 范围选择器定义 TextModifier 影响的字形范围和影响程度。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1472,7 +1472,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 将文本沿指定路径排列。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1517,12 +1517,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 第一段内容... 粗体 普通文本。 - + ``` @@ -1568,9 +1563,9 @@ finalColor = blend(originalColor, overrideColor, blendFactor) ```xml - Hello + Hello World - + ``` @@ -1579,7 +1574,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 复制累积的内容和已渲染的样式,对每个副本应用渐进变换。Repeater 对 Path 和字形列表同时生效,且不会触发文本转形状。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1653,7 +1648,7 @@ alpha = lerp(startAlpha, endAlpha, t) Group 是带变换属性的矢量元素容器。 ```xml - + ``` @@ -1735,7 +1730,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ```xml - + ``` #### 多重填充与描边 @@ -2296,7 +2291,7 @@ contents / Group - + @@ -2320,7 +2315,7 @@ contents / Group - + @@ -2328,7 +2323,7 @@ contents / Group - + @@ -2336,8 +2331,7 @@ contents / Group - + @@ -2374,7 +2368,7 @@ contents / Group - + @@ -2391,33 +2385,33 @@ contents / Group - + - + - + ``` #### D.2.4 TextModifier 逐字变换 ```xml - + - + - + - + ``` From 0d414867edd5a624643c15e9476552e406428a2d Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 13:13:47 +0800 Subject: [PATCH 059/678] Update XML examples in node definition sections to show all attributes with default values. --- pagx/docs/pagx_spec.md | 45 ++++++++++++++---------------------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 9e0c458094..a53829b468 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -925,7 +925,7 @@ VectorElement 按**文档顺序**依次处理,文档中靠前的元素先处 矩形从中心点定义,支持统一圆角。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -954,7 +954,7 @@ rect.bottom = center.y + size.height / 2 椭圆从中心点定义。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -978,7 +978,7 @@ boundingRect.bottom = center.y + size.height / 2 支持正多边形和星形两种模式。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1031,11 +1031,7 @@ y = center.y + outerRadius * sin(angle) 使用 SVG 路径语法定义任意形状,支持内联数据或引用 Resources 中定义的 PathData。 ```xml - - - - - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1048,7 +1044,7 @@ y = center.y + outerRadius * sin(angle) 文本片段提供文本内容的几何形状。一个 TextSpan 经过塑形后会产生**字形列表**(多个字形),而非单一 Path。 ```xml - + ``` @@ -1091,8 +1087,7 @@ y = center.y + outerRadius * sin(angle) 填充使用指定的颜色源绘制几何的内部区域。 ```xml - - + @@ -1136,11 +1131,7 @@ y = center.y + outerRadius * sin(angle) 描边沿几何边界绘制线条。 ```xml - - - - - + @@ -1215,7 +1206,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 裁剪路径到指定的起止范围。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1266,7 +1257,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 将所有形状合并为单个形状。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1364,7 +1355,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 对选定范围内的字形应用变换和样式覆盖。 ```xml - + ``` @@ -1421,7 +1412,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 范围选择器定义 TextModifier 影响的字形范围和影响程度。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1472,7 +1463,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 将文本沿指定路径排列。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1513,13 +1504,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 渲染时会由附加的文字排版模块预先排版,重新计算每个字形的位置。转换为 PAG 二进制格式时,TextLayout 会被预排版展开,字形位置直接写入 TextSpan。 ```xml - - 第一段内容... - 粗体 - 普通文本。 - - - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1574,7 +1559,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 复制累积的内容和已渲染的样式,对每个副本应用渐进变换。Repeater 对 Path 和字形列表同时生效,且不会触发文本转形状。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1648,7 +1633,7 @@ alpha = lerp(startAlpha, endAlpha, t) Group 是带变换属性的矢量元素容器。 ```xml - + ``` From 456a6612c2f443d865823f18e9d9dd85ab89b5b0 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 13:20:24 +0800 Subject: [PATCH 060/678] Make LinearGradient startPoint a required attribute and add it back to all XML examples. --- pagx/docs/pagx_spec.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index a53829b468..090ce9b3c6 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -244,7 +244,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 线性渐变沿起点到终点的方向插值。 ```xml - + @@ -252,7 +252,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `startPoint` | point | 0,0 | 起点 | +| `startPoint` | point | (必填) | 起点 | | `endPoint` | point | (必填) | 终点 | | `matrix` | string | 单位矩阵 | 变换矩阵 | @@ -351,7 +351,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 ```xml - + @@ -463,7 +463,7 @@ PAGX 使用标准的 2D 笛卡尔坐标系: - + @@ -1867,7 +1867,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### LinearGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `startPoint` | point | 0,0 | +| `startPoint` | point | (必填) | | `endPoint` | point | (必填) | | `matrix` | string | 单位矩阵 | @@ -2308,7 +2308,7 @@ contents / Group - + From a04371d21935b138a199e717195e7bca32799b0f Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 13:26:16 +0800 Subject: [PATCH 061/678] Add missing required font attribute to TextSpan examples and fix text attribute usage to CDATA. --- pagx/docs/pagx_spec.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 090ce9b3c6..c157bb68ba 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1031,7 +1031,11 @@ y = center.y + outerRadius * sin(angle) 使用 SVG 路径语法定义任意形状,支持内联数据或引用 Resources 中定义的 PathData。 ```xml - + + + + + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1298,8 +1302,8 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: ```xml - - + + @@ -1343,7 +1347,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: **示例**: ```xml - + @@ -1621,7 +1625,7 @@ alpha = lerp(startAlpha, endAlpha, t) ```xml - + From 4d8897cd85e33581652198da2a07a1faf0d10f13 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 13:31:13 +0800 Subject: [PATCH 062/678] Make Path data attribute required and restore complete XML examples with all attributes. --- pagx/docs/pagx_spec.md | 45 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index c157bb68ba..c7aea4912a 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -282,7 +282,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 锥形渐变(也称扫描渐变)沿圆周方向插值。 ```xml - + @@ -542,7 +542,7 @@ PAGX 文档采用层级结构组织内容: `` 是内容和子图层的基本容器。 ```xml - + @@ -669,9 +669,9 @@ PAGX 文档采用层级结构组织内容: ```xml - + - + ``` @@ -829,7 +829,7 @@ PAGX 文档采用层级结构组织内容: - + @@ -925,7 +925,7 @@ VectorElement 按**文档顺序**依次处理,文档中靠前的元素先处 矩形从中心点定义,支持统一圆角。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -954,7 +954,7 @@ rect.bottom = center.y + size.height / 2 椭圆从中心点定义。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -978,7 +978,7 @@ boundingRect.bottom = center.y + size.height / 2 支持正多边形和星形两种模式。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1040,7 +1040,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `data` | string/idref | "" | SVG 路径数据或 PathData 资源引用 "#id"(语法见 2.9 节) | +| `data` | string/idref | (必填) | SVG 路径数据或 PathData 资源引用 "#id"(语法见 2.9 节) | | `reversed` | bool | false | 反转路径方向 | #### 5.2.5 TextSpan(文本片段) @@ -1048,7 +1048,7 @@ y = center.y + outerRadius * sin(angle) 文本片段提供文本内容的几何形状。一个 TextSpan 经过塑形后会产生**字形列表**(多个字形),而非单一 Path。 ```xml - + ``` @@ -2039,7 +2039,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### Path | 属性 | 类型 | 默认值 | |------|------|--------| -| `data` | string/idref | "" | +| `data` | string/idref | (必填) | | `reversed` | bool | false | #### TextSpan @@ -2280,7 +2280,7 @@ contents / Group - + @@ -2304,7 +2304,7 @@ contents / Group - + @@ -2320,7 +2320,8 @@ contents / Group - + @@ -2357,7 +2358,7 @@ contents / Group - + @@ -2374,33 +2375,33 @@ contents / Group - + - + - + ``` #### D.2.4 TextModifier 逐字变换 ```xml - + - + - + - + ``` From 6453f5939c0ec2966af8a9273622a074b8cc9aae Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 13:40:43 +0800 Subject: [PATCH 063/678] Remove invalid name attribute from Group and replace color names with hex values in XML examples. --- pagx/docs/pagx_spec.md | 58 ++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index c7aea4912a..6f62a7a5cd 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1091,7 +1091,8 @@ y = center.y + outerRadius * sin(angle) 填充使用指定的颜色源绘制几何的内部区域。 ```xml - + + @@ -1135,7 +1136,11 @@ y = center.y + outerRadius * sin(angle) 描边沿几何边界绘制线条。 ```xml - + + + + + @@ -1210,7 +1215,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 裁剪路径到指定的起止范围。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1286,10 +1291,10 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: **示例**: ```xml - + - + ``` ### 5.5 Text Modifiers(文本修改器) @@ -1348,7 +1353,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: ```xml - + @@ -1359,8 +1364,8 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 对选定范围内的字形应用变换和样式覆盖。 ```xml - - + + ``` @@ -1508,7 +1513,18 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 渲染时会由附加的文字排版模块预先排版,重新计算每个字形的位置。转换为 PAG 二进制格式时,TextLayout 会被预排版展开,字形位置直接写入 TextSpan。 ```xml - + + 第一段内容... + 粗体 + 普通文本。 + + + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1552,9 +1568,9 @@ finalColor = blend(originalColor, overrideColor, blendFactor) ```xml - Hello + Hello World - + ``` @@ -1563,7 +1579,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 复制累积的内容和已渲染的样式,对每个副本应用渐进变换。Repeater 对 Path 和字形列表同时生效,且不会触发文本转形状。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1637,14 +1653,13 @@ alpha = lerp(startAlpha, endAlpha, t) Group 是带变换属性的矢量元素容器。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `name` | string | "" | 组名称 | | `anchorPoint` | point | 0,0 | 锚点 "x,y" | | `position` | point | 0,0 | 位置 "x,y" | | `rotation` | float | 0 | 旋转角度 | @@ -1694,10 +1709,10 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ```xml - + - + ``` **示例 2 - 子 Group 几何向上累积**: @@ -1705,21 +1720,21 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - + - + - + ``` **示例 3 - 多个绘制器复用几何**: ```xml - - + + ``` #### 多重填充与描边 @@ -2172,7 +2187,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: #### Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `name` | string | "" | | `anchorPoint` | point | 0,0 | | `position` | point | 0,0 | | `rotation` | float | 0 | From 7797d071be227e6332caa3fdaa786003c823463a Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:12:44 +0800 Subject: [PATCH 064/678] Refactor PAGX data structures into modular header files - Split PAGXNode.h into categorized headers under pagx/model/: - Types.h: Point, Size, Rect, Color, Matrix - Node.h: Base Node class and NodeType enum - VectorElement.h, Resource.h, ColorSource.h - LayerStyle.h, LayerFilter.h, Geometry.h, Painter.h - ShapeModifier.h, TextModifier.h, Repeater.h - Group.h, Layer.h, Model.h (master include) - Create individual enum headers in pagx/model/enums/ - Remove "Node" suffix from type names (LayerNode -> Layer, etc.) - Maintain backward compatibility via type aliases in PAGXNode.h --- pagx/include/pagx/PAGXNode.h | 1019 ++--------------- pagx/include/pagx/PAGXTypes.h | 503 +------- pagx/include/pagx/PathData.h | 2 +- pagx/include/pagx/model/ColorSource.h | 161 +++ pagx/include/pagx/model/Enums.h | 55 + pagx/include/pagx/model/Geometry.h | 130 +++ pagx/include/pagx/model/Group.h | 65 ++ pagx/include/pagx/model/Layer.h | 113 ++ pagx/include/pagx/model/LayerFilter.h | 123 ++ pagx/include/pagx/model/LayerStyle.h | 93 ++ pagx/include/pagx/model/Model.h | 63 + pagx/include/pagx/model/Node.h | 113 ++ pagx/include/pagx/model/Painter.h | 107 ++ pagx/include/pagx/model/Repeater.h | 51 + pagx/include/pagx/model/Resource.h | 84 ++ pagx/include/pagx/model/ShapeModifier.h | 76 ++ pagx/include/pagx/model/TextModifier.h | 129 +++ pagx/include/pagx/model/Types.h | 229 ++++ pagx/include/pagx/model/VectorElement.h | 31 + pagx/include/pagx/model/enums/BlendMode.h | 52 + pagx/include/pagx/model/enums/FillRule.h | 36 + pagx/include/pagx/model/enums/FontStyle.h | 36 + pagx/include/pagx/model/enums/LineCap.h | 37 + pagx/include/pagx/model/enums/LineJoin.h | 37 + pagx/include/pagx/model/enums/MaskType.h | 37 + pagx/include/pagx/model/enums/MergePathMode.h | 39 + pagx/include/pagx/model/enums/Overflow.h | 37 + pagx/include/pagx/model/enums/Placement.h | 36 + pagx/include/pagx/model/enums/PolystarType.h | 36 + pagx/include/pagx/model/enums/RepeaterOrder.h | 36 + pagx/include/pagx/model/enums/SamplingMode.h | 37 + pagx/include/pagx/model/enums/SelectorMode.h | 40 + pagx/include/pagx/model/enums/SelectorShape.h | 40 + pagx/include/pagx/model/enums/SelectorUnit.h | 36 + pagx/include/pagx/model/enums/StrokeAlign.h | 37 + pagx/include/pagx/model/enums/TextAlign.h | 38 + pagx/include/pagx/model/enums/TextAnchor.h | 37 + pagx/include/pagx/model/enums/TextPathAlign.h | 37 + pagx/include/pagx/model/enums/TileMode.h | 38 + pagx/include/pagx/model/enums/TrimType.h | 36 + pagx/include/pagx/model/enums/VerticalAlign.h | 37 + pagx/src/PAGXNode.cpp | 2 + 42 files changed, 2528 insertions(+), 1453 deletions(-) create mode 100644 pagx/include/pagx/model/ColorSource.h create mode 100644 pagx/include/pagx/model/Enums.h create mode 100644 pagx/include/pagx/model/Geometry.h create mode 100644 pagx/include/pagx/model/Group.h create mode 100644 pagx/include/pagx/model/Layer.h create mode 100644 pagx/include/pagx/model/LayerFilter.h create mode 100644 pagx/include/pagx/model/LayerStyle.h create mode 100644 pagx/include/pagx/model/Model.h create mode 100644 pagx/include/pagx/model/Node.h create mode 100644 pagx/include/pagx/model/Painter.h create mode 100644 pagx/include/pagx/model/Repeater.h create mode 100644 pagx/include/pagx/model/Resource.h create mode 100644 pagx/include/pagx/model/ShapeModifier.h create mode 100644 pagx/include/pagx/model/TextModifier.h create mode 100644 pagx/include/pagx/model/Types.h create mode 100644 pagx/include/pagx/model/VectorElement.h create mode 100644 pagx/include/pagx/model/enums/BlendMode.h create mode 100644 pagx/include/pagx/model/enums/FillRule.h create mode 100644 pagx/include/pagx/model/enums/FontStyle.h create mode 100644 pagx/include/pagx/model/enums/LineCap.h create mode 100644 pagx/include/pagx/model/enums/LineJoin.h create mode 100644 pagx/include/pagx/model/enums/MaskType.h create mode 100644 pagx/include/pagx/model/enums/MergePathMode.h create mode 100644 pagx/include/pagx/model/enums/Overflow.h create mode 100644 pagx/include/pagx/model/enums/Placement.h create mode 100644 pagx/include/pagx/model/enums/PolystarType.h create mode 100644 pagx/include/pagx/model/enums/RepeaterOrder.h create mode 100644 pagx/include/pagx/model/enums/SamplingMode.h create mode 100644 pagx/include/pagx/model/enums/SelectorMode.h create mode 100644 pagx/include/pagx/model/enums/SelectorShape.h create mode 100644 pagx/include/pagx/model/enums/SelectorUnit.h create mode 100644 pagx/include/pagx/model/enums/StrokeAlign.h create mode 100644 pagx/include/pagx/model/enums/TextAlign.h create mode 100644 pagx/include/pagx/model/enums/TextAnchor.h create mode 100644 pagx/include/pagx/model/enums/TextPathAlign.h create mode 100644 pagx/include/pagx/model/enums/TileMode.h create mode 100644 pagx/include/pagx/model/enums/TrimType.h create mode 100644 pagx/include/pagx/model/enums/VerticalAlign.h diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index add74c75a0..a3cdd6adb6 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -18,961 +18,74 @@ #pragma once -#include -#include -#include -#include -#include "pagx/PAGXTypes.h" -#include "pagx/PathData.h" +// This file provides backward compatibility. +// New code should include pagx/model/Model.h directly. -namespace pagx { - -class PAGXNode; -class ColorSourceNode; -class VectorElementNode; -class LayerStyleNode; -class LayerFilterNode; -struct LayerNode; - -/** - * Node types in PAGX document. - */ -enum class NodeType { - // Color sources - SolidColor, - LinearGradient, - RadialGradient, - ConicGradient, - DiamondGradient, - ImagePattern, - ColorStop, - - // Geometry elements - Rectangle, - Ellipse, - Polystar, - Path, - TextSpan, - - // Painters - Fill, - Stroke, - - // Shape modifiers - TrimPath, - RoundCorner, - MergePath, - - // Text modifiers - TextModifier, - TextPath, - TextLayout, - RangeSelector, - - // Repeater - Repeater, - - // Container - Group, - - // Layer styles - DropShadowStyle, - InnerShadowStyle, - BackgroundBlurStyle, - - // Layer filters - BlurFilter, - DropShadowFilter, - InnerShadowFilter, - BlendFilter, - ColorMatrixFilter, - - // Resources - Image, - PathData, - Composition, - - // Layer - Layer -}; - -/** - * Returns the string name of a node type. - */ -const char* NodeTypeName(NodeType type); - -/** - * Base class for all PAGX nodes. - */ -class PAGXNode { - public: - virtual ~PAGXNode() = default; - - /** - * Returns the type of this node. - */ - virtual NodeType type() const = 0; - - /** - * Returns a deep clone of this node. - */ - virtual std::unique_ptr clone() const = 0; - - protected: - PAGXNode() = default; -}; - -//============================================================================== -// Resource Node (base class - must be defined before ColorSourceNode) -//============================================================================== - -/** - * Base class for resource nodes. - */ -class ResourceNode : public PAGXNode { - public: - std::string id = {}; -}; - -//============================================================================== -// Color Source Nodes -//============================================================================== - -/** - * A color stop in a gradient. - */ -struct ColorStopNode : public PAGXNode { - float offset = 0; - Color color = {}; - - NodeType type() const override { - return NodeType::ColorStop; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Base class for color source nodes. - * Color sources can be stored as resources (with an id) or inline. - */ -class ColorSourceNode : public ResourceNode { -}; - -/** - * A solid color. - */ -struct SolidColorNode : public ColorSourceNode { - Color color = {}; - - NodeType type() const override { - return NodeType::SolidColor; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A linear gradient. - */ -struct LinearGradientNode : public ColorSourceNode { - Point startPoint = {}; - Point endPoint = {}; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::LinearGradient; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A radial gradient. - */ -struct RadialGradientNode : public ColorSourceNode { - Point center = {}; - float radius = 0; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::RadialGradient; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A conic (sweep) gradient. - */ -struct ConicGradientNode : public ColorSourceNode { - Point center = {}; - float startAngle = 0; - float endAngle = 360; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::ConicGradient; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A diamond gradient. - */ -struct DiamondGradientNode : public ColorSourceNode { - Point center = {}; - float halfDiagonal = 0; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::DiamondGradient; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * An image pattern. - */ -struct ImagePatternNode : public ColorSourceNode { - std::string image = {}; - TileMode tileModeX = TileMode::Clamp; - TileMode tileModeY = TileMode::Clamp; - SamplingMode sampling = SamplingMode::Linear; - Matrix matrix = {}; - - NodeType type() const override { - return NodeType::ImagePattern; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -//============================================================================== -// Vector Element Nodes -//============================================================================== - -/** - * Base class for vector element nodes. - */ -class VectorElementNode : public PAGXNode {}; - -/** - * A rectangle shape. - */ -struct RectangleNode : public VectorElementNode { - Point center = {}; - Size size = {100, 100}; - float roundness = 0; - bool reversed = false; - - NodeType type() const override { - return NodeType::Rectangle; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * An ellipse shape. - */ -struct EllipseNode : public VectorElementNode { - Point center = {}; - Size size = {100, 100}; - bool reversed = false; - - NodeType type() const override { - return NodeType::Ellipse; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A polygon or star shape. - */ -struct PolystarNode : public VectorElementNode { - Point center = {}; - PolystarType polystarType = PolystarType::Star; - float pointCount = 5; - float outerRadius = 100; - float innerRadius = 50; - float rotation = 0; - float outerRoundness = 0; - float innerRoundness = 0; - bool reversed = false; - - NodeType type() const override { - return NodeType::Polystar; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A path shape. - */ -struct PathNode : public VectorElementNode { - PathData data = {}; - bool reversed = false; - - NodeType type() const override { - return NodeType::Path; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A text span. - */ -struct TextSpanNode : public VectorElementNode { - float x = 0; - float y = 0; - std::string font = {}; - float fontSize = 12; - int fontWeight = 400; - FontStyle fontStyle = FontStyle::Normal; - float tracking = 0; - float baselineShift = 0; - TextAnchor textAnchor = TextAnchor::Start; - std::string text = {}; - - NodeType type() const override { - return NodeType::TextSpan; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -//============================================================================== -// Painter Nodes -//============================================================================== - -/** - * A fill painter. - * The color can be a simple color string ("#FF0000"), a reference ("#gradientId"), - * or an inline color source node. - */ -struct FillNode : public VectorElementNode { - std::string color = {}; - std::unique_ptr colorSource = nullptr; - float alpha = 1; - BlendMode blendMode = BlendMode::Normal; - FillRule fillRule = FillRule::Winding; - Placement placement = Placement::Background; - - NodeType type() const override { - return NodeType::Fill; - } - - std::unique_ptr clone() const override { - auto node = std::make_unique(); - node->color = color; - if (colorSource) { - node->colorSource.reset(static_cast(colorSource->clone().release())); - } - node->alpha = alpha; - node->blendMode = blendMode; - node->fillRule = fillRule; - node->placement = placement; - return node; - } -}; - -/** - * A stroke painter. - */ -struct StrokeNode : public VectorElementNode { - std::string color = {}; - std::unique_ptr colorSource = nullptr; - float strokeWidth = 1; - float alpha = 1; - BlendMode blendMode = BlendMode::Normal; - LineCap cap = LineCap::Butt; - LineJoin join = LineJoin::Miter; - float miterLimit = 4; - std::vector dashes = {}; - float dashOffset = 0; - StrokeAlign align = StrokeAlign::Center; - Placement placement = Placement::Background; - - NodeType type() const override { - return NodeType::Stroke; - } - - std::unique_ptr clone() const override { - auto node = std::make_unique(); - node->color = color; - if (colorSource) { - node->colorSource.reset(static_cast(colorSource->clone().release())); - } - node->strokeWidth = strokeWidth; - node->alpha = alpha; - node->blendMode = blendMode; - node->cap = cap; - node->join = join; - node->miterLimit = miterLimit; - node->dashes = dashes; - node->dashOffset = dashOffset; - node->align = align; - node->placement = placement; - return node; - } -}; - -//============================================================================== -// Shape Modifier Nodes -//============================================================================== - -/** - * Trim path modifier. - */ -struct TrimPathNode : public VectorElementNode { - float start = 0; - float end = 1; - float offset = 0; - TrimType trimType = TrimType::Separate; - - NodeType type() const override { - return NodeType::TrimPath; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; +#include "pagx/model/Model.h" -/** - * Round corner modifier. - */ -struct RoundCornerNode : public VectorElementNode { - float radius = 10; - - NodeType type() const override { - return NodeType::RoundCorner; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Merge path modifier. - */ -struct MergePathNode : public VectorElementNode { - MergePathMode mode = MergePathMode::Append; - - NodeType type() const override { - return NodeType::MergePath; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -//============================================================================== -// Text Modifier Nodes -//============================================================================== - -/** - * Range selector for text modifier. - */ -struct RangeSelectorNode : public PAGXNode { - float start = 0; - float end = 1; - float offset = 0; - SelectorUnit unit = SelectorUnit::Percentage; - SelectorShape shape = SelectorShape::Square; - float easeIn = 0; - float easeOut = 0; - SelectorMode mode = SelectorMode::Add; - float weight = 1; - bool randomizeOrder = false; - int randomSeed = 0; - - NodeType type() const override { - return NodeType::RangeSelector; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Text modifier. - */ -struct TextModifierNode : public VectorElementNode { - Point anchorPoint = {}; - Point position = {}; - float rotation = 0; - Point scale = {1, 1}; - float skew = 0; - float skewAxis = 0; - float alpha = 1; - std::string fillColor = {}; - std::string strokeColor = {}; - float strokeWidth = -1; - std::vector rangeSelectors = {}; - - NodeType type() const override { - return NodeType::TextModifier; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Text path modifier. - */ -struct TextPathNode : public VectorElementNode { - std::string path = {}; - TextPathAlign textPathAlign = TextPathAlign::Start; - float firstMargin = 0; - float lastMargin = 0; - bool perpendicularToPath = true; - bool reversed = false; - bool forceAlignment = false; - - NodeType type() const override { - return NodeType::TextPath; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Text layout modifier. - */ -struct TextLayoutNode : public VectorElementNode { - float width = 0; - float height = 0; - TextAlign textAlign = TextAlign::Left; - VerticalAlign verticalAlign = VerticalAlign::Top; - float lineHeight = 1.2f; - float indent = 0; - Overflow overflow = Overflow::Clip; - - NodeType type() const override { - return NodeType::TextLayout; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -//============================================================================== -// Repeater Node -//============================================================================== - -/** - * Repeater modifier. - */ -struct RepeaterNode : public VectorElementNode { - float copies = 3; - float offset = 0; - RepeaterOrder order = RepeaterOrder::BelowOriginal; - Point anchorPoint = {}; - Point position = {100, 100}; - float rotation = 0; - Point scale = {1, 1}; - float startAlpha = 1; - float endAlpha = 1; - - NodeType type() const override { - return NodeType::Repeater; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -//============================================================================== -// Group Node -//============================================================================== - -/** - * Group container. - */ -struct GroupNode : public VectorElementNode { - std::string name = {}; - Point anchorPoint = {}; - Point position = {}; - float rotation = 0; - Point scale = {1, 1}; - float skew = 0; - float skewAxis = 0; - float alpha = 1; - std::vector> elements = {}; - - NodeType type() const override { - return NodeType::Group; - } - - std::unique_ptr clone() const override { - auto node = std::make_unique(); - node->name = name; - node->anchorPoint = anchorPoint; - node->position = position; - node->rotation = rotation; - node->scale = scale; - node->skew = skew; - node->skewAxis = skewAxis; - node->alpha = alpha; - for (const auto& element : elements) { - node->elements.push_back( - std::unique_ptr(static_cast(element->clone().release()))); - } - return node; - } -}; - -//============================================================================== -// Layer Style Nodes -//============================================================================== - -/** - * Base class for layer style nodes. - */ -class LayerStyleNode : public PAGXNode { - public: - BlendMode blendMode = BlendMode::Normal; -}; - -/** - * Drop shadow style. - */ -struct DropShadowStyleNode : public LayerStyleNode { - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - bool showBehindLayer = true; - - NodeType type() const override { - return NodeType::DropShadowStyle; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Inner shadow style. - */ -struct InnerShadowStyleNode : public LayerStyleNode { - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - - NodeType type() const override { - return NodeType::InnerShadowStyle; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Background blur style. - */ -struct BackgroundBlurStyleNode : public LayerStyleNode { - float blurrinessX = 0; - float blurrinessY = 0; - TileMode tileMode = TileMode::Mirror; - - NodeType type() const override { - return NodeType::BackgroundBlurStyle; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -//============================================================================== -// Layer Filter Nodes -//============================================================================== - -/** - * Base class for layer filter nodes. - */ -class LayerFilterNode : public PAGXNode {}; - -/** - * Blur filter. - */ -struct BlurFilterNode : public LayerFilterNode { - float blurrinessX = 0; - float blurrinessY = 0; - TileMode tileMode = TileMode::Decal; - - NodeType type() const override { - return NodeType::BlurFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Drop shadow filter. - */ -struct DropShadowFilterNode : public LayerFilterNode { - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - bool shadowOnly = false; - - NodeType type() const override { - return NodeType::DropShadowFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Inner shadow filter. - */ -struct InnerShadowFilterNode : public LayerFilterNode { - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - bool shadowOnly = false; - - NodeType type() const override { - return NodeType::InnerShadowFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Blend filter. - */ -struct BlendFilterNode : public LayerFilterNode { - Color color = {}; - BlendMode filterBlendMode = BlendMode::Normal; - - NodeType type() const override { - return NodeType::BlendFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Color matrix filter. - */ -struct ColorMatrixFilterNode : public LayerFilterNode { - std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; - - NodeType type() const override { - return NodeType::ColorMatrixFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -//============================================================================== -// Other Resource Nodes -//============================================================================== - -/** - * Image resource. - */ -struct ImageNode : public ResourceNode { - std::string source = {}; - - NodeType type() const override { - return NodeType::Image; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * PathData resource - stores reusable path data. - */ -struct PathDataNode : public ResourceNode { - std::string data = {}; // SVG path data string - - NodeType type() const override { - return NodeType::PathData; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Composition resource. - */ -struct CompositionNode : public ResourceNode { - float width = 0; - float height = 0; - std::vector> layers = {}; - - NodeType type() const override { - return NodeType::Composition; - } - - std::unique_ptr clone() const override; -}; - -//============================================================================== -// Layer Node -//============================================================================== - -/** - * Layer node. - */ -struct LayerNode : public PAGXNode { - std::string id = {}; - std::string name = {}; - bool visible = true; - float alpha = 1; - BlendMode blendMode = BlendMode::Normal; - float x = 0; - float y = 0; - Matrix matrix = {}; - std::vector matrix3D = {}; - bool preserve3D = false; - bool antiAlias = true; - bool groupOpacity = false; - bool passThroughBackground = true; - bool excludeChildEffectsInLayerStyle = false; - Rect scrollRect = {}; - bool hasScrollRect = false; - std::string mask = {}; - MaskType maskType = MaskType::Alpha; - std::string composition = {}; - - std::vector> contents = {}; - std::vector> styles = {}; - std::vector> filters = {}; - std::vector> children = {}; - - // Custom data from SVG data-* attributes (key without "data-" prefix) - std::unordered_map customData = {}; - - NodeType type() const override { - return NodeType::Layer; - } - - std::unique_ptr clone() const override { - auto node = std::make_unique(); - node->id = id; - node->name = name; - node->visible = visible; - node->alpha = alpha; - node->blendMode = blendMode; - node->x = x; - node->y = y; - node->matrix = matrix; - node->matrix3D = matrix3D; - node->preserve3D = preserve3D; - node->antiAlias = antiAlias; - node->groupOpacity = groupOpacity; - node->passThroughBackground = passThroughBackground; - node->excludeChildEffectsInLayerStyle = excludeChildEffectsInLayerStyle; - node->scrollRect = scrollRect; - node->hasScrollRect = hasScrollRect; - node->mask = mask; - node->maskType = maskType; - node->composition = composition; - for (const auto& element : contents) { - node->contents.push_back( - std::unique_ptr(static_cast(element->clone().release()))); - } - for (const auto& style : styles) { - node->styles.push_back( - std::unique_ptr(static_cast(style->clone().release()))); - } - for (const auto& filter : filters) { - node->filters.push_back( - std::unique_ptr(static_cast(filter->clone().release()))); - } - for (const auto& child : children) { - node->children.push_back( - std::unique_ptr(static_cast(child->clone().release()))); - } - node->customData = customData; - return node; - } -}; +namespace pagx { -// Implementation of CompositionNode::clone -inline std::unique_ptr CompositionNode::clone() const { - auto node = std::make_unique(); - node->id = id; - node->width = width; - node->height = height; - for (const auto& layer : layers) { - node->layers.push_back( - std::unique_ptr(static_cast(layer->clone().release()))); - } - return node; -} +// Type aliases for backward compatibility (deprecated, use new names) +using PAGXNode = Node; +using ResourceNode = Resource; +using ColorSourceNode = ColorSource; +using VectorElementNode = VectorElement; +using LayerStyleNode = LayerStyle; +using LayerFilterNode = LayerFilter; + +// Color source nodes +using ColorStopNode = ColorStop; +using SolidColorNode = SolidColor; +using LinearGradientNode = LinearGradient; +using RadialGradientNode = RadialGradient; +using ConicGradientNode = ConicGradient; +using DiamondGradientNode = DiamondGradient; +using ImagePatternNode = ImagePattern; + +// Geometry nodes +using RectangleNode = Rectangle; +using EllipseNode = Ellipse; +using PolystarNode = Polystar; +using PathNode = Path; +using TextSpanNode = TextSpan; + +// Painter nodes +using FillNode = Fill; +using StrokeNode = Stroke; + +// Shape modifier nodes +using TrimPathNode = TrimPath; +using RoundCornerNode = RoundCorner; +using MergePathNode = MergePath; + +// Text modifier nodes +using RangeSelectorNode = RangeSelector; +using TextModifierNode = TextModifier; +using TextPathNode = TextPath; +using TextLayoutNode = TextLayout; + +// Other nodes +using RepeaterNode = Repeater; +using GroupNode = Group; + +// Layer style nodes +using DropShadowStyleNode = DropShadowStyle; +using InnerShadowStyleNode = InnerShadowStyle; +using BackgroundBlurStyleNode = BackgroundBlurStyle; + +// Layer filter nodes +using BlurFilterNode = BlurFilter; +using DropShadowFilterNode = DropShadowFilter; +using InnerShadowFilterNode = InnerShadowFilter; +using BlendFilterNode = BlendFilter; +using ColorMatrixFilterNode = ColorMatrixFilter; + +// Resource nodes +using ImageNode = Image; +using PathDataNode = PathDataResource; +using CompositionNode = Composition; + +// Layer +using LayerNode = Layer; } // namespace pagx diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h index 8a015e8084..074c18d024 100644 --- a/pagx/include/pagx/PAGXTypes.h +++ b/pagx/include/pagx/PAGXTypes.h @@ -18,503 +18,8 @@ #pragma once -#include -#include -#include +// This file provides backward compatibility. +// New code should include pagx/model/Types.h and pagx/model/Enums.h directly. -namespace pagx { - -/** - * A point with x and y coordinates. - */ -struct Point { - float x = 0; - float y = 0; - - bool operator==(const Point& other) const { - return x == other.x && y == other.y; - } - - bool operator!=(const Point& other) const { - return !(*this == other); - } -}; - -/** - * A size with width and height. - */ -struct Size { - float width = 0; - float height = 0; - - bool operator==(const Size& other) const { - return width == other.width && height == other.height; - } - - bool operator!=(const Size& other) const { - return !(*this == other); - } -}; - -/** - * A rectangle defined by position and size. - */ -struct Rect { - float x = 0; - float y = 0; - float width = 0; - float height = 0; - - /** - * Returns a Rect from left, top, right, bottom coordinates. - */ - static Rect MakeLTRB(float l, float t, float r, float b) { - return {l, t, r - l, b - t}; - } - - /** - * Returns a Rect from position and size. - */ - static Rect MakeXYWH(float x, float y, float w, float h) { - return {x, y, w, h}; - } - - float left() const { - return x; - } - - float top() const { - return y; - } - - float right() const { - return x + width; - } - - float bottom() const { - return y + height; - } - - bool isEmpty() const { - return width <= 0 || height <= 0; - } - - void setEmpty() { - x = y = width = height = 0; - } - - bool operator==(const Rect& other) const { - return x == other.x && y == other.y && width == other.width && height == other.height; - } - - bool operator!=(const Rect& other) const { - return !(*this == other); - } -}; - -/** - * An RGBA color with floating-point components in [0, 1]. - */ -struct Color { - float red = 0; - float green = 0; - float blue = 0; - float alpha = 1; - - /** - * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). - */ - static Color FromHex(uint32_t hex, bool hasAlpha = false); - - /** - * Returns a Color from RGBA components in [0, 1]. - */ - static Color FromRGBA(float r, float g, float b, float a = 1); - - /** - * Parses a color string. Supports: - * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" - * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" - * Returns black if parsing fails. - */ - static Color Parse(const std::string& str); - - /** - * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". - */ - std::string toHexString(bool includeAlpha = false) const; - - bool operator==(const Color& other) const { - return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; - } - - bool operator!=(const Color& other) const { - return !(*this == other); - } -}; - -/** - * A 2D affine transformation matrix. - * Matrix form: - * | a c tx | - * | b d ty | - * | 0 0 1 | - */ -struct Matrix { - float a = 1; // scaleX - float b = 0; // skewY - float c = 0; // skewX - float d = 1; // scaleY - float tx = 0; // transX - float ty = 0; // transY - - /** - * Returns the identity matrix. - */ - static Matrix Identity() { - return {}; - } - - /** - * Returns a translation matrix. - */ - static Matrix Translate(float x, float y); - - /** - * Returns a scale matrix. - */ - static Matrix Scale(float sx, float sy); - - /** - * Returns a rotation matrix (angle in degrees). - */ - static Matrix Rotate(float degrees); - - /** - * Parses a matrix string "a,b,c,d,tx,ty". - */ - static Matrix Parse(const std::string& str); - - /** - * Returns the matrix as a string "a,b,c,d,tx,ty". - */ - std::string toString() const; - - /** - * Returns true if this is the identity matrix. - */ - bool isIdentity() const { - return a == 1 && b == 0 && c == 0 && d == 1 && tx == 0 && ty == 0; - } - - /** - * Concatenates this matrix with another. - */ - Matrix operator*(const Matrix& other) const; - - /** - * Transforms a point by this matrix. - */ - Point mapPoint(const Point& point) const; - - bool operator==(const Matrix& other) const { - return a == other.a && b == other.b && c == other.c && d == other.d && tx == other.tx && - ty == other.ty; - } - - bool operator!=(const Matrix& other) const { - return !(*this == other); - } -}; - -//============================================================================== -// Enumerations -//============================================================================== - -/** - * Blend modes for compositing. - */ -enum class BlendMode { - Normal, - Multiply, - Screen, - Overlay, - Darken, - Lighten, - ColorDodge, - ColorBurn, - HardLight, - SoftLight, - Difference, - Exclusion, - Hue, - Saturation, - Color, - Luminosity, - PlusLighter, - PlusDarker -}; - -/** - * Line cap styles for strokes. - */ -enum class LineCap { - Butt, - Round, - Square -}; - -/** - * Line join styles for strokes. - */ -enum class LineJoin { - Miter, - Round, - Bevel -}; - -/** - * Fill rules for paths. - */ -enum class FillRule { - Winding, - EvenOdd -}; - -/** - * Stroke alignment relative to path. - */ -enum class StrokeAlign { - Center, - Inside, - Outside -}; - -/** - * Placement of fill/stroke relative to child layers. - */ -enum class Placement { - Background, - Foreground -}; - -/** - * Tile modes for patterns and gradients. - */ -enum class TileMode { - Clamp, - Repeat, - Mirror, - Decal -}; - -/** - * Sampling modes for images. - */ -enum class SamplingMode { - Nearest, - Linear, - Mipmap -}; - -/** - * Mask types for layer masking. - */ -enum class MaskType { - Alpha, - Luminance, - Contour -}; - -/** - * Polystar types. - */ -enum class PolystarType { - Polygon, - Star -}; - -/** - * Trim path types. - */ -enum class TrimType { - Separate, - Continuous -}; - -/** - * Path merge modes (boolean operations). - */ -enum class MergePathMode { - Append, - Union, - Intersect, - Xor, - Difference -}; - -/** - * Text horizontal alignment. - */ -enum class TextAlign { - Left, - Center, - Right, - Justify -}; - -/** - * Text vertical alignment. - */ -enum class VerticalAlign { - Top, - Center, - Bottom -}; - -/** - * Text overflow handling. - */ -enum class Overflow { - Clip, - Visible, - Ellipsis -}; - -/** - * Font style. - */ -enum class FontStyle { - Normal, - Italic -}; - -/** - * Text path alignment. - */ -enum class TextPathAlign { - Start, - Center, - End -}; - -/** - * Range selector unit. - */ -enum class SelectorUnit { - Index, - Percentage -}; - -/** - * Range selector shape. - */ -enum class SelectorShape { - Square, - RampUp, - RampDown, - Triangle, - Round, - Smooth -}; - -/** - * Range selector combination mode. - */ -enum class SelectorMode { - Add, - Subtract, - Intersect, - Min, - Max, - Difference -}; - -/** - * Repeater stacking order. - */ -enum class RepeaterOrder { - BelowOriginal, - AboveOriginal -}; - -/** - * Text anchor for horizontal alignment. - */ -enum class TextAnchor { - Start, - Middle, - End -}; - -//============================================================================== -// Enum string conversion utilities -//============================================================================== - -std::string BlendModeToString(BlendMode mode); -BlendMode BlendModeFromString(const std::string& str); - -std::string LineCapToString(LineCap cap); -LineCap LineCapFromString(const std::string& str); - -std::string LineJoinToString(LineJoin join); -LineJoin LineJoinFromString(const std::string& str); - -std::string FillRuleToString(FillRule rule); -FillRule FillRuleFromString(const std::string& str); - -std::string StrokeAlignToString(StrokeAlign align); -StrokeAlign StrokeAlignFromString(const std::string& str); - -std::string PlacementToString(Placement placement); -Placement PlacementFromString(const std::string& str); - -std::string TileModeToString(TileMode mode); -TileMode TileModeFromString(const std::string& str); - -std::string SamplingModeToString(SamplingMode mode); -SamplingMode SamplingModeFromString(const std::string& str); - -std::string MaskTypeToString(MaskType type); -MaskType MaskTypeFromString(const std::string& str); - -std::string PolystarTypeToString(PolystarType type); -PolystarType PolystarTypeFromString(const std::string& str); - -std::string TrimTypeToString(TrimType type); -TrimType TrimTypeFromString(const std::string& str); - -std::string MergePathModeToString(MergePathMode mode); -MergePathMode MergePathModeFromString(const std::string& str); - -std::string TextAlignToString(TextAlign align); -TextAlign TextAlignFromString(const std::string& str); - -std::string VerticalAlignToString(VerticalAlign align); -VerticalAlign VerticalAlignFromString(const std::string& str); - -std::string OverflowToString(Overflow overflow); -Overflow OverflowFromString(const std::string& str); - -std::string FontStyleToString(FontStyle style); -FontStyle FontStyleFromString(const std::string& str); - -std::string TextPathAlignToString(TextPathAlign align); -TextPathAlign TextPathAlignFromString(const std::string& str); - -std::string SelectorUnitToString(SelectorUnit unit); -SelectorUnit SelectorUnitFromString(const std::string& str); - -std::string SelectorShapeToString(SelectorShape shape); -SelectorShape SelectorShapeFromString(const std::string& str); - -std::string SelectorModeToString(SelectorMode mode); -SelectorMode SelectorModeFromString(const std::string& str); - -std::string RepeaterOrderToString(RepeaterOrder order); -RepeaterOrder RepeaterOrderFromString(const std::string& str); - -std::string TextAnchorToString(TextAnchor anchor); -TextAnchor TextAnchorFromString(const std::string& str); - -} // namespace pagx +#include "pagx/model/Enums.h" +#include "pagx/model/Types.h" diff --git a/pagx/include/pagx/PathData.h b/pagx/include/pagx/PathData.h index d03bb11db7..95dca5e26c 100644 --- a/pagx/include/pagx/PathData.h +++ b/pagx/include/pagx/PathData.h @@ -20,7 +20,7 @@ #include #include -#include "pagx/PAGXTypes.h" +#include "pagx/model/Types.h" namespace pagx { diff --git a/pagx/include/pagx/model/ColorSource.h b/pagx/include/pagx/model/ColorSource.h new file mode 100644 index 0000000000..3c86c329cc --- /dev/null +++ b/pagx/include/pagx/model/ColorSource.h @@ -0,0 +1,161 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/model/Node.h" +#include "pagx/model/Resource.h" +#include "pagx/model/Types.h" +#include "pagx/model/enums/SamplingMode.h" +#include "pagx/model/enums/TileMode.h" + +namespace pagx { + +/** + * A color stop in a gradient. + */ +struct ColorStop : public Node { + float offset = 0; + Color color = {}; + + NodeType type() const override { + return NodeType::ColorStop; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Base class for color source nodes. + * Color sources can be stored as resources (with an id) or inline. + */ +class ColorSource : public Resource {}; + +/** + * A solid color. + */ +struct SolidColor : public ColorSource { + Color color = {}; + + NodeType type() const override { + return NodeType::SolidColor; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * A linear gradient. + */ +struct LinearGradient : public ColorSource { + Point startPoint = {}; + Point endPoint = {}; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::LinearGradient; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * A radial gradient. + */ +struct RadialGradient : public ColorSource { + Point center = {}; + float radius = 0; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::RadialGradient; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * A conic (sweep) gradient. + */ +struct ConicGradient : public ColorSource { + Point center = {}; + float startAngle = 0; + float endAngle = 360; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::ConicGradient; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * A diamond gradient. + */ +struct DiamondGradient : public ColorSource { + Point center = {}; + float halfDiagonal = 0; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::DiamondGradient; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * An image pattern. + */ +struct ImagePattern : public ColorSource { + std::string image = {}; + TileMode tileModeX = TileMode::Clamp; + TileMode tileModeY = TileMode::Clamp; + SamplingMode sampling = SamplingMode::Linear; + Matrix matrix = {}; + + NodeType type() const override { + return NodeType::ImagePattern; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Enums.h b/pagx/include/pagx/model/Enums.h new file mode 100644 index 0000000000..c1b8ce40b5 --- /dev/null +++ b/pagx/include/pagx/model/Enums.h @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// Layer related +#include "pagx/model/enums/BlendMode.h" +#include "pagx/model/enums/MaskType.h" + +// Painter related +#include "pagx/model/enums/FillRule.h" +#include "pagx/model/enums/LineCap.h" +#include "pagx/model/enums/LineJoin.h" +#include "pagx/model/enums/Placement.h" +#include "pagx/model/enums/StrokeAlign.h" + +// Color source related +#include "pagx/model/enums/SamplingMode.h" +#include "pagx/model/enums/TileMode.h" + +// Geometry related +#include "pagx/model/enums/PolystarType.h" +#include "pagx/model/enums/TextAnchor.h" + +// Shape modifier related +#include "pagx/model/enums/MergePathMode.h" +#include "pagx/model/enums/TrimType.h" + +// Text modifier related +#include "pagx/model/enums/FontStyle.h" +#include "pagx/model/enums/Overflow.h" +#include "pagx/model/enums/SelectorMode.h" +#include "pagx/model/enums/SelectorShape.h" +#include "pagx/model/enums/SelectorUnit.h" +#include "pagx/model/enums/TextAlign.h" +#include "pagx/model/enums/TextPathAlign.h" +#include "pagx/model/enums/VerticalAlign.h" + +// Repeater related +#include "pagx/model/enums/RepeaterOrder.h" diff --git a/pagx/include/pagx/model/Geometry.h b/pagx/include/pagx/model/Geometry.h new file mode 100644 index 0000000000..7123d2637b --- /dev/null +++ b/pagx/include/pagx/model/Geometry.h @@ -0,0 +1,130 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/PathData.h" +#include "pagx/model/Types.h" +#include "pagx/model/VectorElement.h" +#include "pagx/model/enums/FontStyle.h" +#include "pagx/model/enums/PolystarType.h" +#include "pagx/model/enums/TextAnchor.h" + +namespace pagx { + +/** + * A rectangle shape. + */ +struct Rectangle : public VectorElement { + Point center = {}; + Size size = {100, 100}; + float roundness = 0; + bool reversed = false; + + NodeType type() const override { + return NodeType::Rectangle; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * An ellipse shape. + */ +struct Ellipse : public VectorElement { + Point center = {}; + Size size = {100, 100}; + bool reversed = false; + + NodeType type() const override { + return NodeType::Ellipse; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * A polygon or star shape. + */ +struct Polystar : public VectorElement { + Point center = {}; + PolystarType polystarType = PolystarType::Star; + float pointCount = 5; + float outerRadius = 100; + float innerRadius = 50; + float rotation = 0; + float outerRoundness = 0; + float innerRoundness = 0; + bool reversed = false; + + NodeType type() const override { + return NodeType::Polystar; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * A path shape. + */ +struct Path : public VectorElement { + PathData data = {}; + bool reversed = false; + + NodeType type() const override { + return NodeType::Path; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * A text span. + */ +struct TextSpan : public VectorElement { + float x = 0; + float y = 0; + std::string font = {}; + float fontSize = 12; + int fontWeight = 400; + FontStyle fontStyle = FontStyle::Normal; + float tracking = 0; + float baselineShift = 0; + TextAnchor textAnchor = TextAnchor::Start; + std::string text = {}; + + NodeType type() const override { + return NodeType::TextSpan; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Group.h b/pagx/include/pagx/model/Group.h new file mode 100644 index 0000000000..e7d491576a --- /dev/null +++ b/pagx/include/pagx/model/Group.h @@ -0,0 +1,65 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/model/Types.h" +#include "pagx/model/VectorElement.h" + +namespace pagx { + +/** + * Group container. + */ +struct Group : public VectorElement { + std::string name = {}; + Point anchorPoint = {}; + Point position = {}; + float rotation = 0; + Point scale = {1, 1}; + float skew = 0; + float skewAxis = 0; + float alpha = 1; + std::vector> elements = {}; + + NodeType type() const override { + return NodeType::Group; + } + + std::unique_ptr clone() const override { + auto node = std::make_unique(); + node->name = name; + node->anchorPoint = anchorPoint; + node->position = position; + node->rotation = rotation; + node->scale = scale; + node->skew = skew; + node->skewAxis = skewAxis; + node->alpha = alpha; + for (const auto& element : elements) { + node->elements.push_back( + std::unique_ptr(static_cast(element->clone().release()))); + } + return node; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Layer.h b/pagx/include/pagx/model/Layer.h new file mode 100644 index 0000000000..f332fee95f --- /dev/null +++ b/pagx/include/pagx/model/Layer.h @@ -0,0 +1,113 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include "pagx/model/LayerFilter.h" +#include "pagx/model/LayerStyle.h" +#include "pagx/model/Node.h" +#include "pagx/model/Types.h" +#include "pagx/model/VectorElement.h" +#include "pagx/model/enums/BlendMode.h" +#include "pagx/model/enums/MaskType.h" + +namespace pagx { + +/** + * Layer node. + */ +struct Layer : public Node { + std::string id = {}; + std::string name = {}; + bool visible = true; + float alpha = 1; + BlendMode blendMode = BlendMode::Normal; + float x = 0; + float y = 0; + Matrix matrix = {}; + std::vector matrix3D = {}; + bool preserve3D = false; + bool antiAlias = true; + bool groupOpacity = false; + bool passThroughBackground = true; + bool excludeChildEffectsInLayerStyle = false; + Rect scrollRect = {}; + bool hasScrollRect = false; + std::string mask = {}; + MaskType maskType = MaskType::Alpha; + std::string composition = {}; + + std::vector> contents = {}; + std::vector> styles = {}; + std::vector> filters = {}; + std::vector> children = {}; + + // Custom data from SVG data-* attributes (key without "data-" prefix) + std::unordered_map customData = {}; + + NodeType type() const override { + return NodeType::Layer; + } + + std::unique_ptr clone() const override { + auto node = std::make_unique(); + node->id = id; + node->name = name; + node->visible = visible; + node->alpha = alpha; + node->blendMode = blendMode; + node->x = x; + node->y = y; + node->matrix = matrix; + node->matrix3D = matrix3D; + node->preserve3D = preserve3D; + node->antiAlias = antiAlias; + node->groupOpacity = groupOpacity; + node->passThroughBackground = passThroughBackground; + node->excludeChildEffectsInLayerStyle = excludeChildEffectsInLayerStyle; + node->scrollRect = scrollRect; + node->hasScrollRect = hasScrollRect; + node->mask = mask; + node->maskType = maskType; + node->composition = composition; + for (const auto& element : contents) { + node->contents.push_back( + std::unique_ptr(static_cast(element->clone().release()))); + } + for (const auto& style : styles) { + node->styles.push_back( + std::unique_ptr(static_cast(style->clone().release()))); + } + for (const auto& filter : filters) { + node->filters.push_back( + std::unique_ptr(static_cast(filter->clone().release()))); + } + for (const auto& child : children) { + node->children.push_back( + std::unique_ptr(static_cast(child->clone().release()))); + } + node->customData = customData; + return node; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/LayerFilter.h b/pagx/include/pagx/model/LayerFilter.h new file mode 100644 index 0000000000..d804094454 --- /dev/null +++ b/pagx/include/pagx/model/LayerFilter.h @@ -0,0 +1,123 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/Node.h" +#include "pagx/model/Types.h" +#include "pagx/model/enums/BlendMode.h" +#include "pagx/model/enums/TileMode.h" + +namespace pagx { + +/** + * Base class for layer filter nodes. + */ +class LayerFilter : public Node {}; + +/** + * Blur filter. + */ +struct BlurFilter : public LayerFilter { + float blurrinessX = 0; + float blurrinessY = 0; + TileMode tileMode = TileMode::Decal; + + NodeType type() const override { + return NodeType::BlurFilter; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Drop shadow filter. + */ +struct DropShadowFilter : public LayerFilter { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool shadowOnly = false; + + NodeType type() const override { + return NodeType::DropShadowFilter; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Inner shadow filter. + */ +struct InnerShadowFilter : public LayerFilter { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool shadowOnly = false; + + NodeType type() const override { + return NodeType::InnerShadowFilter; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Blend filter. + */ +struct BlendFilter : public LayerFilter { + Color color = {}; + BlendMode filterBlendMode = BlendMode::Normal; + + NodeType type() const override { + return NodeType::BlendFilter; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Color matrix filter. + */ +struct ColorMatrixFilter : public LayerFilter { + std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; + + NodeType type() const override { + return NodeType::ColorMatrixFilter; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/LayerStyle.h b/pagx/include/pagx/model/LayerStyle.h new file mode 100644 index 0000000000..f64dbdf272 --- /dev/null +++ b/pagx/include/pagx/model/LayerStyle.h @@ -0,0 +1,93 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/Node.h" +#include "pagx/model/Types.h" +#include "pagx/model/enums/BlendMode.h" +#include "pagx/model/enums/TileMode.h" + +namespace pagx { + +/** + * Base class for layer style nodes. + */ +class LayerStyle : public Node { + public: + BlendMode blendMode = BlendMode::Normal; +}; + +/** + * Drop shadow style. + */ +struct DropShadowStyle : public LayerStyle { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool showBehindLayer = true; + + NodeType type() const override { + return NodeType::DropShadowStyle; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Inner shadow style. + */ +struct InnerShadowStyle : public LayerStyle { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + + NodeType type() const override { + return NodeType::InnerShadowStyle; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Background blur style. + */ +struct BackgroundBlurStyle : public LayerStyle { + float blurrinessX = 0; + float blurrinessY = 0; + TileMode tileMode = TileMode::Mirror; + + NodeType type() const override { + return NodeType::BackgroundBlurStyle; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Model.h b/pagx/include/pagx/model/Model.h new file mode 100644 index 0000000000..b815accbe4 --- /dev/null +++ b/pagx/include/pagx/model/Model.h @@ -0,0 +1,63 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// Basic types and enums +#include "pagx/model/Enums.h" +#include "pagx/model/Types.h" + +// Base classes +#include "pagx/model/Node.h" +#include "pagx/model/VectorElement.h" + +// Color sources +#include "pagx/model/ColorSource.h" + +// Layer styles and filters +#include "pagx/model/LayerFilter.h" +#include "pagx/model/LayerStyle.h" + +// Vector elements +#include "pagx/model/Geometry.h" +#include "pagx/model/Group.h" +#include "pagx/model/Painter.h" +#include "pagx/model/Repeater.h" +#include "pagx/model/ShapeModifier.h" +#include "pagx/model/TextModifier.h" + +// Resources and Layer +#include "pagx/model/Layer.h" +#include "pagx/model/Resource.h" + +namespace pagx { + +// Implementation of Composition::clone (requires Layer to be fully defined) +inline std::unique_ptr Composition::clone() const { + auto node = std::make_unique(); + node->id = id; + node->width = width; + node->height = height; + for (const auto& layer : layers) { + node->layers.push_back( + std::unique_ptr(static_cast(layer->clone().release()))); + } + return node; +} + +} // namespace pagx diff --git a/pagx/include/pagx/model/Node.h b/pagx/include/pagx/model/Node.h new file mode 100644 index 0000000000..ce51db8f04 --- /dev/null +++ b/pagx/include/pagx/model/Node.h @@ -0,0 +1,113 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Node types in PAGX document. + */ +enum class NodeType { + // Color sources + SolidColor, + LinearGradient, + RadialGradient, + ConicGradient, + DiamondGradient, + ImagePattern, + ColorStop, + + // Geometry elements + Rectangle, + Ellipse, + Polystar, + Path, + TextSpan, + + // Painters + Fill, + Stroke, + + // Shape modifiers + TrimPath, + RoundCorner, + MergePath, + + // Text modifiers + TextModifier, + TextPath, + TextLayout, + RangeSelector, + + // Repeater + Repeater, + + // Container + Group, + + // Layer styles + DropShadowStyle, + InnerShadowStyle, + BackgroundBlurStyle, + + // Layer filters + BlurFilter, + DropShadowFilter, + InnerShadowFilter, + BlendFilter, + ColorMatrixFilter, + + // Resources + Image, + PathData, + Composition, + + // Layer + Layer +}; + +/** + * Returns the string name of a node type. + */ +const char* NodeTypeName(NodeType type); + +/** + * Base class for all PAGX nodes. + */ +class Node { + public: + virtual ~Node() = default; + + /** + * Returns the type of this node. + */ + virtual NodeType type() const = 0; + + /** + * Returns a deep clone of this node. + */ + virtual std::unique_ptr clone() const = 0; + + protected: + Node() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Painter.h b/pagx/include/pagx/model/Painter.h new file mode 100644 index 0000000000..08d79d8c9d --- /dev/null +++ b/pagx/include/pagx/model/Painter.h @@ -0,0 +1,107 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/VectorElement.h" +#include "pagx/model/enums/BlendMode.h" +#include "pagx/model/enums/FillRule.h" +#include "pagx/model/enums/LineCap.h" +#include "pagx/model/enums/LineJoin.h" +#include "pagx/model/enums/Placement.h" +#include "pagx/model/enums/StrokeAlign.h" + +namespace pagx { + +/** + * A fill painter. + * The color can be a simple color string ("#FF0000"), a reference ("#gradientId"), + * or an inline color source node. + */ +struct Fill : public VectorElement { + std::string color = {}; + std::unique_ptr colorSource = nullptr; + float alpha = 1; + BlendMode blendMode = BlendMode::Normal; + FillRule fillRule = FillRule::Winding; + Placement placement = Placement::Background; + + NodeType type() const override { + return NodeType::Fill; + } + + std::unique_ptr clone() const override { + auto node = std::make_unique(); + node->color = color; + if (colorSource) { + node->colorSource.reset(static_cast(colorSource->clone().release())); + } + node->alpha = alpha; + node->blendMode = blendMode; + node->fillRule = fillRule; + node->placement = placement; + return node; + } +}; + +/** + * A stroke painter. + */ +struct Stroke : public VectorElement { + std::string color = {}; + std::unique_ptr colorSource = nullptr; + float strokeWidth = 1; + float alpha = 1; + BlendMode blendMode = BlendMode::Normal; + LineCap cap = LineCap::Butt; + LineJoin join = LineJoin::Miter; + float miterLimit = 4; + std::vector dashes = {}; + float dashOffset = 0; + StrokeAlign align = StrokeAlign::Center; + Placement placement = Placement::Background; + + NodeType type() const override { + return NodeType::Stroke; + } + + std::unique_ptr clone() const override { + auto node = std::make_unique(); + node->color = color; + if (colorSource) { + node->colorSource.reset(static_cast(colorSource->clone().release())); + } + node->strokeWidth = strokeWidth; + node->alpha = alpha; + node->blendMode = blendMode; + node->cap = cap; + node->join = join; + node->miterLimit = miterLimit; + node->dashes = dashes; + node->dashOffset = dashOffset; + node->align = align; + node->placement = placement; + return node; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Repeater.h b/pagx/include/pagx/model/Repeater.h new file mode 100644 index 0000000000..144b3951f0 --- /dev/null +++ b/pagx/include/pagx/model/Repeater.h @@ -0,0 +1,51 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/Types.h" +#include "pagx/model/VectorElement.h" +#include "pagx/model/enums/RepeaterOrder.h" + +namespace pagx { + +/** + * Repeater modifier. + */ +struct Repeater : public VectorElement { + float copies = 3; + float offset = 0; + RepeaterOrder order = RepeaterOrder::BelowOriginal; + Point anchorPoint = {}; + Point position = {100, 100}; + float rotation = 0; + Point scale = {1, 1}; + float startAlpha = 1; + float endAlpha = 1; + + NodeType type() const override { + return NodeType::Repeater; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Resource.h b/pagx/include/pagx/model/Resource.h new file mode 100644 index 0000000000..21451c7267 --- /dev/null +++ b/pagx/include/pagx/model/Resource.h @@ -0,0 +1,84 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/model/Node.h" + +namespace pagx { + +struct Layer; + +/** + * Base class for resource nodes. + * Resources are nodes that can be defined in the Resources section and referenced by id. + */ +class Resource : public Node { + public: + std::string id = {}; +}; + +/** + * Image resource. + */ +struct Image : public Resource { + std::string source = {}; + + NodeType type() const override { + return NodeType::Image; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * PathData resource - stores reusable path data. + */ +struct PathDataResource : public Resource { + std::string data = {}; // SVG path data string + + NodeType type() const override { + return NodeType::PathData; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Composition resource. + */ +struct Composition : public Resource { + float width = 0; + float height = 0; + std::vector> layers = {}; + + NodeType type() const override { + return NodeType::Composition; + } + + std::unique_ptr clone() const override; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/ShapeModifier.h b/pagx/include/pagx/model/ShapeModifier.h new file mode 100644 index 0000000000..9325522c42 --- /dev/null +++ b/pagx/include/pagx/model/ShapeModifier.h @@ -0,0 +1,76 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/VectorElement.h" +#include "pagx/model/enums/MergePathMode.h" +#include "pagx/model/enums/TrimType.h" + +namespace pagx { + +/** + * Trim path modifier. + */ +struct TrimPath : public VectorElement { + float start = 0; + float end = 1; + float offset = 0; + TrimType trimType = TrimType::Separate; + + NodeType type() const override { + return NodeType::TrimPath; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Round corner modifier. + */ +struct RoundCorner : public VectorElement { + float radius = 10; + + NodeType type() const override { + return NodeType::RoundCorner; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Merge path modifier. + */ +struct MergePath : public VectorElement { + MergePathMode mode = MergePathMode::Append; + + NodeType type() const override { + return NodeType::MergePath; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/TextModifier.h b/pagx/include/pagx/model/TextModifier.h new file mode 100644 index 0000000000..a04476f38f --- /dev/null +++ b/pagx/include/pagx/model/TextModifier.h @@ -0,0 +1,129 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/model/Node.h" +#include "pagx/model/Types.h" +#include "pagx/model/VectorElement.h" +#include "pagx/model/enums/Overflow.h" +#include "pagx/model/enums/SelectorMode.h" +#include "pagx/model/enums/SelectorShape.h" +#include "pagx/model/enums/SelectorUnit.h" +#include "pagx/model/enums/TextAlign.h" +#include "pagx/model/enums/TextPathAlign.h" +#include "pagx/model/enums/VerticalAlign.h" + +namespace pagx { + +/** + * Range selector for text modifier. + */ +struct RangeSelector : public Node { + float start = 0; + float end = 1; + float offset = 0; + SelectorUnit unit = SelectorUnit::Percentage; + SelectorShape shape = SelectorShape::Square; + float easeIn = 0; + float easeOut = 0; + SelectorMode mode = SelectorMode::Add; + float weight = 1; + bool randomizeOrder = false; + int randomSeed = 0; + + NodeType type() const override { + return NodeType::RangeSelector; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Text modifier. + */ +struct TextModifier : public VectorElement { + Point anchorPoint = {}; + Point position = {}; + float rotation = 0; + Point scale = {1, 1}; + float skew = 0; + float skewAxis = 0; + float alpha = 1; + std::string fillColor = {}; + std::string strokeColor = {}; + float strokeWidth = -1; + std::vector rangeSelectors = {}; + + NodeType type() const override { + return NodeType::TextModifier; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Text path modifier. + */ +struct TextPath : public VectorElement { + std::string path = {}; + TextPathAlign textPathAlign = TextPathAlign::Start; + float firstMargin = 0; + float lastMargin = 0; + bool perpendicularToPath = true; + bool reversed = false; + bool forceAlignment = false; + + NodeType type() const override { + return NodeType::TextPath; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +/** + * Text layout modifier. + */ +struct TextLayout : public VectorElement { + float width = 0; + float height = 0; + TextAlign textAlign = TextAlign::Left; + VerticalAlign verticalAlign = VerticalAlign::Top; + float lineHeight = 1.2f; + float indent = 0; + Overflow overflow = Overflow::Clip; + + NodeType type() const override { + return NodeType::TextLayout; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Types.h b/pagx/include/pagx/model/Types.h new file mode 100644 index 0000000000..3c17baaadf --- /dev/null +++ b/pagx/include/pagx/model/Types.h @@ -0,0 +1,229 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace pagx { + +/** + * A point with x and y coordinates. + */ +struct Point { + float x = 0; + float y = 0; + + bool operator==(const Point& other) const { + return x == other.x && y == other.y; + } + + bool operator!=(const Point& other) const { + return !(*this == other); + } +}; + +/** + * A size with width and height. + */ +struct Size { + float width = 0; + float height = 0; + + bool operator==(const Size& other) const { + return width == other.width && height == other.height; + } + + bool operator!=(const Size& other) const { + return !(*this == other); + } +}; + +/** + * A rectangle defined by position and size. + */ +struct Rect { + float x = 0; + float y = 0; + float width = 0; + float height = 0; + + /** + * Returns a Rect from left, top, right, bottom coordinates. + */ + static Rect MakeLTRB(float l, float t, float r, float b) { + return {l, t, r - l, b - t}; + } + + /** + * Returns a Rect from position and size. + */ + static Rect MakeXYWH(float x, float y, float w, float h) { + return {x, y, w, h}; + } + + float left() const { + return x; + } + + float top() const { + return y; + } + + float right() const { + return x + width; + } + + float bottom() const { + return y + height; + } + + bool isEmpty() const { + return width <= 0 || height <= 0; + } + + void setEmpty() { + x = y = width = height = 0; + } + + bool operator==(const Rect& other) const { + return x == other.x && y == other.y && width == other.width && height == other.height; + } + + bool operator!=(const Rect& other) const { + return !(*this == other); + } +}; + +/** + * An RGBA color with floating-point components in [0, 1]. + */ +struct Color { + float red = 0; + float green = 0; + float blue = 0; + float alpha = 1; + + /** + * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). + */ + static Color FromHex(uint32_t hex, bool hasAlpha = false); + + /** + * Returns a Color from RGBA components in [0, 1]. + */ + static Color FromRGBA(float r, float g, float b, float a = 1); + + /** + * Parses a color string. Supports: + * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" + * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" + * Returns black if parsing fails. + */ + static Color Parse(const std::string& str); + + /** + * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". + */ + std::string toHexString(bool includeAlpha = false) const; + + bool operator==(const Color& other) const { + return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; + } + + bool operator!=(const Color& other) const { + return !(*this == other); + } +}; + +/** + * A 2D affine transformation matrix. + * Matrix form: + * | a c tx | + * | b d ty | + * | 0 0 1 | + */ +struct Matrix { + float a = 1; // scaleX + float b = 0; // skewY + float c = 0; // skewX + float d = 1; // scaleY + float tx = 0; // transX + float ty = 0; // transY + + /** + * Returns the identity matrix. + */ + static Matrix Identity() { + return {}; + } + + /** + * Returns a translation matrix. + */ + static Matrix Translate(float x, float y); + + /** + * Returns a scale matrix. + */ + static Matrix Scale(float sx, float sy); + + /** + * Returns a rotation matrix (angle in degrees). + */ + static Matrix Rotate(float degrees); + + /** + * Parses a matrix string "a,b,c,d,tx,ty". + */ + static Matrix Parse(const std::string& str); + + /** + * Returns the matrix as a string "a,b,c,d,tx,ty". + */ + std::string toString() const; + + /** + * Returns true if this is the identity matrix. + */ + bool isIdentity() const { + return a == 1 && b == 0 && c == 0 && d == 1 && tx == 0 && ty == 0; + } + + /** + * Concatenates this matrix with another. + */ + Matrix operator*(const Matrix& other) const; + + /** + * Transforms a point by this matrix. + */ + Point mapPoint(const Point& point) const; + + bool operator==(const Matrix& other) const { + return a == other.a && b == other.b && c == other.c && d == other.d && tx == other.tx && + ty == other.ty; + } + + bool operator!=(const Matrix& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/VectorElement.h b/pagx/include/pagx/model/VectorElement.h new file mode 100644 index 0000000000..906029ddd2 --- /dev/null +++ b/pagx/include/pagx/model/VectorElement.h @@ -0,0 +1,31 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Node.h" + +namespace pagx { + +/** + * Base class for vector element nodes. + * VectorElements are nodes that can appear inside layer contents. + */ +class VectorElement : public Node {}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/BlendMode.h b/pagx/include/pagx/model/enums/BlendMode.h new file mode 100644 index 0000000000..10b90cbdcb --- /dev/null +++ b/pagx/include/pagx/model/enums/BlendMode.h @@ -0,0 +1,52 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Blend modes for compositing. + */ +enum class BlendMode { + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, + PlusLighter, + PlusDarker +}; + +std::string BlendModeToString(BlendMode mode); +BlendMode BlendModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/FillRule.h b/pagx/include/pagx/model/enums/FillRule.h new file mode 100644 index 0000000000..5ae1093101 --- /dev/null +++ b/pagx/include/pagx/model/enums/FillRule.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Fill rules for paths. + */ +enum class FillRule { + Winding, + EvenOdd +}; + +std::string FillRuleToString(FillRule rule); +FillRule FillRuleFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/FontStyle.h b/pagx/include/pagx/model/enums/FontStyle.h new file mode 100644 index 0000000000..59568b80ff --- /dev/null +++ b/pagx/include/pagx/model/enums/FontStyle.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Font style. + */ +enum class FontStyle { + Normal, + Italic +}; + +std::string FontStyleToString(FontStyle style); +FontStyle FontStyleFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/LineCap.h b/pagx/include/pagx/model/enums/LineCap.h new file mode 100644 index 0000000000..dfb779acca --- /dev/null +++ b/pagx/include/pagx/model/enums/LineCap.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Line cap styles for strokes. + */ +enum class LineCap { + Butt, + Round, + Square +}; + +std::string LineCapToString(LineCap cap); +LineCap LineCapFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/LineJoin.h b/pagx/include/pagx/model/enums/LineJoin.h new file mode 100644 index 0000000000..e6b12c6f57 --- /dev/null +++ b/pagx/include/pagx/model/enums/LineJoin.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Line join styles for strokes. + */ +enum class LineJoin { + Miter, + Round, + Bevel +}; + +std::string LineJoinToString(LineJoin join); +LineJoin LineJoinFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/MaskType.h b/pagx/include/pagx/model/enums/MaskType.h new file mode 100644 index 0000000000..f274bd9a7c --- /dev/null +++ b/pagx/include/pagx/model/enums/MaskType.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Mask types for layer masking. + */ +enum class MaskType { + Alpha, + Luminance, + Contour +}; + +std::string MaskTypeToString(MaskType type); +MaskType MaskTypeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/MergePathMode.h b/pagx/include/pagx/model/enums/MergePathMode.h new file mode 100644 index 0000000000..94a17879ec --- /dev/null +++ b/pagx/include/pagx/model/enums/MergePathMode.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Path merge modes (boolean operations). + */ +enum class MergePathMode { + Append, + Union, + Intersect, + Xor, + Difference +}; + +std::string MergePathModeToString(MergePathMode mode); +MergePathMode MergePathModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/Overflow.h b/pagx/include/pagx/model/enums/Overflow.h new file mode 100644 index 0000000000..4a6868d063 --- /dev/null +++ b/pagx/include/pagx/model/enums/Overflow.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Text overflow handling. + */ +enum class Overflow { + Clip, + Visible, + Ellipsis +}; + +std::string OverflowToString(Overflow overflow); +Overflow OverflowFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/Placement.h b/pagx/include/pagx/model/enums/Placement.h new file mode 100644 index 0000000000..2cb3aaf32b --- /dev/null +++ b/pagx/include/pagx/model/enums/Placement.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Placement of fill/stroke relative to child layers. + */ +enum class Placement { + Background, + Foreground +}; + +std::string PlacementToString(Placement placement); +Placement PlacementFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/PolystarType.h b/pagx/include/pagx/model/enums/PolystarType.h new file mode 100644 index 0000000000..5834dc05c2 --- /dev/null +++ b/pagx/include/pagx/model/enums/PolystarType.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Polystar types. + */ +enum class PolystarType { + Polygon, + Star +}; + +std::string PolystarTypeToString(PolystarType type); +PolystarType PolystarTypeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/RepeaterOrder.h b/pagx/include/pagx/model/enums/RepeaterOrder.h new file mode 100644 index 0000000000..e98b5a3439 --- /dev/null +++ b/pagx/include/pagx/model/enums/RepeaterOrder.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Repeater stacking order. + */ +enum class RepeaterOrder { + BelowOriginal, + AboveOriginal +}; + +std::string RepeaterOrderToString(RepeaterOrder order); +RepeaterOrder RepeaterOrderFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/SamplingMode.h b/pagx/include/pagx/model/enums/SamplingMode.h new file mode 100644 index 0000000000..270a5160e5 --- /dev/null +++ b/pagx/include/pagx/model/enums/SamplingMode.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Sampling modes for images. + */ +enum class SamplingMode { + Nearest, + Linear, + Mipmap +}; + +std::string SamplingModeToString(SamplingMode mode); +SamplingMode SamplingModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/SelectorMode.h b/pagx/include/pagx/model/enums/SelectorMode.h new file mode 100644 index 0000000000..c6794115ee --- /dev/null +++ b/pagx/include/pagx/model/enums/SelectorMode.h @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Range selector combination mode. + */ +enum class SelectorMode { + Add, + Subtract, + Intersect, + Min, + Max, + Difference +}; + +std::string SelectorModeToString(SelectorMode mode); +SelectorMode SelectorModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/SelectorShape.h b/pagx/include/pagx/model/enums/SelectorShape.h new file mode 100644 index 0000000000..e61239f4e8 --- /dev/null +++ b/pagx/include/pagx/model/enums/SelectorShape.h @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Range selector shape. + */ +enum class SelectorShape { + Square, + RampUp, + RampDown, + Triangle, + Round, + Smooth +}; + +std::string SelectorShapeToString(SelectorShape shape); +SelectorShape SelectorShapeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/SelectorUnit.h b/pagx/include/pagx/model/enums/SelectorUnit.h new file mode 100644 index 0000000000..759da0682d --- /dev/null +++ b/pagx/include/pagx/model/enums/SelectorUnit.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Range selector unit. + */ +enum class SelectorUnit { + Index, + Percentage +}; + +std::string SelectorUnitToString(SelectorUnit unit); +SelectorUnit SelectorUnitFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/StrokeAlign.h b/pagx/include/pagx/model/enums/StrokeAlign.h new file mode 100644 index 0000000000..adccb2ed9f --- /dev/null +++ b/pagx/include/pagx/model/enums/StrokeAlign.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Stroke alignment relative to path. + */ +enum class StrokeAlign { + Center, + Inside, + Outside +}; + +std::string StrokeAlignToString(StrokeAlign align); +StrokeAlign StrokeAlignFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/TextAlign.h b/pagx/include/pagx/model/enums/TextAlign.h new file mode 100644 index 0000000000..88ba8c64b2 --- /dev/null +++ b/pagx/include/pagx/model/enums/TextAlign.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Text horizontal alignment. + */ +enum class TextAlign { + Left, + Center, + Right, + Justify +}; + +std::string TextAlignToString(TextAlign align); +TextAlign TextAlignFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/TextAnchor.h b/pagx/include/pagx/model/enums/TextAnchor.h new file mode 100644 index 0000000000..654298aab6 --- /dev/null +++ b/pagx/include/pagx/model/enums/TextAnchor.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Text anchor for horizontal alignment. + */ +enum class TextAnchor { + Start, + Middle, + End +}; + +std::string TextAnchorToString(TextAnchor anchor); +TextAnchor TextAnchorFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/TextPathAlign.h b/pagx/include/pagx/model/enums/TextPathAlign.h new file mode 100644 index 0000000000..7957c8679b --- /dev/null +++ b/pagx/include/pagx/model/enums/TextPathAlign.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Text path alignment. + */ +enum class TextPathAlign { + Start, + Center, + End +}; + +std::string TextPathAlignToString(TextPathAlign align); +TextPathAlign TextPathAlignFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/TileMode.h b/pagx/include/pagx/model/enums/TileMode.h new file mode 100644 index 0000000000..758865336c --- /dev/null +++ b/pagx/include/pagx/model/enums/TileMode.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Tile modes for patterns and gradients. + */ +enum class TileMode { + Clamp, + Repeat, + Mirror, + Decal +}; + +std::string TileModeToString(TileMode mode); +TileMode TileModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/TrimType.h b/pagx/include/pagx/model/enums/TrimType.h new file mode 100644 index 0000000000..31b3aab19f --- /dev/null +++ b/pagx/include/pagx/model/enums/TrimType.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Trim path types. + */ +enum class TrimType { + Separate, + Continuous +}; + +std::string TrimTypeToString(TrimType type); +TrimType TrimTypeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/enums/VerticalAlign.h b/pagx/include/pagx/model/enums/VerticalAlign.h new file mode 100644 index 0000000000..e1db9e2487 --- /dev/null +++ b/pagx/include/pagx/model/enums/VerticalAlign.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Text vertical alignment. + */ +enum class VerticalAlign { + Top, + Center, + Bottom +}; + +std::string VerticalAlignToString(VerticalAlign align); +VerticalAlign VerticalAlignFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/src/PAGXNode.cpp b/pagx/src/PAGXNode.cpp index db582967a6..6ad34b113f 100644 --- a/pagx/src/PAGXNode.cpp +++ b/pagx/src/PAGXNode.cpp @@ -86,6 +86,8 @@ const char* NodeTypeName(NodeType type) { return "ColorMatrixFilter"; case NodeType::Image: return "Image"; + case NodeType::PathData: + return "PathData"; case NodeType::Composition: return "Composition"; case NodeType::Layer: From 4048bb17347055c751d53ef2ca2bcea4f08d63d0 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:18:05 +0800 Subject: [PATCH 065/678] Remove backward compatibility aliases and use new type names directly. --- pagx/include/pagx/PAGXDocument.h | 12 +-- pagx/include/pagx/PAGXNode.h | 70 ------------ pagx/include/pagx/PAGXTypes.h | 3 - pagx/src/PAGXDocument.cpp | 12 +-- pagx/src/PAGXXMLParser.cpp | 168 ++++++++++++++--------------- pagx/src/PAGXXMLParser.h | 86 +++++++-------- pagx/src/PAGXXMLWriter.cpp | 178 +++++++++++++++---------------- pagx/src/svg/PAGXSVGParser.cpp | 140 ++++++++++++------------ pagx/src/svg/SVGParserInternal.h | 44 ++++---- pagx/src/tgfx/LayerBuilder.cpp | 92 ++++++++-------- test/src/PAGXTest.cpp | 67 ++++++------ 11 files changed, 399 insertions(+), 473 deletions(-) diff --git a/pagx/include/pagx/PAGXDocument.h b/pagx/include/pagx/PAGXDocument.h index 4aff3dd7f4..580529cc91 100644 --- a/pagx/include/pagx/PAGXDocument.h +++ b/pagx/include/pagx/PAGXDocument.h @@ -54,12 +54,12 @@ class PAGXDocument { * Resources (images, gradients, compositions, etc.). * These can be referenced by "#id" in the document. */ - std::vector> resources = {}; + std::vector> resources = {}; /** * Top-level layers. */ - std::vector> layers = {}; + std::vector> layers = {}; /** * Base path for resolving relative resource paths. @@ -103,23 +103,23 @@ class PAGXDocument { * Finds a resource by ID. * Returns nullptr if not found. */ - ResourceNode* findResource(const std::string& id) const; + Resource* findResource(const std::string& id) const; /** * Finds a layer by ID (searches recursively). * Returns nullptr if not found. */ - LayerNode* findLayer(const std::string& id) const; + Layer* findLayer(const std::string& id) const; private: friend class PAGXXMLParser; PAGXDocument() = default; - mutable std::unordered_map resourceMap = {}; + mutable std::unordered_map resourceMap = {}; mutable bool resourceMapDirty = true; void rebuildResourceMap() const; - static LayerNode* findLayerRecursive(const std::vector>& layers, + static Layer* findLayerRecursive(const std::vector>& layers, const std::string& id); }; diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index a3cdd6adb6..4ecddd7909 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -18,74 +18,4 @@ #pragma once -// This file provides backward compatibility. -// New code should include pagx/model/Model.h directly. - #include "pagx/model/Model.h" - -namespace pagx { - -// Type aliases for backward compatibility (deprecated, use new names) -using PAGXNode = Node; -using ResourceNode = Resource; -using ColorSourceNode = ColorSource; -using VectorElementNode = VectorElement; -using LayerStyleNode = LayerStyle; -using LayerFilterNode = LayerFilter; - -// Color source nodes -using ColorStopNode = ColorStop; -using SolidColorNode = SolidColor; -using LinearGradientNode = LinearGradient; -using RadialGradientNode = RadialGradient; -using ConicGradientNode = ConicGradient; -using DiamondGradientNode = DiamondGradient; -using ImagePatternNode = ImagePattern; - -// Geometry nodes -using RectangleNode = Rectangle; -using EllipseNode = Ellipse; -using PolystarNode = Polystar; -using PathNode = Path; -using TextSpanNode = TextSpan; - -// Painter nodes -using FillNode = Fill; -using StrokeNode = Stroke; - -// Shape modifier nodes -using TrimPathNode = TrimPath; -using RoundCornerNode = RoundCorner; -using MergePathNode = MergePath; - -// Text modifier nodes -using RangeSelectorNode = RangeSelector; -using TextModifierNode = TextModifier; -using TextPathNode = TextPath; -using TextLayoutNode = TextLayout; - -// Other nodes -using RepeaterNode = Repeater; -using GroupNode = Group; - -// Layer style nodes -using DropShadowStyleNode = DropShadowStyle; -using InnerShadowStyleNode = InnerShadowStyle; -using BackgroundBlurStyleNode = BackgroundBlurStyle; - -// Layer filter nodes -using BlurFilterNode = BlurFilter; -using DropShadowFilterNode = DropShadowFilter; -using InnerShadowFilterNode = InnerShadowFilter; -using BlendFilterNode = BlendFilter; -using ColorMatrixFilterNode = ColorMatrixFilter; - -// Resource nodes -using ImageNode = Image; -using PathDataNode = PathDataResource; -using CompositionNode = Composition; - -// Layer -using LayerNode = Layer; - -} // namespace pagx diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h index 074c18d024..b953a63082 100644 --- a/pagx/include/pagx/PAGXTypes.h +++ b/pagx/include/pagx/PAGXTypes.h @@ -18,8 +18,5 @@ #pragma once -// This file provides backward compatibility. -// New code should include pagx/model/Types.h and pagx/model/Enums.h directly. - #include "pagx/model/Enums.h" #include "pagx/model/Types.h" diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp index a08e4fce6d..fd17531943 100644 --- a/pagx/src/PAGXDocument.cpp +++ b/pagx/src/PAGXDocument.cpp @@ -68,17 +68,17 @@ std::shared_ptr PAGXDocument::clone() const { doc->basePath = basePath; for (const auto& resource : resources) { doc->resources.push_back( - std::unique_ptr(static_cast(resource->clone().release()))); + std::unique_ptr(static_cast(resource->clone().release()))); } for (const auto& layer : layers) { doc->layers.push_back( - std::unique_ptr(static_cast(layer->clone().release()))); + std::unique_ptr(static_cast(layer->clone().release()))); } doc->resourceMapDirty = true; return doc; } -ResourceNode* PAGXDocument::findResource(const std::string& id) const { +Resource* PAGXDocument::findResource(const std::string& id) const { if (resourceMapDirty) { rebuildResourceMap(); } @@ -86,7 +86,7 @@ ResourceNode* PAGXDocument::findResource(const std::string& id) const { return it != resourceMap.end() ? it->second : nullptr; } -LayerNode* PAGXDocument::findLayer(const std::string& id) const { +Layer* PAGXDocument::findLayer(const std::string& id) const { // First search in top-level layers auto found = findLayerRecursive(layers, id); if (found) { @@ -95,7 +95,7 @@ LayerNode* PAGXDocument::findLayer(const std::string& id) const { // Then search in Composition resources for (const auto& resource : resources) { if (resource->type() == NodeType::Composition) { - auto comp = static_cast(resource.get()); + auto comp = static_cast(resource.get()); found = findLayerRecursive(comp->layers, id); if (found) { return found; @@ -115,7 +115,7 @@ void PAGXDocument::rebuildResourceMap() const { resourceMapDirty = false; } -LayerNode* PAGXDocument::findLayerRecursive(const std::vector>& layers, +Layer* PAGXDocument::findLayerRecursive(const std::vector>& layers, const std::string& id) { for (const auto& layer : layers) { if (layer->id == id) { diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 3bf2f8ed63..25c6d04a17 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -300,7 +300,7 @@ void PAGXXMLParser::parseResources(const XMLNode* node, PAGXDocument* doc) { } } -std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { if (node->tag == "Image") { return parseImage(node); } @@ -309,37 +309,37 @@ std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) } if (node->tag == "SolidColor") { auto solidColor = parseSolidColor(node); - auto resource = std::make_unique(); + auto resource = std::make_unique(); *resource = *solidColor; return resource; } if (node->tag == "LinearGradient") { auto gradient = parseLinearGradient(node); - auto resource = std::make_unique(); + auto resource = std::make_unique(); *resource = *gradient; return resource; } if (node->tag == "RadialGradient") { auto gradient = parseRadialGradient(node); - auto resource = std::make_unique(); + auto resource = std::make_unique(); *resource = *gradient; return resource; } if (node->tag == "ConicGradient") { auto gradient = parseConicGradient(node); - auto resource = std::make_unique(); + auto resource = std::make_unique(); *resource = *gradient; return resource; } if (node->tag == "DiamondGradient") { auto gradient = parseDiamondGradient(node); - auto resource = std::make_unique(); + auto resource = std::make_unique(); *resource = *gradient; return resource; } if (node->tag == "ImagePattern") { auto pattern = parseImagePattern(node); - auto resource = std::make_unique(); + auto resource = std::make_unique(); *resource = *pattern; return resource; } @@ -349,8 +349,8 @@ std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayer(const XMLNode* node) { - auto layer = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseLayer(const XMLNode* node) { + auto layer = std::make_unique(); layer->id = getAttribute(node, "id"); layer->name = getAttribute(node, "name"); layer->visible = getBoolAttribute(node, "visible", true); @@ -406,7 +406,7 @@ std::unique_ptr PAGXXMLParser::parseLayer(const XMLNode* node) { return layer; } -void PAGXXMLParser::parseContents(const XMLNode* node, LayerNode* layer) { +void PAGXXMLParser::parseContents(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { auto element = parseVectorElement(child.get()); if (element) { @@ -415,7 +415,7 @@ void PAGXXMLParser::parseContents(const XMLNode* node, LayerNode* layer) { } } -void PAGXXMLParser::parseStyles(const XMLNode* node, LayerNode* layer) { +void PAGXXMLParser::parseStyles(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { auto style = parseLayerStyle(child.get()); if (style) { @@ -424,7 +424,7 @@ void PAGXXMLParser::parseStyles(const XMLNode* node, LayerNode* layer) { } } -void PAGXXMLParser::parseFilters(const XMLNode* node, LayerNode* layer) { +void PAGXXMLParser::parseFilters(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { auto filter = parseLayerFilter(child.get()); if (filter) { @@ -433,7 +433,7 @@ void PAGXXMLParser::parseFilters(const XMLNode* node, LayerNode* layer) { } } -std::unique_ptr PAGXXMLParser::parseVectorElement(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseVectorElement(const XMLNode* node) { if (node->tag == "Rectangle") { return parseRectangle(node); } @@ -482,7 +482,7 @@ std::unique_ptr PAGXXMLParser::parseVectorElement(const XMLNo return nullptr; } -std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node) { if (node->tag == "SolidColor") { return parseSolidColor(node); } @@ -504,7 +504,7 @@ std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) { if (node->tag == "DropShadowStyle") { return parseDropShadowStyle(node); } @@ -517,7 +517,7 @@ std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* no return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* node) { if (node->tag == "BlurFilter") { return parseBlurFilter(node); } @@ -540,8 +540,8 @@ std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* // Geometry element parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseRectangle(const XMLNode* node) { - auto rect = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseRectangle(const XMLNode* node) { + auto rect = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); rect->center = parsePoint(centerStr); auto sizeStr = getAttribute(node, "size", "0,0"); @@ -551,8 +551,8 @@ std::unique_ptr PAGXXMLParser::parseRectangle(const XMLNode* node return rect; } -std::unique_ptr PAGXXMLParser::parseEllipse(const XMLNode* node) { - auto ellipse = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseEllipse(const XMLNode* node) { + auto ellipse = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); ellipse->center = parsePoint(centerStr); auto sizeStr = getAttribute(node, "size", "0,0"); @@ -561,8 +561,8 @@ std::unique_ptr PAGXXMLParser::parseEllipse(const XMLNode* node) { return ellipse; } -std::unique_ptr PAGXXMLParser::parsePolystar(const XMLNode* node) { - auto polystar = std::make_unique(); +std::unique_ptr PAGXXMLParser::parsePolystar(const XMLNode* node) { + auto polystar = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); polystar->center = parsePoint(centerStr); polystar->polystarType = PolystarTypeFromString(getAttribute(node, "polystarType", "star")); @@ -576,8 +576,8 @@ std::unique_ptr PAGXXMLParser::parsePolystar(const XMLNode* node) return polystar; } -std::unique_ptr PAGXXMLParser::parsePath(const XMLNode* node) { - auto path = std::make_unique(); +std::unique_ptr PAGXXMLParser::parsePath(const XMLNode* node) { + auto path = std::make_unique(); auto dataAttr = getAttribute(node, "data"); if (!dataAttr.empty()) { path->data = PathData::FromSVGString(dataAttr); @@ -586,8 +586,8 @@ std::unique_ptr PAGXXMLParser::parsePath(const XMLNode* node) { return path; } -std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) { - auto textSpan = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) { + auto textSpan = std::make_unique(); textSpan->x = getFloatAttribute(node, "x", 0); textSpan->y = getFloatAttribute(node, "y", 0); textSpan->font = getAttribute(node, "font"); @@ -605,8 +605,8 @@ std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) // Painter parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { - auto fill = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { + auto fill = std::make_unique(); fill->color = getAttribute(node, "color"); fill->alpha = getFloatAttribute(node, "alpha", 1); fill->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); @@ -624,8 +624,8 @@ std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { return fill; } -std::unique_ptr PAGXXMLParser::parseStroke(const XMLNode* node) { - auto stroke = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseStroke(const XMLNode* node) { + auto stroke = std::make_unique(); stroke->color = getAttribute(node, "color"); stroke->strokeWidth = getFloatAttribute(node, "width", 1); stroke->alpha = getFloatAttribute(node, "alpha", 1); @@ -656,8 +656,8 @@ std::unique_ptr PAGXXMLParser::parseStroke(const XMLNode* node) { // Modifier parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseTrimPath(const XMLNode* node) { - auto trim = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseTrimPath(const XMLNode* node) { + auto trim = std::make_unique(); trim->start = getFloatAttribute(node, "start", 0); trim->end = getFloatAttribute(node, "end", 1); trim->offset = getFloatAttribute(node, "offset", 0); @@ -665,20 +665,20 @@ std::unique_ptr PAGXXMLParser::parseTrimPath(const XMLNode* node) return trim; } -std::unique_ptr PAGXXMLParser::parseRoundCorner(const XMLNode* node) { - auto round = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseRoundCorner(const XMLNode* node) { + auto round = std::make_unique(); round->radius = getFloatAttribute(node, "radius", 0); return round; } -std::unique_ptr PAGXXMLParser::parseMergePath(const XMLNode* node) { - auto merge = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseMergePath(const XMLNode* node) { + auto merge = std::make_unique(); merge->mode = MergePathModeFromString(getAttribute(node, "mode", "append")); return merge; } -std::unique_ptr PAGXXMLParser::parseTextModifier(const XMLNode* node) { - auto modifier = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseTextModifier(const XMLNode* node) { + auto modifier = std::make_unique(); auto anchorStr = getAttribute(node, "anchorPoint", "0.5,0.5"); modifier->anchorPoint = parsePoint(anchorStr); auto positionStr = getAttribute(node, "position", "0,0"); @@ -702,8 +702,8 @@ std::unique_ptr PAGXXMLParser::parseTextModifier(const XMLNode return modifier; } -std::unique_ptr PAGXXMLParser::parseTextPath(const XMLNode* node) { - auto textPath = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseTextPath(const XMLNode* node) { + auto textPath = std::make_unique(); textPath->path = getAttribute(node, "path"); textPath->textPathAlign = TextPathAlignFromString(getAttribute(node, "align", "start")); textPath->firstMargin = getFloatAttribute(node, "firstMargin", 0); @@ -714,8 +714,8 @@ std::unique_ptr PAGXXMLParser::parseTextPath(const XMLNode* node) return textPath; } -std::unique_ptr PAGXXMLParser::parseTextLayout(const XMLNode* node) { - auto layout = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseTextLayout(const XMLNode* node) { + auto layout = std::make_unique(); layout->width = getFloatAttribute(node, "width", 0); layout->height = getFloatAttribute(node, "height", 0); layout->textAlign = TextAlignFromString(getAttribute(node, "align", "left")); @@ -726,8 +726,8 @@ std::unique_ptr PAGXXMLParser::parseTextLayout(const XMLNode* no return layout; } -std::unique_ptr PAGXXMLParser::parseRepeater(const XMLNode* node) { - auto repeater = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseRepeater(const XMLNode* node) { + auto repeater = std::make_unique(); repeater->copies = getFloatAttribute(node, "copies", 3); repeater->offset = getFloatAttribute(node, "offset", 0); repeater->order = RepeaterOrderFromString(getAttribute(node, "order", "belowOriginal")); @@ -743,8 +743,8 @@ std::unique_ptr PAGXXMLParser::parseRepeater(const XMLNode* node) return repeater; } -std::unique_ptr PAGXXMLParser::parseGroup(const XMLNode* node) { - auto group = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseGroup(const XMLNode* node) { + auto group = std::make_unique(); group->name = getAttribute(node, "name"); auto anchorStr = getAttribute(node, "anchorPoint", "0,0"); group->anchorPoint = parsePoint(anchorStr); @@ -767,8 +767,8 @@ std::unique_ptr PAGXXMLParser::parseGroup(const XMLNode* node) { return group; } -RangeSelectorNode PAGXXMLParser::parseRangeSelector(const XMLNode* node) { - RangeSelectorNode selector = {}; +RangeSelector PAGXXMLParser::parseRangeSelector(const XMLNode* node) { + RangeSelector selector = {}; selector.start = getFloatAttribute(node, "start", 0); selector.end = getFloatAttribute(node, "end", 1); selector.offset = getFloatAttribute(node, "offset", 0); @@ -787,8 +787,8 @@ RangeSelectorNode PAGXXMLParser::parseRangeSelector(const XMLNode* node) { // Color source parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseSolidColor(const XMLNode* node) { - auto solid = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseSolidColor(const XMLNode* node) { + auto solid = std::make_unique(); solid->id = getAttribute(node, "id"); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { @@ -797,8 +797,8 @@ std::unique_ptr PAGXXMLParser::parseSolidColor(const XMLNode* no return solid; } -std::unique_ptr PAGXXMLParser::parseLinearGradient(const XMLNode* node) { - auto gradient = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseLinearGradient(const XMLNode* node) { + auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto startPointStr = getAttribute(node, "startPoint", "0,0"); gradient->startPoint = parsePoint(startPointStr); @@ -816,8 +816,8 @@ std::unique_ptr PAGXXMLParser::parseLinearGradient(const XML return gradient; } -std::unique_ptr PAGXXMLParser::parseRadialGradient(const XMLNode* node) { - auto gradient = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseRadialGradient(const XMLNode* node) { + auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto centerStr = getAttribute(node, "center", "0,0"); gradient->center = parsePoint(centerStr); @@ -834,8 +834,8 @@ std::unique_ptr PAGXXMLParser::parseRadialGradient(const XML return gradient; } -std::unique_ptr PAGXXMLParser::parseConicGradient(const XMLNode* node) { - auto gradient = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseConicGradient(const XMLNode* node) { + auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto centerStr = getAttribute(node, "center", "0,0"); gradient->center = parsePoint(centerStr); @@ -853,8 +853,8 @@ std::unique_ptr PAGXXMLParser::parseConicGradient(const XMLNo return gradient; } -std::unique_ptr PAGXXMLParser::parseDiamondGradient(const XMLNode* node) { - auto gradient = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseDiamondGradient(const XMLNode* node) { + auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto centerStr = getAttribute(node, "center", "0,0"); gradient->center = parsePoint(centerStr); @@ -871,8 +871,8 @@ std::unique_ptr PAGXXMLParser::parseDiamondGradient(const X return gradient; } -std::unique_ptr PAGXXMLParser::parseImagePattern(const XMLNode* node) { - auto pattern = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseImagePattern(const XMLNode* node) { + auto pattern = std::make_unique(); pattern->id = getAttribute(node, "id"); pattern->image = getAttribute(node, "image"); pattern->tileModeX = TileModeFromString(getAttribute(node, "tileModeX", "clamp")); @@ -885,8 +885,8 @@ std::unique_ptr PAGXXMLParser::parseImagePattern(const XMLNode return pattern; } -ColorStopNode PAGXXMLParser::parseColorStop(const XMLNode* node) { - ColorStopNode stop = {}; +ColorStop PAGXXMLParser::parseColorStop(const XMLNode* node) { + ColorStop stop = {}; stop.offset = getFloatAttribute(node, "offset", 0); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { @@ -899,22 +899,22 @@ ColorStopNode PAGXXMLParser::parseColorStop(const XMLNode* node) { // Resource parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseImage(const XMLNode* node) { - auto image = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseImage(const XMLNode* node) { + auto image = std::make_unique(); image->id = getAttribute(node, "id"); image->source = getAttribute(node, "source"); return image; } -std::unique_ptr PAGXXMLParser::parsePathData(const XMLNode* node) { - auto pathData = std::make_unique(); +std::unique_ptr PAGXXMLParser::parsePathData(const XMLNode* node) { + auto pathData = std::make_unique(); pathData->id = getAttribute(node, "id"); pathData->data = getAttribute(node, "data"); return pathData; } -std::unique_ptr PAGXXMLParser::parseComposition(const XMLNode* node) { - auto comp = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseComposition(const XMLNode* node) { + auto comp = std::make_unique(); comp->id = getAttribute(node, "id"); comp->width = getFloatAttribute(node, "width", 0); comp->height = getFloatAttribute(node, "height", 0); @@ -933,8 +933,8 @@ std::unique_ptr PAGXXMLParser::parseComposition(const XMLNode* // Layer style parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseDropShadowStyle(const XMLNode* node) { - auto style = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseDropShadowStyle(const XMLNode* node) { + auto style = std::make_unique(); style->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); style->offsetX = getFloatAttribute(node, "offsetX", 0); style->offsetY = getFloatAttribute(node, "offsetY", 0); @@ -948,8 +948,8 @@ std::unique_ptr PAGXXMLParser::parseDropShadowStyle(const X return style; } -std::unique_ptr PAGXXMLParser::parseInnerShadowStyle(const XMLNode* node) { - auto style = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseInnerShadowStyle(const XMLNode* node) { + auto style = std::make_unique(); style->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); style->offsetX = getFloatAttribute(node, "offsetX", 0); style->offsetY = getFloatAttribute(node, "offsetY", 0); @@ -962,9 +962,9 @@ std::unique_ptr PAGXXMLParser::parseInnerShadowStyle(const return style; } -std::unique_ptr PAGXXMLParser::parseBackgroundBlurStyle( +std::unique_ptr PAGXXMLParser::parseBackgroundBlurStyle( const XMLNode* node) { - auto style = std::make_unique(); + auto style = std::make_unique(); style->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); style->blurrinessX = getFloatAttribute(node, "blurrinessX", 0); style->blurrinessY = getFloatAttribute(node, "blurrinessY", 0); @@ -976,16 +976,16 @@ std::unique_ptr PAGXXMLParser::parseBackgroundBlurStyle // Layer filter parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseBlurFilter(const XMLNode* node) { - auto filter = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseBlurFilter(const XMLNode* node) { + auto filter = std::make_unique(); filter->blurrinessX = getFloatAttribute(node, "blurrinessX", 0); filter->blurrinessY = getFloatAttribute(node, "blurrinessY", 0); filter->tileMode = TileModeFromString(getAttribute(node, "tileMode", "decal")); return filter; } -std::unique_ptr PAGXXMLParser::parseDropShadowFilter(const XMLNode* node) { - auto filter = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseDropShadowFilter(const XMLNode* node) { + auto filter = std::make_unique(); filter->offsetX = getFloatAttribute(node, "offsetX", 0); filter->offsetY = getFloatAttribute(node, "offsetY", 0); filter->blurrinessX = getFloatAttribute(node, "blurrinessX", 0); @@ -998,8 +998,8 @@ std::unique_ptr PAGXXMLParser::parseDropShadowFilter(const return filter; } -std::unique_ptr PAGXXMLParser::parseInnerShadowFilter(const XMLNode* node) { - auto filter = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseInnerShadowFilter(const XMLNode* node) { + auto filter = std::make_unique(); filter->offsetX = getFloatAttribute(node, "offsetX", 0); filter->offsetY = getFloatAttribute(node, "offsetY", 0); filter->blurrinessX = getFloatAttribute(node, "blurrinessX", 0); @@ -1012,8 +1012,8 @@ std::unique_ptr PAGXXMLParser::parseInnerShadowFilter(con return filter; } -std::unique_ptr PAGXXMLParser::parseBlendFilter(const XMLNode* node) { - auto filter = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseBlendFilter(const XMLNode* node) { + auto filter = std::make_unique(); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { filter->color = Color::Parse(colorStr); @@ -1022,8 +1022,8 @@ std::unique_ptr PAGXXMLParser::parseBlendFilter(const XMLNode* return filter; } -std::unique_ptr PAGXXMLParser::parseColorMatrixFilter(const XMLNode* node) { - auto filter = std::make_unique(); +std::unique_ptr PAGXXMLParser::parseColorMatrixFilter(const XMLNode* node) { + auto filter = std::make_unique(); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { auto values = parseFloatList(matrixStr); diff --git a/pagx/src/PAGXXMLParser.h b/pagx/src/PAGXXMLParser.h index cec690247c..d8189c886e 100644 --- a/pagx/src/PAGXXMLParser.h +++ b/pagx/src/PAGXXMLParser.h @@ -51,55 +51,55 @@ class PAGXXMLParser { static void parseDocument(const XMLNode* root, PAGXDocument* doc); static void parseResources(const XMLNode* node, PAGXDocument* doc); - static std::unique_ptr parseResource(const XMLNode* node); - static std::unique_ptr parseLayer(const XMLNode* node); - static void parseContents(const XMLNode* node, LayerNode* layer); - static void parseStyles(const XMLNode* node, LayerNode* layer); - static void parseFilters(const XMLNode* node, LayerNode* layer); + static std::unique_ptr parseResource(const XMLNode* node); + static std::unique_ptr parseLayer(const XMLNode* node); + static void parseContents(const XMLNode* node, Layer* layer); + static void parseStyles(const XMLNode* node, Layer* layer); + static void parseFilters(const XMLNode* node, Layer* layer); - static std::unique_ptr parseVectorElement(const XMLNode* node); - static std::unique_ptr parseColorSource(const XMLNode* node); - static std::unique_ptr parseLayerStyle(const XMLNode* node); - static std::unique_ptr parseLayerFilter(const XMLNode* node); + static std::unique_ptr parseVectorElement(const XMLNode* node); + static std::unique_ptr parseColorSource(const XMLNode* node); + static std::unique_ptr parseLayerStyle(const XMLNode* node); + static std::unique_ptr parseLayerFilter(const XMLNode* node); - static std::unique_ptr parseRectangle(const XMLNode* node); - static std::unique_ptr parseEllipse(const XMLNode* node); - static std::unique_ptr parsePolystar(const XMLNode* node); - static std::unique_ptr parsePath(const XMLNode* node); - static std::unique_ptr parseTextSpan(const XMLNode* node); - static std::unique_ptr parseFill(const XMLNode* node); - static std::unique_ptr parseStroke(const XMLNode* node); - static std::unique_ptr parseTrimPath(const XMLNode* node); - static std::unique_ptr parseRoundCorner(const XMLNode* node); - static std::unique_ptr parseMergePath(const XMLNode* node); - static std::unique_ptr parseTextModifier(const XMLNode* node); - static std::unique_ptr parseTextPath(const XMLNode* node); - static std::unique_ptr parseTextLayout(const XMLNode* node); - static std::unique_ptr parseRepeater(const XMLNode* node); - static std::unique_ptr parseGroup(const XMLNode* node); - static RangeSelectorNode parseRangeSelector(const XMLNode* node); + static std::unique_ptr parseRectangle(const XMLNode* node); + static std::unique_ptr parseEllipse(const XMLNode* node); + static std::unique_ptr parsePolystar(const XMLNode* node); + static std::unique_ptr parsePath(const XMLNode* node); + static std::unique_ptr parseTextSpan(const XMLNode* node); + static std::unique_ptr parseFill(const XMLNode* node); + static std::unique_ptr parseStroke(const XMLNode* node); + static std::unique_ptr parseTrimPath(const XMLNode* node); + static std::unique_ptr parseRoundCorner(const XMLNode* node); + static std::unique_ptr parseMergePath(const XMLNode* node); + static std::unique_ptr parseTextModifier(const XMLNode* node); + static std::unique_ptr parseTextPath(const XMLNode* node); + static std::unique_ptr parseTextLayout(const XMLNode* node); + static std::unique_ptr parseRepeater(const XMLNode* node); + static std::unique_ptr parseGroup(const XMLNode* node); + static RangeSelector parseRangeSelector(const XMLNode* node); - static std::unique_ptr parseSolidColor(const XMLNode* node); - static std::unique_ptr parseLinearGradient(const XMLNode* node); - static std::unique_ptr parseRadialGradient(const XMLNode* node); - static std::unique_ptr parseConicGradient(const XMLNode* node); - static std::unique_ptr parseDiamondGradient(const XMLNode* node); - static std::unique_ptr parseImagePattern(const XMLNode* node); - static ColorStopNode parseColorStop(const XMLNode* node); + static std::unique_ptr parseSolidColor(const XMLNode* node); + static std::unique_ptr parseLinearGradient(const XMLNode* node); + static std::unique_ptr parseRadialGradient(const XMLNode* node); + static std::unique_ptr parseConicGradient(const XMLNode* node); + static std::unique_ptr parseDiamondGradient(const XMLNode* node); + static std::unique_ptr parseImagePattern(const XMLNode* node); + static ColorStop parseColorStop(const XMLNode* node); - static std::unique_ptr parseImage(const XMLNode* node); - static std::unique_ptr parsePathData(const XMLNode* node); - static std::unique_ptr parseComposition(const XMLNode* node); + static std::unique_ptr parseImage(const XMLNode* node); + static std::unique_ptr parsePathData(const XMLNode* node); + static std::unique_ptr parseComposition(const XMLNode* node); - static std::unique_ptr parseDropShadowStyle(const XMLNode* node); - static std::unique_ptr parseInnerShadowStyle(const XMLNode* node); - static std::unique_ptr parseBackgroundBlurStyle(const XMLNode* node); + static std::unique_ptr parseDropShadowStyle(const XMLNode* node); + static std::unique_ptr parseInnerShadowStyle(const XMLNode* node); + static std::unique_ptr parseBackgroundBlurStyle(const XMLNode* node); - static std::unique_ptr parseBlurFilter(const XMLNode* node); - static std::unique_ptr parseDropShadowFilter(const XMLNode* node); - static std::unique_ptr parseInnerShadowFilter(const XMLNode* node); - static std::unique_ptr parseBlendFilter(const XMLNode* node); - static std::unique_ptr parseColorMatrixFilter(const XMLNode* node); + static std::unique_ptr parseBlurFilter(const XMLNode* node); + static std::unique_ptr parseDropShadowFilter(const XMLNode* node); + static std::unique_ptr parseInnerShadowFilter(const XMLNode* node); + static std::unique_ptr parseBlendFilter(const XMLNode* node); + static std::unique_ptr parseColorMatrixFilter(const XMLNode* node); static std::string getAttribute(const XMLNode* node, const std::string& name, const std::string& defaultValue = ""); diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index 831792cc63..5cb430b88c 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -187,19 +187,19 @@ static std::string floatListToString(const std::vector& values) { // ColorSource serialization helper //============================================================================== -static std::string colorSourceToKey(const ColorSourceNode* node) { +static std::string colorSourceToKey(const ColorSource* node) { if (!node) { return ""; } std::ostringstream oss = {}; switch (node->type()) { case NodeType::SolidColor: { - auto solid = static_cast(node); + auto solid = static_cast(node); oss << "SolidColor:" << solid->color.toHexString(true); break; } case NodeType::LinearGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); oss << "LinearGradient:" << grad->startPoint.x << "," << grad->startPoint.y << ":" << grad->endPoint.x << "," << grad->endPoint.y << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { @@ -208,7 +208,7 @@ static std::string colorSourceToKey(const ColorSourceNode* node) { break; } case NodeType::RadialGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); oss << "RadialGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->radius << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { @@ -217,7 +217,7 @@ static std::string colorSourceToKey(const ColorSourceNode* node) { break; } case NodeType::ConicGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); oss << "ConicGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->startAngle << ":" << grad->endAngle << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { @@ -226,7 +226,7 @@ static std::string colorSourceToKey(const ColorSourceNode* node) { break; } case NodeType::DiamondGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); oss << "DiamondGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->halfDiagonal << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { @@ -235,7 +235,7 @@ static std::string colorSourceToKey(const ColorSourceNode* node) { break; } case NodeType::ImagePattern: { - auto pattern = static_cast(node); + auto pattern = static_cast(node); oss << "ImagePattern:" << pattern->image << ":" << static_cast(pattern->tileModeX) << ":" << static_cast(pattern->tileModeY) << ":" << static_cast(pattern->sampling) << ":" << pattern->matrix.toString(); @@ -263,7 +263,7 @@ class ResourceContext { std::vector> pathDataResources = {}; // id -> svg string // All extracted ColorSource resources (ordered) - std::vector> colorSourceResources = {}; + std::vector> colorSourceResources = {}; int nextPathId = 1; int nextColorId = 1; @@ -275,7 +275,7 @@ class ResourceContext { } for (const auto& resource : doc.resources) { if (resource->type() == NodeType::Composition) { - auto comp = static_cast(resource.get()); + auto comp = static_cast(resource.get()); for (const auto& layer : comp->layers) { collectFromLayer(layer.get()); } @@ -296,7 +296,7 @@ class ResourceContext { } // Register ColorSource usage (for counting) - void registerColorSource(const ColorSourceNode* node) { + void registerColorSource(const ColorSource* node) { if (!node) { return; } @@ -322,7 +322,7 @@ class ResourceContext { } // Check if ColorSource should be extracted to Resources - bool shouldExtractColorSource(const ColorSourceNode* node) const { + bool shouldExtractColorSource(const ColorSource* node) const { if (!node) { return false; } @@ -332,7 +332,7 @@ class ResourceContext { } // Get ColorSource resource id (empty if should inline) - std::string getColorSourceId(const ColorSourceNode* node) const { + std::string getColorSourceId(const ColorSource* node) const { if (!node) { return ""; } @@ -345,7 +345,7 @@ class ResourceContext { } private: - void collectFromLayer(const LayerNode* layer) { + void collectFromLayer(const Layer* layer) { for (const auto& element : layer->contents) { collectFromVectorElement(element.get()); } @@ -354,31 +354,31 @@ class ResourceContext { } } - void collectFromVectorElement(const VectorElementNode* element) { + void collectFromVectorElement(const VectorElement* element) { switch (element->type()) { case NodeType::Path: { - auto path = static_cast(element); + auto path = static_cast(element); if (!path->data.isEmpty()) { getPathDataId(path->data.toSVGString()); } break; } case NodeType::Fill: { - auto fill = static_cast(element); + auto fill = static_cast(element); if (fill->colorSource) { registerColorSource(fill->colorSource.get()); } break; } case NodeType::Stroke: { - auto stroke = static_cast(element); + auto stroke = static_cast(element); if (stroke->colorSource) { registerColorSource(stroke->colorSource.get()); } break; } case NodeType::Group: { - auto group = static_cast(element); + auto group = static_cast(element); for (const auto& child : group->elements) { collectFromVectorElement(child.get()); } @@ -394,19 +394,19 @@ class ResourceContext { // Forward declarations //============================================================================== -static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool writeId); -static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, +static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId); +static void writeVectorElement(XMLBuilder& xml, const VectorElement* node, const ResourceContext& ctx); -static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node); -static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node); -static void writeResource(XMLBuilder& xml, const ResourceNode* node, const ResourceContext& ctx); -static void writeLayer(XMLBuilder& xml, const LayerNode* node, const ResourceContext& ctx); +static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node); +static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node); +static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceContext& ctx); +static void writeLayer(XMLBuilder& xml, const Layer* node, const ResourceContext& ctx); //============================================================================== // ColorStop and ColorSource writing //============================================================================== -static void writeColorStops(XMLBuilder& xml, const std::vector& stops) { +static void writeColorStops(XMLBuilder& xml, const std::vector& stops) { for (const auto& stop : stops) { xml.openElement("ColorStop"); xml.addRequiredAttribute("offset", stop.offset); @@ -415,10 +415,10 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& s } } -static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool writeId) { +static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId) { switch (node->type()) { case NodeType::SolidColor: { - auto solid = static_cast(node); + auto solid = static_cast(node); xml.openElement("SolidColor"); if (writeId && !solid->id.empty()) { xml.addAttribute("id", solid->id); @@ -428,7 +428,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool break; } case NodeType::LinearGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); xml.openElement("LinearGradient"); if (writeId && !grad->id.empty()) { xml.addAttribute("id", grad->id); @@ -450,7 +450,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool break; } case NodeType::RadialGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); xml.openElement("RadialGradient"); if (writeId && !grad->id.empty()) { xml.addAttribute("id", grad->id); @@ -472,7 +472,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool break; } case NodeType::ConicGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); xml.openElement("ConicGradient"); if (writeId && !grad->id.empty()) { xml.addAttribute("id", grad->id); @@ -495,7 +495,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool break; } case NodeType::DiamondGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); xml.openElement("DiamondGradient"); if (writeId && !grad->id.empty()) { xml.addAttribute("id", grad->id); @@ -517,7 +517,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool break; } case NodeType::ImagePattern: { - auto pattern = static_cast(node); + auto pattern = static_cast(node); xml.openElement("ImagePattern"); if (writeId && !pattern->id.empty()) { xml.addAttribute("id", pattern->id); @@ -544,11 +544,11 @@ static void writeColorSource(XMLBuilder& xml, const ColorSourceNode* node, bool } // Write ColorSource with assigned id (for Resources section) -static void writeColorSourceWithId(XMLBuilder& xml, const ColorSourceNode* node, +static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, const std::string& id) { switch (node->type()) { case NodeType::SolidColor: { - auto solid = static_cast(node); + auto solid = static_cast(node); xml.openElement("SolidColor"); xml.addAttribute("id", id); xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); @@ -556,7 +556,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSourceNode* node, break; } case NodeType::LinearGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); xml.openElement("LinearGradient"); xml.addAttribute("id", id); if (grad->startPoint.x != 0 || grad->startPoint.y != 0) { @@ -576,7 +576,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSourceNode* node, break; } case NodeType::RadialGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); xml.openElement("RadialGradient"); xml.addAttribute("id", id); if (grad->center.x != 0 || grad->center.y != 0) { @@ -596,7 +596,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSourceNode* node, break; } case NodeType::ConicGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); xml.openElement("ConicGradient"); xml.addAttribute("id", id); if (grad->center.x != 0 || grad->center.y != 0) { @@ -617,7 +617,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSourceNode* node, break; } case NodeType::DiamondGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); xml.openElement("DiamondGradient"); xml.addAttribute("id", id); if (grad->center.x != 0 || grad->center.y != 0) { @@ -637,7 +637,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSourceNode* node, break; } case NodeType::ImagePattern: { - auto pattern = static_cast(node); + auto pattern = static_cast(node); xml.openElement("ImagePattern"); xml.addAttribute("id", id); xml.addAttribute("image", pattern->image); @@ -665,11 +665,11 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSourceNode* node, // VectorElement writing //============================================================================== -static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, +static void writeVectorElement(XMLBuilder& xml, const VectorElement* node, const ResourceContext& ctx) { switch (node->type()) { case NodeType::Rectangle: { - auto rect = static_cast(node); + auto rect = static_cast(node); xml.openElement("Rectangle"); if (rect->center.x != 0 || rect->center.y != 0) { xml.addAttribute("center", pointToString(rect->center)); @@ -683,7 +683,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::Ellipse: { - auto ellipse = static_cast(node); + auto ellipse = static_cast(node); xml.openElement("Ellipse"); if (ellipse->center.x != 0 || ellipse->center.y != 0) { xml.addAttribute("center", pointToString(ellipse->center)); @@ -696,7 +696,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::Polystar: { - auto polystar = static_cast(node); + auto polystar = static_cast(node); xml.openElement("Polystar"); if (polystar->center.x != 0 || polystar->center.y != 0) { xml.addAttribute("center", pointToString(polystar->center)); @@ -713,7 +713,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::Path: { - auto path = static_cast(node); + auto path = static_cast(node); xml.openElement("Path"); if (!path->data.isEmpty()) { // Always reference PathData from Resources @@ -731,7 +731,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::TextSpan: { - auto text = static_cast(node); + auto text = static_cast(node); xml.openElement("TextSpan"); xml.addAttribute("x", text->x); xml.addAttribute("y", text->y); @@ -752,7 +752,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::Fill: { - auto fill = static_cast(node); + auto fill = static_cast(node); xml.openElement("Fill"); // Check if ColorSource should be referenced or inlined if (fill->colorSource) { @@ -786,7 +786,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::Stroke: { - auto stroke = static_cast(node); + auto stroke = static_cast(node); xml.openElement("Stroke"); // Check if ColorSource should be referenced or inlined if (stroke->colorSource) { @@ -832,7 +832,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::TrimPath: { - auto trim = static_cast(node); + auto trim = static_cast(node); xml.openElement("TrimPath"); xml.addAttribute("start", trim->start); xml.addAttribute("end", trim->end, 1.0f); @@ -844,14 +844,14 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::RoundCorner: { - auto round = static_cast(node); + auto round = static_cast(node); xml.openElement("RoundCorner"); xml.addAttribute("radius", round->radius, 10.0f); xml.closeElementSelfClosing(); break; } case NodeType::MergePath: { - auto merge = static_cast(node); + auto merge = static_cast(node); xml.openElement("MergePath"); if (merge->mode != MergePathMode::Append) { xml.addAttribute("mode", MergePathModeToString(merge->mode)); @@ -860,7 +860,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::TextModifier: { - auto modifier = static_cast(node); + auto modifier = static_cast(node); xml.openElement("TextModifier"); if (modifier->anchorPoint.x != 0 || modifier->anchorPoint.y != 0) { xml.addAttribute("anchorPoint", pointToString(modifier->anchorPoint)); @@ -910,7 +910,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::TextPath: { - auto textPath = static_cast(node); + auto textPath = static_cast(node); xml.openElement("TextPath"); xml.addAttribute("path", textPath->path); if (textPath->textPathAlign != TextPathAlign::Start) { @@ -925,7 +925,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::TextLayout: { - auto layout = static_cast(node); + auto layout = static_cast(node); xml.openElement("TextLayout"); xml.addRequiredAttribute("width", layout->width); xml.addRequiredAttribute("height", layout->height); @@ -944,7 +944,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::Repeater: { - auto repeater = static_cast(node); + auto repeater = static_cast(node); xml.openElement("Repeater"); xml.addAttribute("copies", repeater->copies, 3.0f); xml.addAttribute("offset", repeater->offset); @@ -967,7 +967,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, break; } case NodeType::Group: { - auto group = static_cast(node); + auto group = static_cast(node); xml.openElement("Group"); xml.addAttribute("name", group->name); if (group->anchorPoint.x != 0 || group->anchorPoint.y != 0) { @@ -1003,10 +1003,10 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElementNode* node, // LayerStyle writing //============================================================================== -static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node) { +static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { switch (node->type()) { case NodeType::DropShadowStyle: { - auto style = static_cast(node); + auto style = static_cast(node); xml.openElement("DropShadowStyle"); if (style->blendMode != BlendMode::Normal) { xml.addAttribute("blendMode", BlendModeToString(style->blendMode)); @@ -1021,7 +1021,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node) { break; } case NodeType::InnerShadowStyle: { - auto style = static_cast(node); + auto style = static_cast(node); xml.openElement("InnerShadowStyle"); if (style->blendMode != BlendMode::Normal) { xml.addAttribute("blendMode", BlendModeToString(style->blendMode)); @@ -1035,7 +1035,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node) { break; } case NodeType::BackgroundBlurStyle: { - auto style = static_cast(node); + auto style = static_cast(node); xml.openElement("BackgroundBlurStyle"); if (style->blendMode != BlendMode::Normal) { xml.addAttribute("blendMode", BlendModeToString(style->blendMode)); @@ -1057,10 +1057,10 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyleNode* node) { // LayerFilter writing //============================================================================== -static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node) { +static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { switch (node->type()) { case NodeType::BlurFilter: { - auto filter = static_cast(node); + auto filter = static_cast(node); xml.openElement("BlurFilter"); xml.addRequiredAttribute("blurrinessX", filter->blurrinessX); xml.addRequiredAttribute("blurrinessY", filter->blurrinessY); @@ -1071,7 +1071,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node) { break; } case NodeType::DropShadowFilter: { - auto filter = static_cast(node); + auto filter = static_cast(node); xml.openElement("DropShadowFilter"); xml.addAttribute("offsetX", filter->offsetX); xml.addAttribute("offsetY", filter->offsetY); @@ -1083,7 +1083,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node) { break; } case NodeType::InnerShadowFilter: { - auto filter = static_cast(node); + auto filter = static_cast(node); xml.openElement("InnerShadowFilter"); xml.addAttribute("offsetX", filter->offsetX); xml.addAttribute("offsetY", filter->offsetY); @@ -1095,7 +1095,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node) { break; } case NodeType::BlendFilter: { - auto filter = static_cast(node); + auto filter = static_cast(node); xml.openElement("BlendFilter"); xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); if (filter->filterBlendMode != BlendMode::Normal) { @@ -1105,7 +1105,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node) { break; } case NodeType::ColorMatrixFilter: { - auto filter = static_cast(node); + auto filter = static_cast(node); xml.openElement("ColorMatrixFilter"); std::vector values(filter->matrix.begin(), filter->matrix.end()); xml.addAttribute("matrix", floatListToString(values)); @@ -1121,10 +1121,10 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilterNode* node) { // Resource writing //============================================================================== -static void writeResource(XMLBuilder& xml, const ResourceNode* node, const ResourceContext& ctx) { +static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceContext& ctx) { switch (node->type()) { case NodeType::Image: { - auto image = static_cast(node); + auto image = static_cast(node); xml.openElement("Image"); xml.addAttribute("id", image->id); xml.addAttribute("source", image->source); @@ -1132,7 +1132,7 @@ static void writeResource(XMLBuilder& xml, const ResourceNode* node, const Resou break; } case NodeType::PathData: { - auto pathData = static_cast(node); + auto pathData = static_cast(node); xml.openElement("PathData"); xml.addAttribute("id", pathData->id); xml.addAttribute("data", pathData->data); @@ -1140,7 +1140,7 @@ static void writeResource(XMLBuilder& xml, const ResourceNode* node, const Resou break; } case NodeType::Composition: { - auto comp = static_cast(node); + auto comp = static_cast(node); xml.openElement("Composition"); xml.addAttribute("id", comp->id); xml.addRequiredAttribute("width", static_cast(comp->width)); @@ -1162,7 +1162,7 @@ static void writeResource(XMLBuilder& xml, const ResourceNode* node, const Resou case NodeType::ConicGradient: case NodeType::DiamondGradient: case NodeType::ImagePattern: - writeColorSource(xml, static_cast(node), true); + writeColorSource(xml, static_cast(node), true); break; default: break; @@ -1173,7 +1173,7 @@ static void writeResource(XMLBuilder& xml, const ResourceNode* node, const Resou // Layer writing //============================================================================== -static void writeLayer(XMLBuilder& xml, const LayerNode* node, const ResourceContext& ctx) { +static void writeLayer(XMLBuilder& xml, const Layer* node, const ResourceContext& ctx) { xml.openElement("Layer"); if (!node->id.empty()) { xml.addAttribute("id", node->id); @@ -1266,12 +1266,12 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { // Build ColorSource resources list (only those with multiple references) // We need to store pointers to actual ColorSource nodes for writing - std::unordered_map colorSourceByKey = {}; + std::unordered_map colorSourceByKey = {}; for (const auto& layer : doc.layers) { - std::function collectColorSources = [&](const LayerNode* layer) { + std::function collectColorSources = [&](const Layer* layer) { for (const auto& element : layer->contents) { if (element->type() == NodeType::Fill) { - auto fill = static_cast(element.get()); + auto fill = static_cast(element.get()); if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { std::string key = colorSourceToKey(fill->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { @@ -1279,7 +1279,7 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { } } } else if (element->type() == NodeType::Stroke) { - auto stroke = static_cast(element.get()); + auto stroke = static_cast(element.get()); if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { std::string key = colorSourceToKey(stroke->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { @@ -1287,11 +1287,11 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { } } } else if (element->type() == NodeType::Group) { - auto group = static_cast(element.get()); - std::function collectFromGroup = [&](const GroupNode* g) { + auto group = static_cast(element.get()); + std::function collectFromGroup = [&](const Group* g) { for (const auto& child : g->elements) { if (child->type() == NodeType::Fill) { - auto fill = static_cast(child.get()); + auto fill = static_cast(child.get()); if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { std::string key = colorSourceToKey(fill->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { @@ -1299,7 +1299,7 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { } } } else if (child->type() == NodeType::Stroke) { - auto stroke = static_cast(child.get()); + auto stroke = static_cast(child.get()); if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { std::string key = colorSourceToKey(stroke->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { @@ -1307,7 +1307,7 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { } } } else if (child->type() == NodeType::Group) { - collectFromGroup(static_cast(child.get())); + collectFromGroup(static_cast(child.get())); } } }; @@ -1324,11 +1324,11 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { // Also collect from Compositions for (const auto& resource : doc.resources) { if (resource->type() == NodeType::Composition) { - auto comp = static_cast(resource.get()); - std::function collectColorSources = [&](const LayerNode* layer) { + auto comp = static_cast(resource.get()); + std::function collectColorSources = [&](const Layer* layer) { for (const auto& element : layer->contents) { if (element->type() == NodeType::Fill) { - auto fill = static_cast(element.get()); + auto fill = static_cast(element.get()); if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { std::string key = colorSourceToKey(fill->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { @@ -1336,7 +1336,7 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { } } } else if (element->type() == NodeType::Stroke) { - auto stroke = static_cast(element.get()); + auto stroke = static_cast(element.get()); if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { std::string key = colorSourceToKey(stroke->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { @@ -1344,11 +1344,11 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { } } } else if (element->type() == NodeType::Group) { - auto group = static_cast(element.get()); - std::function collectFromGroup = [&](const GroupNode* g) { + auto group = static_cast(element.get()); + std::function collectFromGroup = [&](const Group* g) { for (const auto& child : g->elements) { if (child->type() == NodeType::Fill) { - auto fill = static_cast(child.get()); + auto fill = static_cast(child.get()); if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { std::string key = colorSourceToKey(fill->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { @@ -1356,7 +1356,7 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { } } } else if (child->type() == NodeType::Stroke) { - auto stroke = static_cast(child.get()); + auto stroke = static_cast(child.get()); if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { std::string key = colorSourceToKey(stroke->colorSource.get()); @@ -1365,7 +1365,7 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { } } } else if (child->type() == NodeType::Group) { - collectFromGroup(static_cast(child.get())); + collectFromGroup(static_cast(child.get())); } } }; diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index cab84e9e77..20ea273248 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -158,7 +158,7 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr rootStyle = computeInheritedStyle(root, rootStyle); // Collect converted layers. - std::vector> convertedLayers; + std::vector> convertedLayers; child = root->getFirstChild(); while (child) { if (child->name != "defs") { @@ -182,7 +182,7 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr // If viewBox transform is needed, wrap in a root layer with the transform. // Otherwise, add layers directly to document (no root wrapper). if (needsViewBoxTransform) { - auto rootLayer = std::make_unique(); + auto rootLayer = std::make_unique(); rootLayer->matrix = viewBoxMatrix; for (auto& layer : convertedLayers) { rootLayer->children.push_back(std::move(layer)); @@ -244,7 +244,7 @@ void SVGParserImpl::parseDefs(const std::shared_ptr& defsNode) { } } -std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, +std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, const InheritedStyle& parentStyle) { const auto& tag = element->name; @@ -256,7 +256,7 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr(); + auto layer = std::make_unique(); // Parse common layer attributes. layer->id = getAttribute(element, "id"); @@ -330,11 +330,11 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle) { const auto& tag = element->name; - // Handle text element specially - it returns a GroupNode with TextSpan. + // Handle text element specially - it returns a Group with TextSpan. if (tag == "text") { auto textGroup = convertText(element, inheritedStyle); if (textGroup) { @@ -351,7 +351,7 @@ void SVGParserImpl::convertChildren(const std::shared_ptr& element, addFillStroke(element, contents, inheritedStyle); } -std::unique_ptr SVGParserImpl::convertElement( +std::unique_ptr SVGParserImpl::convertElement( const std::shared_ptr& element) { const auto& tag = element->name; @@ -376,18 +376,18 @@ std::unique_ptr SVGParserImpl::convertElement( return nullptr; } -std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& element, +std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& element, const InheritedStyle& parentStyle) { // Compute inherited style for this group element. InheritedStyle inheritedStyle = computeInheritedStyle(element, parentStyle); - auto group = std::make_unique(); + auto group = std::make_unique(); group->name = getAttribute(element, "id"); std::string transform = getAttribute(element, "transform"); if (!transform.empty()) { - // For GroupNode, we need to decompose the matrix into position/rotation/scale. + // For Group, we need to decompose the matrix into position/rotation/scale. // For simplicity, just store as position offset for translation. Matrix m = parseTransform(transform); group->position = {m.tx, m.ty}; @@ -412,7 +412,7 @@ std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr SVGParserImpl::convertRect( +std::unique_ptr SVGParserImpl::convertRect( const std::shared_ptr& element) { float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); @@ -425,7 +425,7 @@ std::unique_ptr SVGParserImpl::convertRect( ry = rx; } - auto rect = std::make_unique(); + auto rect = std::make_unique(); rect->center.x = x + width / 2; rect->center.y = y + height / 2; rect->size.width = width; @@ -435,13 +435,13 @@ std::unique_ptr SVGParserImpl::convertRect( return rect; } -std::unique_ptr SVGParserImpl::convertCircle( +std::unique_ptr SVGParserImpl::convertCircle( const std::shared_ptr& element) { float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); float r = parseLength(getAttribute(element, "r"), _viewBoxWidth); - auto ellipse = std::make_unique(); + auto ellipse = std::make_unique(); ellipse->center.x = cx; ellipse->center.y = cy; ellipse->size.width = r * 2; @@ -450,14 +450,14 @@ std::unique_ptr SVGParserImpl::convertCircle( return ellipse; } -std::unique_ptr SVGParserImpl::convertEllipse( +std::unique_ptr SVGParserImpl::convertEllipse( const std::shared_ptr& element) { float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); - auto ellipse = std::make_unique(); + auto ellipse = std::make_unique(); ellipse->center.x = cx; ellipse->center.y = cy; ellipse->size.width = rx * 2; @@ -466,37 +466,37 @@ std::unique_ptr SVGParserImpl::convertEllipse( return ellipse; } -std::unique_ptr SVGParserImpl::convertLine( +std::unique_ptr SVGParserImpl::convertLine( const std::shared_ptr& element) { float x1 = parseLength(getAttribute(element, "x1"), _viewBoxWidth); float y1 = parseLength(getAttribute(element, "y1"), _viewBoxHeight); float x2 = parseLength(getAttribute(element, "x2"), _viewBoxWidth); float y2 = parseLength(getAttribute(element, "y2"), _viewBoxHeight); - auto path = std::make_unique(); + auto path = std::make_unique(); path->data.moveTo(x1, y1); path->data.lineTo(x2, y2); return path; } -std::unique_ptr SVGParserImpl::convertPolyline( +std::unique_ptr SVGParserImpl::convertPolyline( const std::shared_ptr& element) { - auto path = std::make_unique(); + auto path = std::make_unique(); path->data = parsePoints(getAttribute(element, "points"), false); return path; } -std::unique_ptr SVGParserImpl::convertPolygon( +std::unique_ptr SVGParserImpl::convertPolygon( const std::shared_ptr& element) { - auto path = std::make_unique(); + auto path = std::make_unique(); path->data = parsePoints(getAttribute(element, "points"), true); return path; } -std::unique_ptr SVGParserImpl::convertPath( +std::unique_ptr SVGParserImpl::convertPath( const std::shared_ptr& element) { - auto path = std::make_unique(); + auto path = std::make_unique(); std::string d = getAttribute(element, "d"); if (!d.empty()) { path->data = PathData::FromSVGString(d); @@ -504,9 +504,9 @@ std::unique_ptr SVGParserImpl::convertPath( return path; } -std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr& element, +std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr& element, const InheritedStyle& inheritedStyle) { - auto group = std::make_unique(); + auto group = std::make_unique(); float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); @@ -531,7 +531,7 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr(); + auto textSpan = std::make_unique(); textSpan->x = x; textSpan->y = y; textSpan->text = textContent; @@ -554,7 +554,7 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr SVGParserImpl::convertUse( +std::unique_ptr SVGParserImpl::convertUse( const std::shared_ptr& element) { std::string href = getAttribute(element, "xlink:href"); if (href.empty()) { @@ -574,7 +574,7 @@ std::unique_ptr SVGParserImpl::convertUse( float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); if (x != 0 || y != 0) { // Wrap in a group with translation. - auto group = std::make_unique(); + auto group = std::make_unique(); group->position = {x, y}; group->elements.push_back(std::move(node)); return group; @@ -584,14 +584,14 @@ std::unique_ptr SVGParserImpl::convertUse( } // For non-expanded use references, just create an empty group for now. - auto group = std::make_unique(); + auto group = std::make_unique(); group->name = "_useRef:" + refId; return group; } -std::unique_ptr SVGParserImpl::convertLinearGradient( +std::unique_ptr SVGParserImpl::convertLinearGradient( const std::shared_ptr& element) { - auto gradient = std::make_unique(); + auto gradient = std::make_unique(); gradient->id = getAttribute(element, "id"); @@ -635,7 +635,7 @@ std::unique_ptr SVGParserImpl::convertLinearGradient( auto child = element->getFirstChild(); while (child) { if (child->name == "stop") { - ColorStopNode stop; + ColorStop stop; stop.offset = parseLength(getAttribute(child, "offset", "0"), 1.0f); stop.color = parseColor(getAttribute(child, "stop-color", "#000000")); float opacity = parseLength(getAttribute(child, "stop-opacity", "1"), 1.0f); @@ -648,9 +648,9 @@ std::unique_ptr SVGParserImpl::convertLinearGradient( return gradient; } -std::unique_ptr SVGParserImpl::convertRadialGradient( +std::unique_ptr SVGParserImpl::convertRadialGradient( const std::shared_ptr& element) { - auto gradient = std::make_unique(); + auto gradient = std::make_unique(); gradient->id = getAttribute(element, "id"); @@ -696,7 +696,7 @@ std::unique_ptr SVGParserImpl::convertRadialGradient( auto child = element->getFirstChild(); while (child) { if (child->name == "stop") { - ColorStopNode stop; + ColorStop stop; stop.offset = parseLength(getAttribute(child, "offset", "0"), 1.0f); stop.color = parseColor(getAttribute(child, "stop-color", "#000000")); float opacity = parseLength(getAttribute(child, "stop-opacity", "1"), 1.0f); @@ -709,9 +709,9 @@ std::unique_ptr SVGParserImpl::convertRadialGradient( return gradient; } -std::unique_ptr SVGParserImpl::convertPattern( +std::unique_ptr SVGParserImpl::convertPattern( const std::shared_ptr& element, const Rect& shapeBounds) { - auto pattern = std::make_unique(); + auto pattern = std::make_unique(); pattern->id = getAttribute(element, "id"); @@ -829,7 +829,7 @@ std::unique_ptr SVGParserImpl::convertPattern( } void SVGParserImpl::addFillStroke(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle) { // Get shape bounds for pattern calculations (computed once, used if needed). Rect shapeBounds = getShapeBounds(element); @@ -846,11 +846,11 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, if (fill != "none") { if (fill.empty()) { // No fill specified anywhere - use SVG default black. - auto fillNode = std::make_unique(); + auto fillNode = std::make_unique(); fillNode->color = "#000000"; contents.push_back(std::move(fillNode)); } else if (fill.find("url(") == 0) { - auto fillNode = std::make_unique(); + auto fillNode = std::make_unique(); std::string refId = resolveUrl(fill); // Try to inline the gradient or pattern. @@ -867,7 +867,7 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } contents.push_back(std::move(fillNode)); } else { - auto fillNode = std::make_unique(); + auto fillNode = std::make_unique(); Color color = parseColor(fill); // Determine effective fill-opacity. @@ -900,7 +900,7 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } if (!stroke.empty() && stroke != "none") { - auto strokeNode = std::make_unique(); + auto strokeNode = std::make_unique(); if (stroke.find("url(") == 0) { std::string refId = resolveUrl(stroke); @@ -1465,7 +1465,7 @@ std::string SVGParserImpl::registerImageResource(const std::string& imageSource) std::string imageId = generateUniqueId("image"); // Create and add the image resource to the document. - auto imageNode = std::make_unique(); + auto imageNode = std::make_unique(); imageNode->id = imageId; imageNode->source = imageSource; _document->resources.push_back(std::move(imageNode)); @@ -1477,29 +1477,29 @@ std::string SVGParserImpl::registerImageResource(const std::string& imageSource) } // Helper function to check if two VectorElement nodes are the same geometry. -static bool isSameGeometry(const VectorElementNode* a, const VectorElementNode* b) { +static bool isSameGeometry(const VectorElement* a, const VectorElement* b) { if (!a || !b || a->type() != b->type()) { return false; } switch (a->type()) { case NodeType::Rectangle: { - auto rectA = static_cast(a); - auto rectB = static_cast(b); + auto rectA = static_cast(a); + auto rectB = static_cast(b); return rectA->center.x == rectB->center.x && rectA->center.y == rectB->center.y && rectA->size.width == rectB->size.width && rectA->size.height == rectB->size.height && rectA->roundness == rectB->roundness; } case NodeType::Ellipse: { - auto ellipseA = static_cast(a); - auto ellipseB = static_cast(b); + auto ellipseA = static_cast(a); + auto ellipseB = static_cast(b); return ellipseA->center.x == ellipseB->center.x && ellipseA->center.y == ellipseB->center.y && ellipseA->size.width == ellipseB->size.width && ellipseA->size.height == ellipseB->size.height; } case NodeType::Path: { - auto pathA = static_cast(a); - auto pathB = static_cast(b); + auto pathA = static_cast(a); + auto pathB = static_cast(b); return pathA->data.toSVGString() == pathB->data.toSVGString(); } default: @@ -1508,8 +1508,8 @@ static bool isSameGeometry(const VectorElementNode* a, const VectorElementNode* } // Check if a layer is a simple shape layer (contains exactly one geometry and one Fill or Stroke). -static bool isSimpleShapeLayer(const LayerNode* layer, const VectorElementNode*& outGeometry, - const VectorElementNode*& outPainter) { +static bool isSimpleShapeLayer(const Layer* layer, const VectorElement*& outGeometry, + const VectorElement*& outPainter) { if (!layer || layer->contents.size() != 2) { return false; } @@ -1537,23 +1537,23 @@ static bool isSimpleShapeLayer(const LayerNode* layer, const VectorElementNode*& return false; } -void SVGParserImpl::mergeAdjacentLayers(std::vector>& layers) { +void SVGParserImpl::mergeAdjacentLayers(std::vector>& layers) { if (layers.size() < 2) { return; } - std::vector> merged; + std::vector> merged; size_t i = 0; while (i < layers.size()) { - const VectorElementNode* geomA = nullptr; - const VectorElementNode* painterA = nullptr; + const VectorElement* geomA = nullptr; + const VectorElement* painterA = nullptr; if (isSimpleShapeLayer(layers[i].get(), geomA, painterA)) { // Check if the next layer has the same geometry. if (i + 1 < layers.size()) { - const VectorElementNode* geomB = nullptr; - const VectorElementNode* painterB = nullptr; + const VectorElement* geomB = nullptr; + const VectorElement* painterB = nullptr; if (isSimpleShapeLayer(layers[i + 1].get(), geomB, painterB) && isSameGeometry(geomA, geomB)) { @@ -1563,28 +1563,28 @@ void SVGParserImpl::mergeAdjacentLayers(std::vector>& if (aHasFill != bHasFill) { // Create merged layer. - auto mergedLayer = std::make_unique(); + auto mergedLayer = std::make_unique(); // Keep geometry from first layer. auto geomClone = layers[i]->contents[0]->clone(); mergedLayer->contents.push_back( - std::unique_ptr(static_cast(geomClone.release()))); + std::unique_ptr(static_cast(geomClone.release()))); // Add Fill first, then Stroke (standard order). if (aHasFill) { auto fillClone = layers[i]->contents[1]->clone(); mergedLayer->contents.push_back( - std::unique_ptr(static_cast(fillClone.release()))); + std::unique_ptr(static_cast(fillClone.release()))); auto strokeClone = layers[i + 1]->contents[1]->clone(); mergedLayer->contents.push_back( - std::unique_ptr(static_cast(strokeClone.release()))); + std::unique_ptr(static_cast(strokeClone.release()))); } else { auto fillClone = layers[i + 1]->contents[1]->clone(); mergedLayer->contents.push_back( - std::unique_ptr(static_cast(fillClone.release()))); + std::unique_ptr(static_cast(fillClone.release()))); auto strokeClone = layers[i]->contents[1]->clone(); mergedLayer->contents.push_back( - std::unique_ptr(static_cast(strokeClone.release()))); + std::unique_ptr(static_cast(strokeClone.release()))); } merged.push_back(std::move(mergedLayer)); @@ -1603,9 +1603,9 @@ void SVGParserImpl::mergeAdjacentLayers(std::vector>& layers = std::move(merged); } -std::unique_ptr SVGParserImpl::convertMaskElement( +std::unique_ptr SVGParserImpl::convertMaskElement( const std::shared_ptr& maskElement, const InheritedStyle& parentStyle) { - auto maskLayer = std::make_unique(); + auto maskLayer = std::make_unique(); maskLayer->id = getAttribute(maskElement, "id"); maskLayer->name = maskLayer->id; maskLayer->visible = false; @@ -1634,12 +1634,12 @@ std::unique_ptr SVGParserImpl::convertMaskElement( void SVGParserImpl::convertFilterElement( const std::shared_ptr& filterElement, - std::vector>& filters) { + std::vector>& filters) { // Parse filter children to find effect elements. auto child = filterElement->getFirstChild(); while (child) { if (child->name == "feGaussianBlur") { - auto blurFilter = std::make_unique(); + auto blurFilter = std::make_unique(); std::string stdDeviation = getAttribute(child, "stdDeviation", "0"); // stdDeviation can be one value (both X and Y) or two values (X Y). std::istringstream iss(stdDeviation); @@ -1684,7 +1684,7 @@ std::string SVGParserImpl::generateUniqueId(const std::string& prefix) { return id; } -void SVGParserImpl::parseCustomData(const std::shared_ptr& element, LayerNode* layer) { +void SVGParserImpl::parseCustomData(const std::shared_ptr& element, Layer* layer) { if (!element || !layer) { return; } diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index e1edb2af7a..425ce95973 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -55,39 +55,39 @@ class SVGParserImpl { void parseDefs(const std::shared_ptr& defsNode); - std::unique_ptr convertToLayer(const std::shared_ptr& element, + std::unique_ptr convertToLayer(const std::shared_ptr& element, const InheritedStyle& parentStyle); void convertChildren(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle); - std::unique_ptr convertElement(const std::shared_ptr& element); - std::unique_ptr convertG(const std::shared_ptr& element, + std::unique_ptr convertElement(const std::shared_ptr& element); + std::unique_ptr convertG(const std::shared_ptr& element, const InheritedStyle& inheritedStyle); - std::unique_ptr convertRect(const std::shared_ptr& element); - std::unique_ptr convertCircle(const std::shared_ptr& element); - std::unique_ptr convertEllipse(const std::shared_ptr& element); - std::unique_ptr convertLine(const std::shared_ptr& element); - std::unique_ptr convertPolyline(const std::shared_ptr& element); - std::unique_ptr convertPolygon(const std::shared_ptr& element); - std::unique_ptr convertPath(const std::shared_ptr& element); - std::unique_ptr convertText(const std::shared_ptr& element, + std::unique_ptr convertRect(const std::shared_ptr& element); + std::unique_ptr convertCircle(const std::shared_ptr& element); + std::unique_ptr convertEllipse(const std::shared_ptr& element); + std::unique_ptr convertLine(const std::shared_ptr& element); + std::unique_ptr convertPolyline(const std::shared_ptr& element); + std::unique_ptr convertPolygon(const std::shared_ptr& element); + std::unique_ptr convertPath(const std::shared_ptr& element); + std::unique_ptr convertText(const std::shared_ptr& element, const InheritedStyle& inheritedStyle); - std::unique_ptr convertUse(const std::shared_ptr& element); + std::unique_ptr convertUse(const std::shared_ptr& element); - std::unique_ptr convertLinearGradient( + std::unique_ptr convertLinearGradient( const std::shared_ptr& element); - std::unique_ptr convertRadialGradient( + std::unique_ptr convertRadialGradient( const std::shared_ptr& element); - std::unique_ptr convertPattern(const std::shared_ptr& element, + std::unique_ptr convertPattern(const std::shared_ptr& element, const Rect& shapeBounds); - std::unique_ptr convertMaskElement(const std::shared_ptr& maskElement, + std::unique_ptr convertMaskElement(const std::shared_ptr& maskElement, const InheritedStyle& parentStyle); void convertFilterElement(const std::shared_ptr& filterElement, - std::vector>& filters); + std::vector>& filters); void addFillStroke(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle); // Compute shape bounds from SVG element attributes. @@ -113,7 +113,7 @@ class SVGParserImpl { // Merge adjacent layers that have the same shape geometry. // This optimizes the output by combining Fill and Stroke for identical shapes into one Layer. - void mergeAdjacentLayers(std::vector>& layers); + void mergeAdjacentLayers(std::vector>& layers); // Collect all IDs from the SVG document to avoid conflicts when generating new IDs. void collectAllIds(const std::shared_ptr& node); @@ -122,12 +122,12 @@ class SVGParserImpl { std::string generateUniqueId(const std::string& prefix); // Parse data-* attributes from element and add to layer's customData. - void parseCustomData(const std::shared_ptr& element, LayerNode* layer); + void parseCustomData(const std::shared_ptr& element, Layer* layer); PAGXSVGParser::Options _options = {}; std::shared_ptr _document = nullptr; std::unordered_map> _defs = {}; - std::vector> _maskLayers = {}; + std::vector> _maskLayers = {}; std::unordered_map _imageSourceToId = {}; // Maps image source to resource ID. std::unordered_set _existingIds = {}; // All IDs found in SVG to avoid conflicts. int _nextImageId = 0; diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 2bbe9af6c0..9aa947c1e6 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -203,7 +203,7 @@ class LayerBuilderImpl { // Build layer tree auto rootLayer = tgfx::Layer::Make(); for (const auto& layer : document.layers) { - auto childLayer = convertLayerNode(layer.get()); + auto childLayer = convertLayer(layer.get()); if (childLayer) { rootLayer->addChild(childLayer); } @@ -215,7 +215,7 @@ class LayerBuilderImpl { } private: - std::shared_ptr convertLayerNode(const LayerNode* node) { + std::shared_ptr convertLayer(const Layer* node) { if (!node) { return nullptr; } @@ -232,7 +232,7 @@ class LayerBuilderImpl { applyLayerAttributes(node, layer.get()); for (const auto& child : node->children) { - auto childLayer = convertLayerNode(child.get()); + auto childLayer = convertLayer(child.get()); if (childLayer) { layer->addChild(childLayer); } @@ -242,7 +242,7 @@ class LayerBuilderImpl { return layer; } - std::shared_ptr convertVectorLayer(const LayerNode* node) { + std::shared_ptr convertVectorLayer(const Layer* node) { auto layer = tgfx::VectorLayer::Make(); std::vector> contents; @@ -257,42 +257,42 @@ class LayerBuilderImpl { return layer; } - std::shared_ptr convertVectorElement(const VectorElementNode* node) { + std::shared_ptr convertVectorElement(const VectorElement* node) { if (!node) { return nullptr; } switch (node->type()) { case NodeType::Rectangle: - return convertRectangle(static_cast(node)); + return convertRectangle(static_cast(node)); case NodeType::Ellipse: - return convertEllipse(static_cast(node)); + return convertEllipse(static_cast(node)); case NodeType::Polystar: - return convertPolystar(static_cast(node)); + return convertPolystar(static_cast(node)); case NodeType::Path: - return convertPath(static_cast(node)); + return convertPath(static_cast(node)); case NodeType::TextSpan: - return convertTextSpan(static_cast(node)); + return convertTextSpan(static_cast(node)); case NodeType::Fill: - return convertFill(static_cast(node)); + return convertFill(static_cast(node)); case NodeType::Stroke: - return convertStroke(static_cast(node)); + return convertStroke(static_cast(node)); case NodeType::TrimPath: - return convertTrimPath(static_cast(node)); + return convertTrimPath(static_cast(node)); case NodeType::RoundCorner: - return convertRoundCorner(static_cast(node)); + return convertRoundCorner(static_cast(node)); case NodeType::MergePath: - return convertMergePath(static_cast(node)); + return convertMergePath(static_cast(node)); case NodeType::Repeater: - return convertRepeater(static_cast(node)); + return convertRepeater(static_cast(node)); case NodeType::Group: - return convertGroup(static_cast(node)); + return convertGroup(static_cast(node)); default: return nullptr; } } - std::shared_ptr convertRectangle(const RectangleNode* node) { + std::shared_ptr convertRectangle(const Rectangle* node) { auto rect = std::make_shared(); rect->setCenter(ToTGFX(node->center)); rect->setSize({node->size.width, node->size.height}); @@ -301,7 +301,7 @@ class LayerBuilderImpl { return rect; } - std::shared_ptr convertEllipse(const EllipseNode* node) { + std::shared_ptr convertEllipse(const Ellipse* node) { auto ellipse = std::make_shared(); ellipse->setCenter(ToTGFX(node->center)); ellipse->setSize({node->size.width, node->size.height}); @@ -309,7 +309,7 @@ class LayerBuilderImpl { return ellipse; } - std::shared_ptr convertPolystar(const PolystarNode* node) { + std::shared_ptr convertPolystar(const Polystar* node) { auto polystar = std::make_shared(); polystar->setCenter(ToTGFX(node->center)); polystar->setPointCount(node->pointCount); @@ -327,14 +327,14 @@ class LayerBuilderImpl { return polystar; } - std::shared_ptr convertPath(const PathNode* node) { + std::shared_ptr convertPath(const Path* node) { auto shapePath = std::make_shared(); auto tgfxPath = ToTGFX(node->data); shapePath->setPath(tgfxPath); return shapePath; } - std::shared_ptr convertTextSpan(const TextSpanNode* node) { + std::shared_ptr convertTextSpan(const TextSpan* node) { auto textSpan = std::make_shared(); std::shared_ptr typeface = nullptr; @@ -371,7 +371,7 @@ class LayerBuilderImpl { return textSpan; } - std::shared_ptr convertFill(const FillNode* node) { + std::shared_ptr convertFill(const Fill* node) { auto fill = std::make_shared(); std::shared_ptr colorSource = nullptr; @@ -390,7 +390,7 @@ class LayerBuilderImpl { return fill; } - std::shared_ptr convertStroke(const StrokeNode* node) { + std::shared_ptr convertStroke(const Stroke* node) { auto stroke = std::make_shared(); std::shared_ptr colorSource = nullptr; @@ -418,26 +418,26 @@ class LayerBuilderImpl { return stroke; } - std::shared_ptr convertColorSource(const ColorSourceNode* node) { + std::shared_ptr convertColorSource(const ColorSource* node) { if (!node) { return nullptr; } switch (node->type()) { case NodeType::SolidColor: { - auto solid = static_cast(node); + auto solid = static_cast(node); return tgfx::SolidColor::Make(ToTGFX(solid->color)); } case NodeType::LinearGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); return convertLinearGradient(grad); } case NodeType::RadialGradient: { - auto grad = static_cast(node); + auto grad = static_cast(node); return convertRadialGradient(grad); } case NodeType::ImagePattern: { - auto pattern = static_cast(node); + auto pattern = static_cast(node); return convertImagePattern(pattern); } default: @@ -445,7 +445,7 @@ class LayerBuilderImpl { } } - std::shared_ptr convertLinearGradient(const LinearGradientNode* node) { + std::shared_ptr convertLinearGradient(const LinearGradient* node) { std::vector colors; std::vector positions; @@ -463,7 +463,7 @@ class LayerBuilderImpl { positions); } - std::shared_ptr convertRadialGradient(const RadialGradientNode* node) { + std::shared_ptr convertRadialGradient(const RadialGradient* node) { std::vector colors; std::vector positions; @@ -480,7 +480,7 @@ class LayerBuilderImpl { return tgfx::Gradient::MakeRadial(ToTGFX(node->center), node->radius, colors, positions); } - std::shared_ptr convertImagePattern(const ImagePatternNode* node) { + std::shared_ptr convertImagePattern(const ImagePattern* node) { if (!node || node->image.empty()) { return nullptr; } @@ -534,7 +534,7 @@ class LayerBuilderImpl { } for (const auto& resource : *_resources) { if (resource->type() == NodeType::Image) { - auto imageNode = static_cast(resource.get()); + auto imageNode = static_cast(resource.get()); if (imageNode->id == resourceId) { if (imageNode->source.find("data:") == 0) { return ImageFromDataURI(imageNode->source); @@ -550,7 +550,7 @@ class LayerBuilderImpl { } return nullptr; } - std::shared_ptr convertTrimPath(const TrimPathNode* node) { + std::shared_ptr convertTrimPath(const TrimPath* node) { auto trim = std::make_shared(); trim->setStart(node->start); trim->setEnd(node->end); @@ -558,25 +558,25 @@ class LayerBuilderImpl { return trim; } - std::shared_ptr convertRoundCorner(const RoundCornerNode* node) { + std::shared_ptr convertRoundCorner(const RoundCorner* node) { auto round = std::make_shared(); round->setRadius(node->radius); return round; } - std::shared_ptr convertMergePath(const MergePathNode*) { + std::shared_ptr convertMergePath(const MergePath*) { auto merge = std::make_shared(); return merge; } - std::shared_ptr convertRepeater(const RepeaterNode* node) { + std::shared_ptr convertRepeater(const Repeater* node) { auto repeater = std::make_shared(); repeater->setCopies(node->copies); repeater->setOffset(node->offset); return repeater; } - std::shared_ptr convertGroup(const GroupNode* node) { + std::shared_ptr convertGroup(const Group* node) { auto group = std::make_shared(); std::vector> elements; @@ -591,7 +591,7 @@ class LayerBuilderImpl { return group; } - void applyLayerAttributes(const LayerNode* node, tgfx::Layer* layer) { + void applyLayerAttributes(const Layer* node, tgfx::Layer* layer) { layer->setVisible(node->visible); layer->setAlpha(node->alpha); @@ -624,19 +624,19 @@ class LayerBuilderImpl { } } - std::shared_ptr convertLayerStyle(const LayerStyleNode* node) { + std::shared_ptr convertLayerStyle(const LayerStyle* node) { if (!node) { return nullptr; } switch (node->type()) { case NodeType::DropShadowStyle: { - auto style = static_cast(node); + auto style = static_cast(node); return tgfx::DropShadowStyle::Make(style->offsetX, style->offsetY, style->blurrinessX, style->blurrinessY, ToTGFX(style->color)); } case NodeType::InnerShadowStyle: { - auto style = static_cast(node); + auto style = static_cast(node); return tgfx::InnerShadowStyle::Make(style->offsetX, style->offsetY, style->blurrinessX, style->blurrinessY, ToTGFX(style->color)); } @@ -645,18 +645,18 @@ class LayerBuilderImpl { } } - std::shared_ptr convertLayerFilter(const LayerFilterNode* node) { + std::shared_ptr convertLayerFilter(const LayerFilter* node) { if (!node) { return nullptr; } switch (node->type()) { case NodeType::BlurFilter: { - auto filter = static_cast(node); + auto filter = static_cast(node); return tgfx::BlurFilter::Make(filter->blurrinessX, filter->blurrinessY); } case NodeType::DropShadowFilter: { - auto filter = static_cast(node); + auto filter = static_cast(node); return tgfx::DropShadowFilter::Make(filter->offsetX, filter->offsetY, filter->blurrinessX, filter->blurrinessY, ToTGFX(filter->color)); } @@ -666,7 +666,7 @@ class LayerBuilderImpl { } LayerBuilder::Options _options = {}; - const std::vector>* _resources = nullptr; + const std::vector>* _resources = nullptr; }; // Public API implementation diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 0218189661..2af3a07cae 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -19,9 +19,8 @@ #include #include "pagx/LayerBuilder.h" #include "pagx/PAGXDocument.h" -#include "pagx/PAGXNode.h" #include "pagx/PAGXSVGParser.h" -#include "pagx/PAGXTypes.h" +#include "pagx/model/Model.h" #include "pagx/PathData.h" #include "tgfx/core/Data.h" #include "tgfx/core/Stream.h" @@ -187,8 +186,8 @@ PAG_TEST(PAGXTest, PathDataForEach) { * Test case: Strong-typed PAGX node creation */ PAG_TEST(PAGXTest, PAGXNodeBasic) { - // Test RectangleNode creation - auto rect = std::make_unique(); + // Test Rectangle creation + auto rect = std::make_unique(); rect->center.x = 50; rect->center.y = 50; rect->size.width = 100; @@ -200,21 +199,21 @@ PAG_TEST(PAGXTest, PAGXNodeBasic) { EXPECT_FLOAT_EQ(rect->center.x, 50); EXPECT_FLOAT_EQ(rect->size.width, 100); - // Test PathNode creation - auto path = std::make_unique(); + // Test Path creation + auto path = std::make_unique(); path->data = pagx::PathData::FromSVGString("M0 0 L100 100"); EXPECT_EQ(path->type(), pagx::NodeType::Path); EXPECT_GT(path->data.verbs().size(), 0u); - // Test FillNode creation - auto fill = std::make_unique(); + // Test Fill creation + auto fill = std::make_unique(); fill->color = "#FF0000"; fill->alpha = 0.8f; EXPECT_EQ(fill->type(), pagx::NodeType::Fill); EXPECT_EQ(fill->color, "#FF0000"); - // Test GroupNode with children - auto group = std::make_unique(); + // Test Group with children + auto group = std::make_unique(); group->name = "testGroup"; group->elements.push_back(std::move(rect)); group->elements.push_back(std::move(fill)); @@ -232,21 +231,21 @@ PAG_TEST(PAGXTest, PAGXDocumentXMLExport) { EXPECT_EQ(doc->height, 300.0f); // Create a layer with contents - auto layer = std::make_unique(); + auto layer = std::make_unique(); layer->id = "layer1"; layer->name = "Test Layer"; // Add a group with rectangle and fill - auto group = std::make_unique(); + auto group = std::make_unique(); group->name = "testGroup"; - auto rect = std::make_unique(); + auto rect = std::make_unique(); rect->center.x = 50; rect->center.y = 50; rect->size.width = 80; rect->size.height = 60; - auto fill = std::make_unique(); + auto fill = std::make_unique(); fill->color = "#0000FF"; group->elements.push_back(std::move(rect)); @@ -275,16 +274,16 @@ PAG_TEST(PAGXTest, PAGXDocumentRoundTrip) { auto doc1 = pagx::PAGXDocument::Make(200, 150); ASSERT_TRUE(doc1 != nullptr); - auto layer = std::make_unique(); + auto layer = std::make_unique(); layer->name = "TestLayer"; - auto rect = std::make_unique(); + auto rect = std::make_unique(); rect->center.x = 50; rect->center.y = 50; rect->size.width = 80; rect->size.height = 60; - auto fill = std::make_unique(); + auto fill = std::make_unique(); fill->color = "#00FF00"; layer->contents.push_back(std::move(rect)); @@ -361,25 +360,25 @@ PAG_TEST(PAGXTest, PAGXTypesBasic) { /** * Test case: Color source nodes */ -PAG_TEST(PAGXTest, ColorSourceNodes) { - // Test SolidColorNode - auto solid = std::make_unique(); +PAG_TEST(PAGXTest, ColorSources) { + // Test SolidColor + auto solid = std::make_unique(); solid->color = pagx::Color::FromRGBA(1.0f, 0.0f, 0.0f, 1.0f); EXPECT_EQ(solid->type(), pagx::NodeType::SolidColor); EXPECT_FLOAT_EQ(solid->color.red, 1.0f); - // Test LinearGradientNode - auto linear = std::make_unique(); + // Test LinearGradient + auto linear = std::make_unique(); linear->startPoint.x = 0; linear->startPoint.y = 0; linear->endPoint.x = 100; linear->endPoint.y = 0; - pagx::ColorStopNode stop1; + pagx::ColorStop stop1; stop1.offset = 0; stop1.color = pagx::Color::FromRGBA(1.0f, 0.0f, 0.0f, 1.0f); - pagx::ColorStopNode stop2; + pagx::ColorStop stop2; stop2.offset = 1; stop2.color = pagx::Color::FromRGBA(0.0f, 0.0f, 1.0f, 1.0f); @@ -389,8 +388,8 @@ PAG_TEST(PAGXTest, ColorSourceNodes) { EXPECT_EQ(linear->type(), pagx::NodeType::LinearGradient); EXPECT_EQ(linear->colorStops.size(), 2u); - // Test RadialGradientNode - auto radial = std::make_unique(); + // Test RadialGradient + auto radial = std::make_unique(); radial->center.x = 50; radial->center.y = 50; radial->radius = 50; @@ -402,14 +401,14 @@ PAG_TEST(PAGXTest, ColorSourceNodes) { /** * Test case: Layer node with styles and filters */ -PAG_TEST(PAGXTest, LayerNodeStylesFilters) { - auto layer = std::make_unique(); +PAG_TEST(PAGXTest, LayerStylesFilters) { + auto layer = std::make_unique(); layer->name = "StyledLayer"; layer->alpha = 0.8f; layer->blendMode = pagx::BlendMode::Multiply; // Add drop shadow style - auto dropShadow = std::make_unique(); + auto dropShadow = std::make_unique(); dropShadow->offsetX = 5; dropShadow->offsetY = 5; dropShadow->blurrinessX = 10; @@ -418,7 +417,7 @@ PAG_TEST(PAGXTest, LayerNodeStylesFilters) { layer->styles.push_back(std::move(dropShadow)); // Add blur filter - auto blur = std::make_unique(); + auto blur = std::make_unique(); blur->blurrinessX = 5; blur->blurrinessY = 5; layer->filters.push_back(std::move(blur)); @@ -434,7 +433,7 @@ PAG_TEST(PAGXTest, LayerNodeStylesFilters) { */ PAG_TEST(PAGXTest, NodeClone) { // Test simple node clone - auto rect = std::make_unique(); + auto rect = std::make_unique(); rect->center.x = 50; rect->center.y = 50; rect->size.width = 100; @@ -444,18 +443,18 @@ PAG_TEST(PAGXTest, NodeClone) { ASSERT_TRUE(cloned != nullptr); EXPECT_EQ(cloned->type(), pagx::NodeType::Rectangle); - auto clonedRect = static_cast(cloned.get()); + auto clonedRect = static_cast(cloned.get()); EXPECT_FLOAT_EQ(clonedRect->center.x, 50); EXPECT_FLOAT_EQ(clonedRect->size.width, 100); // Test group with children clone - auto group = std::make_unique(); + auto group = std::make_unique(); group->name = "testGroup"; group->elements.push_back(std::move(rect)); auto clonedGroup = group->clone(); ASSERT_TRUE(clonedGroup != nullptr); - auto clonedGroupPtr = static_cast(clonedGroup.get()); + auto clonedGroupPtr = static_cast(clonedGroup.get()); EXPECT_EQ(clonedGroupPtr->name, "testGroup"); EXPECT_EQ(clonedGroupPtr->elements.size(), 1u); } From 5b5d4a6ddd2da49f39848fcc0207dc94076a3df7 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:23:47 +0800 Subject: [PATCH 066/678] Reorganize model directory into types/ and nodes/ subdirectories. - types/: Basic data types (Types.h), enums (Enums.h, enums/*.h) - nodes/: All node classes (Node, VectorElement, Layer, Group, etc.) - Fix RoundCorner.radius default value from 10 to 0 per spec --- pagx/include/pagx/LayerBuilder.h | 12 ++++- pagx/include/pagx/PAGXTypes.h | 4 +- pagx/include/pagx/PathData.h | 2 +- pagx/include/pagx/model/Model.h | 30 ++++++------- .../pagx/model/{ => nodes}/ColorSource.h | 10 ++--- .../include/pagx/model/{ => nodes}/Geometry.h | 10 ++--- pagx/include/pagx/model/{ => nodes}/Group.h | 4 +- pagx/include/pagx/model/{ => nodes}/Layer.h | 14 +++--- .../pagx/model/{ => nodes}/LayerFilter.h | 8 ++-- .../pagx/model/{ => nodes}/LayerStyle.h | 8 ++-- pagx/include/pagx/model/{ => nodes}/Node.h | 0 pagx/include/pagx/model/{ => nodes}/Painter.h | 16 +++---- .../include/pagx/model/{ => nodes}/Repeater.h | 6 +-- .../include/pagx/model/{ => nodes}/Resource.h | 2 +- .../pagx/model/{ => nodes}/ShapeModifier.h | 8 ++-- .../pagx/model/{ => nodes}/TextModifier.h | 20 ++++----- .../pagx/model/{ => nodes}/VectorElement.h | 2 +- pagx/include/pagx/model/{ => types}/Enums.h | 44 +++++++++---------- pagx/include/pagx/model/{ => types}/Types.h | 0 .../pagx/model/{ => types}/enums/BlendMode.h | 0 .../pagx/model/{ => types}/enums/FillRule.h | 0 .../pagx/model/{ => types}/enums/FontStyle.h | 0 .../pagx/model/{ => types}/enums/LineCap.h | 0 .../pagx/model/{ => types}/enums/LineJoin.h | 0 .../pagx/model/{ => types}/enums/MaskType.h | 0 .../model/{ => types}/enums/MergePathMode.h | 0 .../pagx/model/{ => types}/enums/Overflow.h | 0 .../pagx/model/{ => types}/enums/Placement.h | 0 .../model/{ => types}/enums/PolystarType.h | 0 .../model/{ => types}/enums/RepeaterOrder.h | 0 .../model/{ => types}/enums/SamplingMode.h | 0 .../model/{ => types}/enums/SelectorMode.h | 0 .../model/{ => types}/enums/SelectorShape.h | 0 .../model/{ => types}/enums/SelectorUnit.h | 0 .../model/{ => types}/enums/StrokeAlign.h | 0 .../pagx/model/{ => types}/enums/TextAlign.h | 0 .../pagx/model/{ => types}/enums/TextAnchor.h | 0 .../model/{ => types}/enums/TextPathAlign.h | 0 .../pagx/model/{ => types}/enums/TileMode.h | 0 .../pagx/model/{ => types}/enums/TrimType.h | 0 .../model/{ => types}/enums/VerticalAlign.h | 0 pagx/src/tgfx/LayerBuilder.cpp | 20 +++++++-- 42 files changed, 122 insertions(+), 98 deletions(-) rename pagx/include/pagx/model/{ => nodes}/ColorSource.h (94%) rename pagx/include/pagx/model/{ => nodes}/Geometry.h (93%) rename pagx/include/pagx/model/{ => nodes}/Group.h (96%) rename pagx/include/pagx/model/{ => nodes}/Layer.h (92%) rename pagx/include/pagx/model/{ => nodes}/LayerFilter.h (94%) rename pagx/include/pagx/model/{ => nodes}/LayerStyle.h (93%) rename pagx/include/pagx/model/{ => nodes}/Node.h (100%) rename pagx/include/pagx/model/{ => nodes}/Painter.h (89%) rename pagx/include/pagx/model/{ => nodes}/Repeater.h (91%) rename pagx/include/pagx/model/{ => nodes}/Resource.h (98%) rename pagx/include/pagx/model/{ => nodes}/ShapeModifier.h (92%) rename pagx/include/pagx/model/{ => nodes}/TextModifier.h (87%) rename pagx/include/pagx/model/{ => nodes}/VectorElement.h (97%) rename pagx/include/pagx/model/{ => types}/Enums.h (51%) rename pagx/include/pagx/model/{ => types}/Types.h (100%) rename pagx/include/pagx/model/{ => types}/enums/BlendMode.h (100%) rename pagx/include/pagx/model/{ => types}/enums/FillRule.h (100%) rename pagx/include/pagx/model/{ => types}/enums/FontStyle.h (100%) rename pagx/include/pagx/model/{ => types}/enums/LineCap.h (100%) rename pagx/include/pagx/model/{ => types}/enums/LineJoin.h (100%) rename pagx/include/pagx/model/{ => types}/enums/MaskType.h (100%) rename pagx/include/pagx/model/{ => types}/enums/MergePathMode.h (100%) rename pagx/include/pagx/model/{ => types}/enums/Overflow.h (100%) rename pagx/include/pagx/model/{ => types}/enums/Placement.h (100%) rename pagx/include/pagx/model/{ => types}/enums/PolystarType.h (100%) rename pagx/include/pagx/model/{ => types}/enums/RepeaterOrder.h (100%) rename pagx/include/pagx/model/{ => types}/enums/SamplingMode.h (100%) rename pagx/include/pagx/model/{ => types}/enums/SelectorMode.h (100%) rename pagx/include/pagx/model/{ => types}/enums/SelectorShape.h (100%) rename pagx/include/pagx/model/{ => types}/enums/SelectorUnit.h (100%) rename pagx/include/pagx/model/{ => types}/enums/StrokeAlign.h (100%) rename pagx/include/pagx/model/{ => types}/enums/TextAlign.h (100%) rename pagx/include/pagx/model/{ => types}/enums/TextAnchor.h (100%) rename pagx/include/pagx/model/{ => types}/enums/TextPathAlign.h (100%) rename pagx/include/pagx/model/{ => types}/enums/TileMode.h (100%) rename pagx/include/pagx/model/{ => types}/enums/TrimType.h (100%) rename pagx/include/pagx/model/{ => types}/enums/VerticalAlign.h (100%) diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index e8b0c64634..c84f09ecf2 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -25,6 +25,10 @@ #include "tgfx/core/Typeface.h" #include "tgfx/layers/Layer.h" +namespace tgfx { +class TextShaper; +} + namespace pagx { /** @@ -59,12 +63,18 @@ class LayerBuilder { */ std::vector> fallbackTypefaces; + /** + * Text shaper for text rendering with fallback support. + * If not provided, a default TextShaper will be created from fallbackTypefaces. + */ + std::shared_ptr textShaper; + /** * Base path for resolving relative resource paths. */ std::string basePath; - Options() : fallbackTypefaces(), basePath() { + Options() : fallbackTypefaces(), textShaper(nullptr), basePath() { } }; diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h index b953a63082..54c4bb9de8 100644 --- a/pagx/include/pagx/PAGXTypes.h +++ b/pagx/include/pagx/PAGXTypes.h @@ -18,5 +18,5 @@ #pragma once -#include "pagx/model/Enums.h" -#include "pagx/model/Types.h" +#include "pagx/model/types/Enums.h" +#include "pagx/model/types/Types.h" diff --git a/pagx/include/pagx/PathData.h b/pagx/include/pagx/PathData.h index 95dca5e26c..d2d8395666 100644 --- a/pagx/include/pagx/PathData.h +++ b/pagx/include/pagx/PathData.h @@ -20,7 +20,7 @@ #include #include -#include "pagx/model/Types.h" +#include "pagx/model/types/Types.h" namespace pagx { diff --git a/pagx/include/pagx/model/Model.h b/pagx/include/pagx/model/Model.h index b815accbe4..21a07d6117 100644 --- a/pagx/include/pagx/model/Model.h +++ b/pagx/include/pagx/model/Model.h @@ -19,31 +19,31 @@ #pragma once // Basic types and enums -#include "pagx/model/Enums.h" -#include "pagx/model/Types.h" +#include "pagx/model/types/Enums.h" +#include "pagx/model/types/Types.h" // Base classes -#include "pagx/model/Node.h" -#include "pagx/model/VectorElement.h" +#include "pagx/model/nodes/Node.h" +#include "pagx/model/nodes/VectorElement.h" // Color sources -#include "pagx/model/ColorSource.h" +#include "pagx/model/nodes/ColorSource.h" // Layer styles and filters -#include "pagx/model/LayerFilter.h" -#include "pagx/model/LayerStyle.h" +#include "pagx/model/nodes/LayerFilter.h" +#include "pagx/model/nodes/LayerStyle.h" // Vector elements -#include "pagx/model/Geometry.h" -#include "pagx/model/Group.h" -#include "pagx/model/Painter.h" -#include "pagx/model/Repeater.h" -#include "pagx/model/ShapeModifier.h" -#include "pagx/model/TextModifier.h" +#include "pagx/model/nodes/Geometry.h" +#include "pagx/model/nodes/Group.h" +#include "pagx/model/nodes/Painter.h" +#include "pagx/model/nodes/Repeater.h" +#include "pagx/model/nodes/ShapeModifier.h" +#include "pagx/model/nodes/TextModifier.h" // Resources and Layer -#include "pagx/model/Layer.h" -#include "pagx/model/Resource.h" +#include "pagx/model/nodes/Layer.h" +#include "pagx/model/nodes/Resource.h" namespace pagx { diff --git a/pagx/include/pagx/model/ColorSource.h b/pagx/include/pagx/model/nodes/ColorSource.h similarity index 94% rename from pagx/include/pagx/model/ColorSource.h rename to pagx/include/pagx/model/nodes/ColorSource.h index 3c86c329cc..06284a594a 100644 --- a/pagx/include/pagx/model/ColorSource.h +++ b/pagx/include/pagx/model/nodes/ColorSource.h @@ -21,11 +21,11 @@ #include #include #include -#include "pagx/model/Node.h" -#include "pagx/model/Resource.h" -#include "pagx/model/Types.h" -#include "pagx/model/enums/SamplingMode.h" -#include "pagx/model/enums/TileMode.h" +#include "pagx/model/nodes/Node.h" +#include "pagx/model/nodes/Resource.h" +#include "pagx/model/types/Types.h" +#include "pagx/model/types/enums/SamplingMode.h" +#include "pagx/model/types/enums/TileMode.h" namespace pagx { diff --git a/pagx/include/pagx/model/Geometry.h b/pagx/include/pagx/model/nodes/Geometry.h similarity index 93% rename from pagx/include/pagx/model/Geometry.h rename to pagx/include/pagx/model/nodes/Geometry.h index 7123d2637b..a3797dff9a 100644 --- a/pagx/include/pagx/model/Geometry.h +++ b/pagx/include/pagx/model/nodes/Geometry.h @@ -21,11 +21,11 @@ #include #include #include "pagx/PathData.h" -#include "pagx/model/Types.h" -#include "pagx/model/VectorElement.h" -#include "pagx/model/enums/FontStyle.h" -#include "pagx/model/enums/PolystarType.h" -#include "pagx/model/enums/TextAnchor.h" +#include "pagx/model/types/Types.h" +#include "pagx/model/nodes/VectorElement.h" +#include "pagx/model/types/enums/FontStyle.h" +#include "pagx/model/types/enums/PolystarType.h" +#include "pagx/model/types/enums/TextAnchor.h" namespace pagx { diff --git a/pagx/include/pagx/model/Group.h b/pagx/include/pagx/model/nodes/Group.h similarity index 96% rename from pagx/include/pagx/model/Group.h rename to pagx/include/pagx/model/nodes/Group.h index e7d491576a..6aba5aca25 100644 --- a/pagx/include/pagx/model/Group.h +++ b/pagx/include/pagx/model/nodes/Group.h @@ -21,8 +21,8 @@ #include #include #include -#include "pagx/model/Types.h" -#include "pagx/model/VectorElement.h" +#include "pagx/model/types/Types.h" +#include "pagx/model/nodes/VectorElement.h" namespace pagx { diff --git a/pagx/include/pagx/model/Layer.h b/pagx/include/pagx/model/nodes/Layer.h similarity index 92% rename from pagx/include/pagx/model/Layer.h rename to pagx/include/pagx/model/nodes/Layer.h index f332fee95f..159f750600 100644 --- a/pagx/include/pagx/model/Layer.h +++ b/pagx/include/pagx/model/nodes/Layer.h @@ -22,13 +22,13 @@ #include #include #include -#include "pagx/model/LayerFilter.h" -#include "pagx/model/LayerStyle.h" -#include "pagx/model/Node.h" -#include "pagx/model/Types.h" -#include "pagx/model/VectorElement.h" -#include "pagx/model/enums/BlendMode.h" -#include "pagx/model/enums/MaskType.h" +#include "pagx/model/nodes/LayerFilter.h" +#include "pagx/model/nodes/LayerStyle.h" +#include "pagx/model/nodes/Node.h" +#include "pagx/model/types/Types.h" +#include "pagx/model/nodes/VectorElement.h" +#include "pagx/model/types/enums/BlendMode.h" +#include "pagx/model/types/enums/MaskType.h" namespace pagx { diff --git a/pagx/include/pagx/model/LayerFilter.h b/pagx/include/pagx/model/nodes/LayerFilter.h similarity index 94% rename from pagx/include/pagx/model/LayerFilter.h rename to pagx/include/pagx/model/nodes/LayerFilter.h index d804094454..6e99bf4f45 100644 --- a/pagx/include/pagx/model/LayerFilter.h +++ b/pagx/include/pagx/model/nodes/LayerFilter.h @@ -20,10 +20,10 @@ #include #include -#include "pagx/model/Node.h" -#include "pagx/model/Types.h" -#include "pagx/model/enums/BlendMode.h" -#include "pagx/model/enums/TileMode.h" +#include "pagx/model/nodes/Node.h" +#include "pagx/model/types/Types.h" +#include "pagx/model/types/enums/BlendMode.h" +#include "pagx/model/types/enums/TileMode.h" namespace pagx { diff --git a/pagx/include/pagx/model/LayerStyle.h b/pagx/include/pagx/model/nodes/LayerStyle.h similarity index 93% rename from pagx/include/pagx/model/LayerStyle.h rename to pagx/include/pagx/model/nodes/LayerStyle.h index f64dbdf272..72b7b3f0f5 100644 --- a/pagx/include/pagx/model/LayerStyle.h +++ b/pagx/include/pagx/model/nodes/LayerStyle.h @@ -19,10 +19,10 @@ #pragma once #include -#include "pagx/model/Node.h" -#include "pagx/model/Types.h" -#include "pagx/model/enums/BlendMode.h" -#include "pagx/model/enums/TileMode.h" +#include "pagx/model/nodes/Node.h" +#include "pagx/model/types/Types.h" +#include "pagx/model/types/enums/BlendMode.h" +#include "pagx/model/types/enums/TileMode.h" namespace pagx { diff --git a/pagx/include/pagx/model/Node.h b/pagx/include/pagx/model/nodes/Node.h similarity index 100% rename from pagx/include/pagx/model/Node.h rename to pagx/include/pagx/model/nodes/Node.h diff --git a/pagx/include/pagx/model/Painter.h b/pagx/include/pagx/model/nodes/Painter.h similarity index 89% rename from pagx/include/pagx/model/Painter.h rename to pagx/include/pagx/model/nodes/Painter.h index 08d79d8c9d..0009c4ba62 100644 --- a/pagx/include/pagx/model/Painter.h +++ b/pagx/include/pagx/model/nodes/Painter.h @@ -21,14 +21,14 @@ #include #include #include -#include "pagx/model/ColorSource.h" -#include "pagx/model/VectorElement.h" -#include "pagx/model/enums/BlendMode.h" -#include "pagx/model/enums/FillRule.h" -#include "pagx/model/enums/LineCap.h" -#include "pagx/model/enums/LineJoin.h" -#include "pagx/model/enums/Placement.h" -#include "pagx/model/enums/StrokeAlign.h" +#include "pagx/model/nodes/ColorSource.h" +#include "pagx/model/nodes/VectorElement.h" +#include "pagx/model/types/enums/BlendMode.h" +#include "pagx/model/types/enums/FillRule.h" +#include "pagx/model/types/enums/LineCap.h" +#include "pagx/model/types/enums/LineJoin.h" +#include "pagx/model/types/enums/Placement.h" +#include "pagx/model/types/enums/StrokeAlign.h" namespace pagx { diff --git a/pagx/include/pagx/model/Repeater.h b/pagx/include/pagx/model/nodes/Repeater.h similarity index 91% rename from pagx/include/pagx/model/Repeater.h rename to pagx/include/pagx/model/nodes/Repeater.h index 144b3951f0..366aa191ff 100644 --- a/pagx/include/pagx/model/Repeater.h +++ b/pagx/include/pagx/model/nodes/Repeater.h @@ -19,9 +19,9 @@ #pragma once #include -#include "pagx/model/Types.h" -#include "pagx/model/VectorElement.h" -#include "pagx/model/enums/RepeaterOrder.h" +#include "pagx/model/types/Types.h" +#include "pagx/model/nodes/VectorElement.h" +#include "pagx/model/types/enums/RepeaterOrder.h" namespace pagx { diff --git a/pagx/include/pagx/model/Resource.h b/pagx/include/pagx/model/nodes/Resource.h similarity index 98% rename from pagx/include/pagx/model/Resource.h rename to pagx/include/pagx/model/nodes/Resource.h index 21451c7267..0dc10d1417 100644 --- a/pagx/include/pagx/model/Resource.h +++ b/pagx/include/pagx/model/nodes/Resource.h @@ -21,7 +21,7 @@ #include #include #include -#include "pagx/model/Node.h" +#include "pagx/model/nodes/Node.h" namespace pagx { diff --git a/pagx/include/pagx/model/ShapeModifier.h b/pagx/include/pagx/model/nodes/ShapeModifier.h similarity index 92% rename from pagx/include/pagx/model/ShapeModifier.h rename to pagx/include/pagx/model/nodes/ShapeModifier.h index 9325522c42..d4319ae7d3 100644 --- a/pagx/include/pagx/model/ShapeModifier.h +++ b/pagx/include/pagx/model/nodes/ShapeModifier.h @@ -19,9 +19,9 @@ #pragma once #include -#include "pagx/model/VectorElement.h" -#include "pagx/model/enums/MergePathMode.h" -#include "pagx/model/enums/TrimType.h" +#include "pagx/model/nodes/VectorElement.h" +#include "pagx/model/types/enums/MergePathMode.h" +#include "pagx/model/types/enums/TrimType.h" namespace pagx { @@ -47,7 +47,7 @@ struct TrimPath : public VectorElement { * Round corner modifier. */ struct RoundCorner : public VectorElement { - float radius = 10; + float radius = 0; NodeType type() const override { return NodeType::RoundCorner; diff --git a/pagx/include/pagx/model/TextModifier.h b/pagx/include/pagx/model/nodes/TextModifier.h similarity index 87% rename from pagx/include/pagx/model/TextModifier.h rename to pagx/include/pagx/model/nodes/TextModifier.h index a04476f38f..78d3c18bfd 100644 --- a/pagx/include/pagx/model/TextModifier.h +++ b/pagx/include/pagx/model/nodes/TextModifier.h @@ -21,16 +21,16 @@ #include #include #include -#include "pagx/model/Node.h" -#include "pagx/model/Types.h" -#include "pagx/model/VectorElement.h" -#include "pagx/model/enums/Overflow.h" -#include "pagx/model/enums/SelectorMode.h" -#include "pagx/model/enums/SelectorShape.h" -#include "pagx/model/enums/SelectorUnit.h" -#include "pagx/model/enums/TextAlign.h" -#include "pagx/model/enums/TextPathAlign.h" -#include "pagx/model/enums/VerticalAlign.h" +#include "pagx/model/nodes/Node.h" +#include "pagx/model/types/Types.h" +#include "pagx/model/nodes/VectorElement.h" +#include "pagx/model/types/enums/Overflow.h" +#include "pagx/model/types/enums/SelectorMode.h" +#include "pagx/model/types/enums/SelectorShape.h" +#include "pagx/model/types/enums/SelectorUnit.h" +#include "pagx/model/types/enums/TextAlign.h" +#include "pagx/model/types/enums/TextPathAlign.h" +#include "pagx/model/types/enums/VerticalAlign.h" namespace pagx { diff --git a/pagx/include/pagx/model/VectorElement.h b/pagx/include/pagx/model/nodes/VectorElement.h similarity index 97% rename from pagx/include/pagx/model/VectorElement.h rename to pagx/include/pagx/model/nodes/VectorElement.h index 906029ddd2..f2b2860977 100644 --- a/pagx/include/pagx/model/VectorElement.h +++ b/pagx/include/pagx/model/nodes/VectorElement.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/model/Node.h" +#include "pagx/model/nodes/Node.h" namespace pagx { diff --git a/pagx/include/pagx/model/Enums.h b/pagx/include/pagx/model/types/Enums.h similarity index 51% rename from pagx/include/pagx/model/Enums.h rename to pagx/include/pagx/model/types/Enums.h index c1b8ce40b5..7f5f562d28 100644 --- a/pagx/include/pagx/model/Enums.h +++ b/pagx/include/pagx/model/types/Enums.h @@ -19,37 +19,37 @@ #pragma once // Layer related -#include "pagx/model/enums/BlendMode.h" -#include "pagx/model/enums/MaskType.h" +#include "pagx/model/types/enums/BlendMode.h" +#include "pagx/model/types/enums/MaskType.h" // Painter related -#include "pagx/model/enums/FillRule.h" -#include "pagx/model/enums/LineCap.h" -#include "pagx/model/enums/LineJoin.h" -#include "pagx/model/enums/Placement.h" -#include "pagx/model/enums/StrokeAlign.h" +#include "pagx/model/types/enums/FillRule.h" +#include "pagx/model/types/enums/LineCap.h" +#include "pagx/model/types/enums/LineJoin.h" +#include "pagx/model/types/enums/Placement.h" +#include "pagx/model/types/enums/StrokeAlign.h" // Color source related -#include "pagx/model/enums/SamplingMode.h" -#include "pagx/model/enums/TileMode.h" +#include "pagx/model/types/enums/SamplingMode.h" +#include "pagx/model/types/enums/TileMode.h" // Geometry related -#include "pagx/model/enums/PolystarType.h" -#include "pagx/model/enums/TextAnchor.h" +#include "pagx/model/types/enums/PolystarType.h" +#include "pagx/model/types/enums/TextAnchor.h" // Shape modifier related -#include "pagx/model/enums/MergePathMode.h" -#include "pagx/model/enums/TrimType.h" +#include "pagx/model/types/enums/MergePathMode.h" +#include "pagx/model/types/enums/TrimType.h" // Text modifier related -#include "pagx/model/enums/FontStyle.h" -#include "pagx/model/enums/Overflow.h" -#include "pagx/model/enums/SelectorMode.h" -#include "pagx/model/enums/SelectorShape.h" -#include "pagx/model/enums/SelectorUnit.h" -#include "pagx/model/enums/TextAlign.h" -#include "pagx/model/enums/TextPathAlign.h" -#include "pagx/model/enums/VerticalAlign.h" +#include "pagx/model/types/enums/FontStyle.h" +#include "pagx/model/types/enums/Overflow.h" +#include "pagx/model/types/enums/SelectorMode.h" +#include "pagx/model/types/enums/SelectorShape.h" +#include "pagx/model/types/enums/SelectorUnit.h" +#include "pagx/model/types/enums/TextAlign.h" +#include "pagx/model/types/enums/TextPathAlign.h" +#include "pagx/model/types/enums/VerticalAlign.h" // Repeater related -#include "pagx/model/enums/RepeaterOrder.h" +#include "pagx/model/types/enums/RepeaterOrder.h" diff --git a/pagx/include/pagx/model/Types.h b/pagx/include/pagx/model/types/Types.h similarity index 100% rename from pagx/include/pagx/model/Types.h rename to pagx/include/pagx/model/types/Types.h diff --git a/pagx/include/pagx/model/enums/BlendMode.h b/pagx/include/pagx/model/types/enums/BlendMode.h similarity index 100% rename from pagx/include/pagx/model/enums/BlendMode.h rename to pagx/include/pagx/model/types/enums/BlendMode.h diff --git a/pagx/include/pagx/model/enums/FillRule.h b/pagx/include/pagx/model/types/enums/FillRule.h similarity index 100% rename from pagx/include/pagx/model/enums/FillRule.h rename to pagx/include/pagx/model/types/enums/FillRule.h diff --git a/pagx/include/pagx/model/enums/FontStyle.h b/pagx/include/pagx/model/types/enums/FontStyle.h similarity index 100% rename from pagx/include/pagx/model/enums/FontStyle.h rename to pagx/include/pagx/model/types/enums/FontStyle.h diff --git a/pagx/include/pagx/model/enums/LineCap.h b/pagx/include/pagx/model/types/enums/LineCap.h similarity index 100% rename from pagx/include/pagx/model/enums/LineCap.h rename to pagx/include/pagx/model/types/enums/LineCap.h diff --git a/pagx/include/pagx/model/enums/LineJoin.h b/pagx/include/pagx/model/types/enums/LineJoin.h similarity index 100% rename from pagx/include/pagx/model/enums/LineJoin.h rename to pagx/include/pagx/model/types/enums/LineJoin.h diff --git a/pagx/include/pagx/model/enums/MaskType.h b/pagx/include/pagx/model/types/enums/MaskType.h similarity index 100% rename from pagx/include/pagx/model/enums/MaskType.h rename to pagx/include/pagx/model/types/enums/MaskType.h diff --git a/pagx/include/pagx/model/enums/MergePathMode.h b/pagx/include/pagx/model/types/enums/MergePathMode.h similarity index 100% rename from pagx/include/pagx/model/enums/MergePathMode.h rename to pagx/include/pagx/model/types/enums/MergePathMode.h diff --git a/pagx/include/pagx/model/enums/Overflow.h b/pagx/include/pagx/model/types/enums/Overflow.h similarity index 100% rename from pagx/include/pagx/model/enums/Overflow.h rename to pagx/include/pagx/model/types/enums/Overflow.h diff --git a/pagx/include/pagx/model/enums/Placement.h b/pagx/include/pagx/model/types/enums/Placement.h similarity index 100% rename from pagx/include/pagx/model/enums/Placement.h rename to pagx/include/pagx/model/types/enums/Placement.h diff --git a/pagx/include/pagx/model/enums/PolystarType.h b/pagx/include/pagx/model/types/enums/PolystarType.h similarity index 100% rename from pagx/include/pagx/model/enums/PolystarType.h rename to pagx/include/pagx/model/types/enums/PolystarType.h diff --git a/pagx/include/pagx/model/enums/RepeaterOrder.h b/pagx/include/pagx/model/types/enums/RepeaterOrder.h similarity index 100% rename from pagx/include/pagx/model/enums/RepeaterOrder.h rename to pagx/include/pagx/model/types/enums/RepeaterOrder.h diff --git a/pagx/include/pagx/model/enums/SamplingMode.h b/pagx/include/pagx/model/types/enums/SamplingMode.h similarity index 100% rename from pagx/include/pagx/model/enums/SamplingMode.h rename to pagx/include/pagx/model/types/enums/SamplingMode.h diff --git a/pagx/include/pagx/model/enums/SelectorMode.h b/pagx/include/pagx/model/types/enums/SelectorMode.h similarity index 100% rename from pagx/include/pagx/model/enums/SelectorMode.h rename to pagx/include/pagx/model/types/enums/SelectorMode.h diff --git a/pagx/include/pagx/model/enums/SelectorShape.h b/pagx/include/pagx/model/types/enums/SelectorShape.h similarity index 100% rename from pagx/include/pagx/model/enums/SelectorShape.h rename to pagx/include/pagx/model/types/enums/SelectorShape.h diff --git a/pagx/include/pagx/model/enums/SelectorUnit.h b/pagx/include/pagx/model/types/enums/SelectorUnit.h similarity index 100% rename from pagx/include/pagx/model/enums/SelectorUnit.h rename to pagx/include/pagx/model/types/enums/SelectorUnit.h diff --git a/pagx/include/pagx/model/enums/StrokeAlign.h b/pagx/include/pagx/model/types/enums/StrokeAlign.h similarity index 100% rename from pagx/include/pagx/model/enums/StrokeAlign.h rename to pagx/include/pagx/model/types/enums/StrokeAlign.h diff --git a/pagx/include/pagx/model/enums/TextAlign.h b/pagx/include/pagx/model/types/enums/TextAlign.h similarity index 100% rename from pagx/include/pagx/model/enums/TextAlign.h rename to pagx/include/pagx/model/types/enums/TextAlign.h diff --git a/pagx/include/pagx/model/enums/TextAnchor.h b/pagx/include/pagx/model/types/enums/TextAnchor.h similarity index 100% rename from pagx/include/pagx/model/enums/TextAnchor.h rename to pagx/include/pagx/model/types/enums/TextAnchor.h diff --git a/pagx/include/pagx/model/enums/TextPathAlign.h b/pagx/include/pagx/model/types/enums/TextPathAlign.h similarity index 100% rename from pagx/include/pagx/model/enums/TextPathAlign.h rename to pagx/include/pagx/model/types/enums/TextPathAlign.h diff --git a/pagx/include/pagx/model/enums/TileMode.h b/pagx/include/pagx/model/types/enums/TileMode.h similarity index 100% rename from pagx/include/pagx/model/enums/TileMode.h rename to pagx/include/pagx/model/types/enums/TileMode.h diff --git a/pagx/include/pagx/model/enums/TrimType.h b/pagx/include/pagx/model/types/enums/TrimType.h similarity index 100% rename from pagx/include/pagx/model/enums/TrimType.h rename to pagx/include/pagx/model/types/enums/TrimType.h diff --git a/pagx/include/pagx/model/enums/VerticalAlign.h b/pagx/include/pagx/model/types/enums/VerticalAlign.h similarity index 100% rename from pagx/include/pagx/model/enums/VerticalAlign.h rename to pagx/include/pagx/model/types/enums/VerticalAlign.h diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 9aa947c1e6..eee0a099ef 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -46,6 +46,7 @@ #include "tgfx/layers/vectors/TextSpan.h" #include "tgfx/layers/vectors/TrimPath.h" #include "tgfx/layers/vectors/VectorGroup.h" +#include "tgfx/svg/TextShaper.h" namespace pagx { @@ -190,6 +191,12 @@ static tgfx::LineJoin ToTGFX(LineJoin join) { class LayerBuilderImpl { public: explicit LayerBuilderImpl(const LayerBuilder::Options& options) : _options(options) { + // Create text shaper from options or fallback typefaces. + if (_options.textShaper) { + _textShaper = _options.textShaper; + } else if (!_options.fallbackTypefaces.empty()) { + _textShaper = tgfx::TextShaper::Make(_options.fallbackTypefaces); + } } PAGXContent build(const PAGXDocument& document) { @@ -351,9 +358,15 @@ class LayerBuilderImpl { } float xOffset = 0; - if (typeface && !node->text.empty()) { - auto font = tgfx::Font(typeface, node->fontSize); - auto textBlob = tgfx::TextBlob::MakeFrom(node->text, font); + if (!node->text.empty()) { + std::shared_ptr textBlob = nullptr; + // Use TextShaper for fallback support (including emoji). + if (_textShaper) { + textBlob = _textShaper->shape(node->text, typeface, node->fontSize); + } else if (typeface) { + auto font = tgfx::Font(typeface, node->fontSize); + textBlob = tgfx::TextBlob::MakeFrom(node->text, font); + } textSpan->setTextBlob(textBlob); // Apply text-anchor offset based on text width. @@ -667,6 +680,7 @@ class LayerBuilderImpl { LayerBuilder::Options _options = {}; const std::vector>* _resources = nullptr; + std::shared_ptr _textShaper = nullptr; }; // Public API implementation From 52750a4e2bb7aea7ad2c0f1ec00384d498f03934 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:24:13 +0800 Subject: [PATCH 067/678] Remove redundant blend mode section from 4.4 in PAGX spec. --- pagx/docs/pagx_spec.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 6f62a7a5cd..e8f35c53b4 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -803,7 +803,7 @@ PAGX 文档采用层级结构组织内容: | 1 | ``` -### 4.4 Clipping, Masking and Compositing(裁剪、遮罩与合成) +### 4.4 Clipping and Masking(裁剪与遮罩) #### 4.4.1 scrollRect(滚动裁剪) @@ -843,10 +843,6 @@ PAGX 文档采用层级结构组织内容: - 遮罩图层自身不渲染(`visible` 属性被忽略) - 遮罩图层的变换不影响被遮罩图层 -#### 4.4.3 混合模式(Blend Modes) - -混合模式详见 4.1 BlendMode。 - --- ## 5. VectorElement System(矢量元素系统) From 484378277371fbc207980aff6cdd20f7e85822a9 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:37:09 +0800 Subject: [PATCH 068/678] Remove textAnchor attribute from TextSpan in PAGX spec. --- pagx/docs/pagx_spec.md | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index e8f35c53b4..344da6b60c 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1044,7 +1044,7 @@ y = center.y + outerRadius * sin(angle) 文本片段提供文本内容的几何形状。一个 TextSpan 经过塑形后会产生**字形列表**(多个字形),而非单一 Path。 ```xml - + ``` @@ -1059,22 +1059,12 @@ y = center.y + outerRadius * sin(angle) | `fontStyle` | enum | normal | normal 或 italic | | `tracking` | float | 0 | 字距 | | `baselineShift` | float | 0 | 基线偏移 | -| `textAnchor` | TextAnchor | start | 文本锚点(见下方) | - -**TextAnchor(文本锚点)**: - -| 值 | 说明 | -|------|------| -| `start` | 文本从 x 位置开始(默认) | -| `middle` | 文本以 x 位置为中心 | -| `end` | 文本以 x 位置为结束 | **处理流程**: 1. 根据 `font`、`fontSize`、`fontWeight`、`fontStyle` 查找字体 2. 应用 `tracking`(字距调整) 3. 将文本塑形(shaping)为字形列表 -4. 根据 `textAnchor` 计算水平偏移 -5. 按 `x`、`y` 位置和偏移放置 +4. 按 `x`、`y` 位置放置 **字体回退**:当指定字体不可用时,按平台默认字体回退链选择替代字体。 @@ -1822,7 +1812,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | 枚举 | 值 | 定义位置 | |------|------|----------| | **PolystarType** | `polygon`, `star` | 5.2.3 | -| **TextAnchor** | `start`, `middle`, `end` | 5.2.5 | ### A.4 修改器相关 @@ -2064,7 +2053,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `fontStyle` | enum | normal | | `tracking` | float | 0 | | `baselineShift` | float | 0 | -| `textAnchor` | TextAnchor | start | ### B.7 绘制器节点 From 8bd4b4b8a9b6d250a9de2e1ed2975cc020c441d3 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:40:01 +0800 Subject: [PATCH 069/678] Update PAGX spec to describe bidirectional conversion with PAG format. --- pagx/docs/pagx_spec.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 344da6b60c..359fdb69c1 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -16,11 +16,11 @@ - **生态兼容**:可作为 After Effects、Figma、腾讯设计等设计工具的通用交换格式,实现设计资产无缝流转。 -- **高效部署**:设计资产可一键导出并部署到研发环境,转换为二进制 PAG 格式后获得极高压缩比和运行时性能。 +- **高效部署**:设计资产可一键导出并部署到研发环境,与二进制 PAG 格式双向互转,获得极高压缩比和运行时性能。 ### 1.2 文件结构 -PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视频、音频、字体等),也支持通过数据 URI 内嵌资源。发布时可转换为内嵌所有资源的二进制 PAG 格式以优化加载性能。 +PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视频、音频、字体等),也支持通过数据 URI 内嵌资源。PAGX 与二进制 PAG 格式可双向互转:发布时转换为 PAG 以优化加载性能,调试时转换回 PAGX 以便阅读和编辑。 ### 1.3 文档组织 @@ -1772,9 +1772,11 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - 缺失的可选属性使用默认值 - 无效值尽可能回退到默认值 -### 6.2 转换为 PAG +### 6.2 格式互转 -将 PAGX 转换为 PAG 二进制格式时: +PAGX 与 PAG 二进制格式可双向无损转换。 + +**PAGX → PAG**: 1. 内嵌外部资源 2. 预排版文本(转换为字形坐标) @@ -1782,6 +1784,12 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: 4. 合成内联展开 5. 排版相关文本属性预计算为位置偏移 +**PAG → PAGX**: + +1. 提取内嵌资源为外部文件 +2. 还原图层结构和属性 +3. 重建合成引用关系 + --- ## Appendix A. Enumerations(枚举类型汇总) From 400643f85633ef32885a44b6add7304dc9baec16 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:47:43 +0800 Subject: [PATCH 070/678] Move PAG format conversion description to file structure section. --- pagx/docs/pagx_spec.md | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 359fdb69c1..f66f795935 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -16,11 +16,11 @@ - **生态兼容**:可作为 After Effects、Figma、腾讯设计等设计工具的通用交换格式,实现设计资产无缝流转。 -- **高效部署**:设计资产可一键导出并部署到研发环境,与二进制 PAG 格式双向互转,获得极高压缩比和运行时性能。 +- **高效部署**:设计资产可一键导出并部署到研发环境,转换为二进制 PAG 格式后获得极高压缩比和运行时性能。 ### 1.2 文件结构 -PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视频、音频、字体等),也支持通过数据 URI 内嵌资源。PAGX 与二进制 PAG 格式可双向互转:发布时转换为 PAG 以优化加载性能,调试时转换回 PAGX 以便阅读和编辑。 +PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视频、音频、字体等),也支持通过数据 URI 内嵌资源。PAGX 与二进制 PAG 格式可双向互转,二进制格式会内嵌所有外部资源。 ### 1.3 文档组织 @@ -1772,24 +1772,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - 缺失的可选属性使用默认值 - 无效值尽可能回退到默认值 -### 6.2 格式互转 - -PAGX 与 PAG 二进制格式可双向无损转换。 - -**PAGX → PAG**: - -1. 内嵌外部资源 -2. 预排版文本(转换为字形坐标) -3. 静态图层可合并优化 -4. 合成内联展开 -5. 排版相关文本属性预计算为位置偏移 - -**PAG → PAGX**: - -1. 提取内嵌资源为外部文件 -2. 还原图层结构和属性 -3. 重建合成引用关系 - --- ## Appendix A. Enumerations(枚举类型汇总) From 5057602f53074f33ca00d2d3f928fa520394c512 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:49:38 +0800 Subject: [PATCH 071/678] Clarify contents as Layer.contents in PAGX spec appendix. --- pagx/docs/pagx_spec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index f66f795935..daffc54cb8 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -2223,10 +2223,10 @@ pagx ### C.3 VectorElement 包含关系 -`contents` 和 `Group` 可包含以下 VectorElement: +`Layer.contents` 和 `Group` 可包含以下 VectorElement: ``` -contents / Group +Layer.contents / Group ├── Rectangle ├── Ellipse ├── Polystar From 47bf94f66581ec9ba29e06da7c74da30a87a92a5 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:57:05 +0800 Subject: [PATCH 072/678] Refactor PAGX spec appendices with XML examples and better organization. --- pagx/docs/pagx_spec.md | 683 ++++++++++++++++++++++++----------------- 1 file changed, 393 insertions(+), 290 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index daffc54cb8..f827b2f8de 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -20,7 +20,7 @@ ### 1.2 文件结构 -PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视频、音频、字体等),也支持通过数据 URI 内嵌资源。PAGX 与二进制 PAG 格式可双向互转,二进制格式会内嵌所有外部资源。 +PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视频、音频、字体等),也支持通过数据 URI 内嵌资源。PAGX 与二进制 PAG 格式可双向互转:发布时转换为 PAG 以优化加载性能,调试时转换回 PAGX 以便阅读和编辑。 ### 1.3 文档组织 @@ -33,10 +33,9 @@ PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视 **附录**(方便速查): -- **附录 A**:枚举类型汇总 -- **附录 B**:节点定义速查 -- **附录 C**:节点分类与包含关系 -- **附录 D**:完整示例 +- **附录 A**:节点层级与包含关系 +- **附录 B**:示例 +- **附录 C**:速查手册 --- @@ -1774,61 +1773,268 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: --- -## Appendix A. Enumerations(枚举类型汇总) +## Appendix A. Node Hierarchy(节点层级与包含关系) -本附录汇总规范中所有枚举类型,方便速查。 +本附录描述节点的分类和嵌套规则。 + +### A.1 节点分类 + +| 分类 | 节点 | +|------|------| +| **文档根** | `pagx` | +| **资源** | `Resources`, `Image`, `PathData`, `SolidColor`, `LinearGradient`, `RadialGradient`, `ConicGradient`, `DiamondGradient`, `ColorStop`, `ImagePattern`, `Composition` | +| **图层** | `Layer` | +| **图层样式** | `DropShadowStyle`, `InnerShadowStyle`, `BackgroundBlurStyle` | +| **滤镜** | `BlurFilter`, `DropShadowFilter`, `InnerShadowFilter`, `BlendFilter`, `ColorMatrixFilter` | +| **几何元素** | `Rectangle`, `Ellipse`, `Polystar`, `Path`, `TextSpan` | +| **绘制器** | `Fill`, `Stroke` | +| **形状修改器** | `TrimPath`, `RoundCorner`, `MergePath` | +| **文本修改器** | `TextModifier`, `RangeSelector`, `TextPath`, `TextLayout` | +| **其他** | `Repeater`, `Group` | + +### A.2 文档包含关系 + +``` +pagx +├── Resources +│ ├── Image +│ ├── PathData +│ ├── SolidColor +│ ├── LinearGradient → ColorStop* +│ ├── RadialGradient → ColorStop* +│ ├── ConicGradient → ColorStop* +│ ├── DiamondGradient → ColorStop* +│ ├── ImagePattern +│ └── Composition → Layer* +│ +└── Layer* + ├── contents + │ └── VectorElement*(见下方) + ├── styles + │ ├── DropShadowStyle + │ ├── InnerShadowStyle + │ └── BackgroundBlurStyle + ├── filters + │ ├── BlurFilter + │ ├── DropShadowFilter + │ ├── InnerShadowFilter + │ ├── BlendFilter + │ └── ColorMatrixFilter + └── Layer*(子图层) +``` + +### A.3 VectorElement 包含关系 + +`Layer.contents` 和 `Group` 可包含以下 VectorElement: -### A.1 图层相关 +``` +Layer.contents / Group +├── Rectangle +├── Ellipse +├── Polystar +├── Path +├── TextSpan +├── Fill(可内嵌颜色源) +│ └── SolidColor / LinearGradient / RadialGradient / ConicGradient / DiamondGradient / ImagePattern +├── Stroke(可内嵌颜色源) +│ └── SolidColor / LinearGradient / RadialGradient / ConicGradient / DiamondGradient / ImagePattern +├── TrimPath +├── RoundCorner +├── MergePath +├── TextModifier → RangeSelector* +├── TextPath +├── TextLayout +├── Repeater +└── Group*(递归) +``` -| 枚举 | 值 | 定义位置 | -|------|------|----------| -| **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter`, `plusDarker` | 4.1 | -| **MaskType** | `alpha`, `luminance`, `contour` | 4.1 | -| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | 2.12.8 | -| **SamplingMode** | `nearest`, `linear`, `mipmap` | 2.12.8 | +--- -### A.2 绘制器相关 +## Appendix B. Examples(示例) -| 枚举 | 值 | 定义位置 | -|------|------|----------| -| **FillRule** | `winding`, `evenOdd` | 5.3.1 | -| **LineCap** | `butt`, `round`, `square` | 5.3.2 | -| **LineJoin** | `miter`, `round`, `bevel` | 5.3.2 | -| **StrokeAlign** | `center`, `inside`, `outside` | 5.3.2 | -| **LayerPlacement** | `background`, `foreground` | 5.3.3 | +### B.1 完整示例 -### A.3 几何元素相关 +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` -| 枚举 | 值 | 定义位置 | -|------|------|----------| -| **PolystarType** | `polygon`, `star` | 5.2.3 | +### B.2 功能示例 -### A.4 修改器相关 +以下示例省略外层 ``、``、`` 等样板代码,仅展示核心片段。 -| 枚举 | 值 | 定义位置 | -|------|------|----------| -| **TrimType** | `separate`, `continuous` | 5.4.1 | -| **MergePathOp** | `append`, `union`, `intersect`, `xor`, `difference` | 5.4.3 | -| **SelectorUnit** | `index`, `percentage` | 5.5.4 | -| **SelectorShape** | `square`, `rampUp`, `rampDown`, `triangle`, `round`, `smooth` | 5.5.4 | -| **SelectorMode** | `add`, `subtract`, `intersect`, `min`, `max`, `difference` | 5.5.4 | -| **TextPathAlign** | `start`, `center`, `end` | 5.5.5 | -| **TextAlign** | `left`, `center`, `right`, `justify` | 5.5.6 | -| **VerticalAlign** | `top`, `center`, `bottom` | 5.5.6 | -| **Overflow** | `clip`, `visible`, `ellipsis` | 5.5.6 | -| **RepeaterOrder** | `belowOriginal`, `aboveOriginal` | 5.6 | +#### B.2.1 多重填充/描边 + +```xml + + + + + + + + + + + + +``` + +#### B.2.2 TrimPath 路径裁剪 + +```xml + + + + + + + + + + + +``` + +#### B.2.3 Repeater 阵列效果 + +```xml + + + + + + + + + + + +``` + +#### B.2.4 TextModifier 逐字变换 + +```xml + + + + + + + + + + + + + + + + + +``` + +#### B.2.5 TextLayout 富文本排版 + +```xml + + This is + bold + and + italic + text in a paragraph that will automatically wrap to fit the container width. + + + +``` + +#### B.2.6 TextPath 沿路径文本 + +```xml + + + + + + +``` --- -## Appendix B. Node Reference(节点定义速查) +## Appendix C. Quick Reference(速查手册) 本附录列出所有节点的属性定义,省略详细说明。 **注意**:`id` 和 `data-*` 属性为通用属性,可用于任意元素(见 2.3 节),各表中不再重复列出。 -### B.1 文档结构节点 +### C.1 文档结构节点 #### pagx + +`...` + | 属性 | 类型 | 默认值 | |------|------|--------| | `version` | string | (必填) | @@ -1836,29 +2042,44 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `height` | float | (必填) | #### Composition + +`...` + | 属性 | 类型 | 默认值 | |------|------|--------| | `width` | float | (必填) | | `height` | float | (必填) | -### B.2 资源节点 +### C.2 资源节点 #### Image + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `source` | string | (必填) | #### PathData + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `data` | string | (必填) | #### SolidColor + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `color` | color | (必填) | #### LinearGradient + +`...` + | 属性 | 类型 | 默认值 | |------|------|--------| | `startPoint` | point | (必填) | @@ -1866,6 +2087,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `matrix` | string | 单位矩阵 | #### RadialGradient + +`...` + | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | @@ -1873,6 +2097,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `matrix` | string | 单位矩阵 | #### ConicGradient + +`...` + | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | @@ -1881,6 +2108,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `matrix` | string | 单位矩阵 | #### DiamondGradient + +`...` + | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | @@ -1888,12 +2118,18 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `matrix` | string | 单位矩阵 | #### ColorStop + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `offset` | float | (必填) | | `color` | color | (必填) | #### ImagePattern + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `image` | idref | (必填) | @@ -1902,9 +2138,12 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `sampling` | SamplingMode | linear | | `matrix` | string | 单位矩阵 | -### B.3 图层节点 +### C.3 图层节点 #### Layer + +`...` + | 属性 | 类型 | 默认值 | |------|------|--------| | `name` | string | "" | @@ -1925,9 +2164,12 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `maskType` | MaskType | alpha | | `composition` | idref | - | -### B.4 图层样式节点 +### C.4 图层样式节点 #### DropShadowStyle + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `offsetX` | float | 0 | @@ -1939,6 +2181,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `blendMode` | BlendMode | normal | #### InnerShadowStyle + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `offsetX` | float | 0 | @@ -1949,6 +2194,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `blendMode` | BlendMode | normal | #### BackgroundBlurStyle + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `blurrinessX` | float | 0 | @@ -1956,9 +2204,12 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `tileMode` | TileMode | mirror | | `blendMode` | BlendMode | normal | -### B.5 滤镜节点 +### C.5 滤镜节点 #### BlurFilter + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `blurrinessX` | float | (必填) | @@ -1966,6 +2217,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `tileMode` | TileMode | decal | #### DropShadowFilter + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `offsetX` | float | 0 | @@ -1976,6 +2230,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `shadowOnly` | bool | false | #### InnerShadowFilter + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `offsetX` | float | 0 | @@ -1986,19 +2243,28 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `shadowOnly` | bool | false | #### BlendFilter + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `color` | color | (必填) | | `blendMode` | BlendMode | normal | #### ColorMatrixFilter + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `matrix` | string | (必填) | -### B.6 几何元素节点 +### C.6 几何元素节点 #### Rectangle + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | @@ -2007,6 +2273,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `reversed` | bool | false | #### Ellipse + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | @@ -2014,6 +2283,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `reversed` | bool | false | #### Polystar + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | @@ -2027,12 +2299,18 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `reversed` | bool | false | #### Path + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `data` | string/idref | (必填) | | `reversed` | bool | false | #### TextSpan + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `x` | float | 0 | @@ -2044,9 +2322,12 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `tracking` | float | 0 | | `baselineShift` | float | 0 | -### B.7 绘制器节点 +### C.7 绘制器节点 #### Fill + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `color` | color/idref | #000000 | @@ -2056,6 +2337,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `placement` | LayerPlacement | background | #### Stroke + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `color` | color/idref | #000000 | @@ -2070,9 +2354,12 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `align` | StrokeAlign | center | | `placement` | LayerPlacement | background | -### B.8 形状修改器节点 +### C.8 形状修改器节点 #### TrimPath + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `start` | float | 0 | @@ -2081,18 +2368,27 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `type` | TrimType | separate | #### RoundCorner + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `radius` | float | 10 | #### MergePath + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `mode` | MergePathOp | append | -### B.9 文本修改器节点 +### C.9 文本修改器节点 #### TextModifier + +`...` + | 属性 | 类型 | 默认值 | |------|------|--------| | `anchorPoint` | point | 0,0 | @@ -2107,6 +2403,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `strokeWidth` | float | - | #### RangeSelector + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `start` | float | 0 | @@ -2122,6 +2421,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `randomSeed` | int | 0 | #### TextPath + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `path` | idref | (必填) | @@ -2133,6 +2435,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `forceAlignment` | bool | false | #### TextLayout + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `width` | float | (必填) | @@ -2143,9 +2448,12 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `indent` | float | 0 | | `overflow` | Overflow | clip | -### B.10 其他节点 +### C.10 其他节点 #### Repeater + +`` + | 属性 | 类型 | 默认值 | |------|------|--------| | `copies` | float | 3 | @@ -2159,6 +2467,9 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `endAlpha` | float | 1 | #### Group + +`...` + | 属性 | 类型 | 默认值 | |------|------|--------| | `anchorPoint` | point | 0,0 | @@ -2169,252 +2480,44 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | `skewAxis` | float | 0 | | `alpha` | float | 1 | ---- - -## Appendix C. Node Hierarchy(节点分类与包含关系) +### C.11 枚举类型 -本附录描述节点的分类和嵌套规则。 - -### C.1 节点分类 +#### 图层相关 -| 分类 | 节点 | +| 枚举 | 值 | |------|------| -| **文档根** | `pagx` | -| **资源** | `Resources`, `Image`, `PathData`, `SolidColor`, `LinearGradient`, `RadialGradient`, `ConicGradient`, `DiamondGradient`, `ColorStop`, `ImagePattern`, `Composition` | -| **图层** | `Layer` | -| **图层样式** | `DropShadowStyle`, `InnerShadowStyle`, `BackgroundBlurStyle` | -| **滤镜** | `BlurFilter`, `DropShadowFilter`, `InnerShadowFilter`, `BlendFilter`, `ColorMatrixFilter` | -| **几何元素** | `Rectangle`, `Ellipse`, `Polystar`, `Path`, `TextSpan` | -| **绘制器** | `Fill`, `Stroke` | -| **形状修改器** | `TrimPath`, `RoundCorner`, `MergePath` | -| **文本修改器** | `TextModifier`, `RangeSelector`, `TextPath`, `TextLayout` | -| **其他** | `Repeater`, `Group` | - -### C.2 包含关系 - -``` -pagx -├── Resources -│ ├── Image -│ ├── PathData -│ ├── SolidColor -│ ├── LinearGradient → ColorStop* -│ ├── RadialGradient → ColorStop* -│ ├── ConicGradient → ColorStop* -│ ├── DiamondGradient → ColorStop* -│ ├── ImagePattern -│ └── Composition → Layer* -│ -└── Layer* - ├── contents - │ └── VectorElement*(见下方) - ├── styles - │ ├── DropShadowStyle - │ ├── InnerShadowStyle - │ └── BackgroundBlurStyle - ├── filters - │ ├── BlurFilter - │ ├── DropShadowFilter - │ ├── InnerShadowFilter - │ ├── BlendFilter - │ └── ColorMatrixFilter - └── Layer*(子图层) -``` - -### C.3 VectorElement 包含关系 - -`Layer.contents` 和 `Group` 可包含以下 VectorElement: - -``` -Layer.contents / Group -├── Rectangle -├── Ellipse -├── Polystar -├── Path -├── TextSpan -├── Fill(可内嵌颜色源) -│ └── SolidColor / LinearGradient / RadialGradient / ConicGradient / DiamondGradient / ImagePattern -├── Stroke(可内嵌颜色源) -│ └── SolidColor / LinearGradient / RadialGradient / ConicGradient / DiamondGradient / ImagePattern -├── TrimPath -├── RoundCorner -├── MergePath -├── TextModifier → RangeSelector* -├── TextPath -├── TextLayout -├── Repeater -└── Group*(递归) -``` - ---- - -## Appendix D. Examples(示例) - -### D.1 Complete Example(完整示例) - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### D.2 Feature Examples(功能示例) - -以下示例省略外层 ``、``、`` 等样板代码,仅展示核心片段。 - -#### D.2.1 多重填充/描边 - -```xml - - - - - - - - - - - - -``` - -#### D.2.2 TrimPath 路径裁剪 - -```xml - - - - - - - - - - - -``` - -#### D.2.3 Repeater 阵列效果 - -```xml - - - - - - - - - - - -``` - -#### D.2.4 TextModifier 逐字变换 +| **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter`, `plusDarker` | +| **MaskType** | `alpha`, `luminance`, `contour` | +| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | +| **SamplingMode** | `nearest`, `linear`, `mipmap` | -```xml - - - - - - - - +#### 绘制器相关 - - - - - - - - -``` +| 枚举 | 值 | +|------|------| +| **FillRule** | `winding`, `evenOdd` | +| **LineCap** | `butt`, `round`, `square` | +| **LineJoin** | `miter`, `round`, `bevel` | +| **StrokeAlign** | `center`, `inside`, `outside` | +| **LayerPlacement** | `background`, `foreground` | -#### D.2.5 TextLayout 富文本排版 +#### 几何元素相关 -```xml - - This is - bold - and - italic - text in a paragraph that will automatically wrap to fit the container width. - - - -``` +| 枚举 | 值 | +|------|------| +| **PolystarType** | `polygon`, `star` | -#### D.2.6 TextPath 沿路径文本 +#### 修改器相关 -```xml - - - - - - -``` +| 枚举 | 值 | +|------|------| +| **TrimType** | `separate`, `continuous` | +| **MergePathOp** | `append`, `union`, `intersect`, `xor`, `difference` | +| **SelectorUnit** | `index`, `percentage` | +| **SelectorShape** | `square`, `rampUp`, `rampDown`, `triangle`, `round`, `smooth` | +| **SelectorMode** | `add`, `subtract`, `intersect`, `min`, `max`, `difference` | +| **TextPathAlign** | `start`, `center`, `end` | +| **TextAlign** | `left`, `center`, `right`, `justify` | +| **VerticalAlign** | `top`, `center`, `bottom` | +| **Overflow** | `clip`, `visible`, `ellipsis` | +| **RepeaterOrder** | `belowOriginal`, `aboveOriginal` | From d6bc04933a964afa5ba1464e3096ca277310e06a Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 14:58:07 +0800 Subject: [PATCH 073/678] Rename Appendix C to Node and Attribute Reference in PAGX spec. --- pagx/docs/pagx_spec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index f827b2f8de..3e43af6cdb 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -35,7 +35,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视 - **附录 A**:节点层级与包含关系 - **附录 B**:示例 -- **附录 C**:速查手册 +- **附录 C**:节点与属性速查 --- @@ -2023,7 +2023,7 @@ Layer.contents / Group --- -## Appendix C. Quick Reference(速查手册) +## Appendix C. Node and Attribute Reference(节点与属性速查) 本附录列出所有节点的属性定义,省略详细说明。 From 1884e3125f52bc3ae2cf3beead901fcefcb5ef69 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 15:04:48 +0800 Subject: [PATCH 074/678] Update quick reference XML examples to use default values and code block format. --- pagx/docs/pagx_spec.md | 178 ++++++++++++++++++++++++++++++++--------- 1 file changed, 142 insertions(+), 36 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 3e43af6cdb..0288873012 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -2033,7 +2033,9 @@ Layer.contents / Group #### pagx -`...` +```xml +... +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2043,7 +2045,9 @@ Layer.contents / Group #### Composition -`...` +```xml +... +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2054,7 +2058,9 @@ Layer.contents / Group #### Image -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2062,7 +2068,9 @@ Layer.contents / Group #### PathData -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2070,7 +2078,9 @@ Layer.contents / Group #### SolidColor -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2078,7 +2088,12 @@ Layer.contents / Group #### LinearGradient -`...` +```xml + + + + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2088,7 +2103,12 @@ Layer.contents / Group #### RadialGradient -`...` +```xml + + + + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2098,7 +2118,12 @@ Layer.contents / Group #### ConicGradient -`...` +```xml + + + + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2109,7 +2134,12 @@ Layer.contents / Group #### DiamondGradient -`...` +```xml + + + + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2119,7 +2149,9 @@ Layer.contents / Group #### ColorStop -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2128,7 +2160,9 @@ Layer.contents / Group #### ImagePattern -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2142,7 +2176,13 @@ Layer.contents / Group #### Layer -`...` +```xml + + ... + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2168,7 +2208,10 @@ Layer.contents / Group #### DropShadowStyle -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2182,7 +2225,10 @@ Layer.contents / Group #### InnerShadowStyle -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2195,7 +2241,9 @@ Layer.contents / Group #### BackgroundBlurStyle -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2208,7 +2256,9 @@ Layer.contents / Group #### BlurFilter -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2218,7 +2268,10 @@ Layer.contents / Group #### DropShadowFilter -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2231,7 +2284,10 @@ Layer.contents / Group #### InnerShadowFilter -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2244,7 +2300,9 @@ Layer.contents / Group #### BlendFilter -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2253,7 +2311,9 @@ Layer.contents / Group #### ColorMatrixFilter -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2263,7 +2323,9 @@ Layer.contents / Group #### Rectangle -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2274,7 +2336,9 @@ Layer.contents / Group #### Ellipse -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2284,7 +2348,10 @@ Layer.contents / Group #### Polystar -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2300,7 +2367,9 @@ Layer.contents / Group #### Path -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2309,7 +2378,10 @@ Layer.contents / Group #### TextSpan -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2326,7 +2398,9 @@ Layer.contents / Group #### Fill -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2338,7 +2412,10 @@ Layer.contents / Group #### Stroke -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2358,7 +2435,9 @@ Layer.contents / Group #### TrimPath -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2369,7 +2448,9 @@ Layer.contents / Group #### RoundCorner -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2377,7 +2458,9 @@ Layer.contents / Group #### MergePath -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2387,7 +2470,12 @@ Layer.contents / Group #### TextModifier -`...` +```xml + + + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2404,7 +2492,11 @@ Layer.contents / Group #### RangeSelector -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2422,7 +2514,10 @@ Layer.contents / Group #### TextPath -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2436,7 +2531,10 @@ Layer.contents / Group #### TextLayout -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2452,7 +2550,10 @@ Layer.contents / Group #### Repeater -`` +```xml + +``` | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2468,7 +2569,12 @@ Layer.contents / Group #### Group -`...` +```xml + + ... + +``` | 属性 | 类型 | 默认值 | |------|------|--------| From 445131b2bc18f6a5e2a04970917bb5f1b2702c44 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 15:22:32 +0800 Subject: [PATCH 075/678] Remove clone() method from all node structs to simplify the API. --- pagx/include/pagx/nodes/BackgroundBlurStyle.h | 41 +++++++ pagx/include/pagx/nodes/BlendFilter.h | 39 ++++++ pagx/include/pagx/nodes/BlurFilter.h | 39 ++++++ pagx/include/pagx/nodes/ColorMatrixFilter.h | 37 ++++++ pagx/include/pagx/nodes/ColorStop.h | 38 ++++++ pagx/include/pagx/nodes/Composition.h | 45 +++++++ pagx/include/pagx/nodes/ConicGradient.h | 45 +++++++ pagx/include/pagx/nodes/DiamondGradient.h | 44 +++++++ pagx/include/pagx/nodes/DropShadowFilter.h | 42 +++++++ pagx/include/pagx/nodes/DropShadowStyle.h | 44 +++++++ pagx/include/pagx/nodes/Ellipse.h | 39 ++++++ pagx/include/pagx/nodes/Fill.h | 48 ++++++++ pagx/include/pagx/nodes/Group.h | 46 +++++++ pagx/include/pagx/nodes/Image.h | 38 ++++++ pagx/include/pagx/nodes/ImagePattern.h | 45 +++++++ pagx/include/pagx/nodes/InnerShadowFilter.h | 42 +++++++ pagx/include/pagx/nodes/InnerShadowStyle.h | 43 +++++++ pagx/include/pagx/nodes/Layer.h | 69 +++++++++++ pagx/include/pagx/nodes/LinearGradient.h | 44 +++++++ pagx/include/pagx/nodes/MergePath.h | 37 ++++++ pagx/include/pagx/nodes/Node.h | 113 ++++++++++++++++++ pagx/include/pagx/nodes/Path.h | 38 ++++++ pagx/include/pagx/nodes/PathDataResource.h | 38 ++++++ pagx/include/pagx/nodes/Polystar.h | 46 +++++++ pagx/include/pagx/nodes/RadialGradient.h | 44 +++++++ pagx/include/pagx/nodes/RangeSelector.h | 49 ++++++++ pagx/include/pagx/nodes/Rectangle.h | 40 +++++++ pagx/include/pagx/nodes/Repeater.h | 46 +++++++ pagx/include/pagx/nodes/RoundCorner.h | 36 ++++++ pagx/include/pagx/nodes/SolidColor.h | 39 ++++++ pagx/include/pagx/nodes/Stroke.h | 55 +++++++++ pagx/include/pagx/nodes/TextLayout.h | 45 +++++++ pagx/include/pagx/nodes/TextModifier.h | 50 ++++++++ pagx/include/pagx/nodes/TextPath.h | 44 +++++++ pagx/include/pagx/nodes/TextSpan.h | 46 +++++++ pagx/include/pagx/nodes/TrimPath.h | 40 +++++++ 36 files changed, 1634 insertions(+) create mode 100644 pagx/include/pagx/nodes/BackgroundBlurStyle.h create mode 100644 pagx/include/pagx/nodes/BlendFilter.h create mode 100644 pagx/include/pagx/nodes/BlurFilter.h create mode 100644 pagx/include/pagx/nodes/ColorMatrixFilter.h create mode 100644 pagx/include/pagx/nodes/ColorStop.h create mode 100644 pagx/include/pagx/nodes/Composition.h create mode 100644 pagx/include/pagx/nodes/ConicGradient.h create mode 100644 pagx/include/pagx/nodes/DiamondGradient.h create mode 100644 pagx/include/pagx/nodes/DropShadowFilter.h create mode 100644 pagx/include/pagx/nodes/DropShadowStyle.h create mode 100644 pagx/include/pagx/nodes/Ellipse.h create mode 100644 pagx/include/pagx/nodes/Fill.h create mode 100644 pagx/include/pagx/nodes/Group.h create mode 100644 pagx/include/pagx/nodes/Image.h create mode 100644 pagx/include/pagx/nodes/ImagePattern.h create mode 100644 pagx/include/pagx/nodes/InnerShadowFilter.h create mode 100644 pagx/include/pagx/nodes/InnerShadowStyle.h create mode 100644 pagx/include/pagx/nodes/Layer.h create mode 100644 pagx/include/pagx/nodes/LinearGradient.h create mode 100644 pagx/include/pagx/nodes/MergePath.h create mode 100644 pagx/include/pagx/nodes/Node.h create mode 100644 pagx/include/pagx/nodes/Path.h create mode 100644 pagx/include/pagx/nodes/PathDataResource.h create mode 100644 pagx/include/pagx/nodes/Polystar.h create mode 100644 pagx/include/pagx/nodes/RadialGradient.h create mode 100644 pagx/include/pagx/nodes/RangeSelector.h create mode 100644 pagx/include/pagx/nodes/Rectangle.h create mode 100644 pagx/include/pagx/nodes/Repeater.h create mode 100644 pagx/include/pagx/nodes/RoundCorner.h create mode 100644 pagx/include/pagx/nodes/SolidColor.h create mode 100644 pagx/include/pagx/nodes/Stroke.h create mode 100644 pagx/include/pagx/nodes/TextLayout.h create mode 100644 pagx/include/pagx/nodes/TextModifier.h create mode 100644 pagx/include/pagx/nodes/TextPath.h create mode 100644 pagx/include/pagx/nodes/TextSpan.h create mode 100644 pagx/include/pagx/nodes/TrimPath.h diff --git a/pagx/include/pagx/nodes/BackgroundBlurStyle.h b/pagx/include/pagx/nodes/BackgroundBlurStyle.h new file mode 100644 index 0000000000..1718f99e85 --- /dev/null +++ b/pagx/include/pagx/nodes/BackgroundBlurStyle.h @@ -0,0 +1,41 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/BlendMode.h" +#include "pagx/types/TileMode.h" + +namespace pagx { + +/** + * Background blur style. + */ +struct BackgroundBlurStyle : public Node { + float blurrinessX = 0; + float blurrinessY = 0; + TileMode tileMode = TileMode::Mirror; + BlendMode blendMode = BlendMode::Normal; + + NodeType type() const override { + return NodeType::BackgroundBlurStyle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/BlendFilter.h b/pagx/include/pagx/nodes/BlendFilter.h new file mode 100644 index 0000000000..36835a18f5 --- /dev/null +++ b/pagx/include/pagx/nodes/BlendFilter.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/BlendMode.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * Blend filter. + */ +struct BlendFilter : public Node { + Color color = {}; + BlendMode blendMode = BlendMode::Normal; + + NodeType type() const override { + return NodeType::BlendFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/BlurFilter.h b/pagx/include/pagx/nodes/BlurFilter.h new file mode 100644 index 0000000000..ed20482bed --- /dev/null +++ b/pagx/include/pagx/nodes/BlurFilter.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/TileMode.h" + +namespace pagx { + +/** + * Blur filter. + */ +struct BlurFilter : public Node { + float blurrinessX = 0; + float blurrinessY = 0; + TileMode tileMode = TileMode::Decal; + + NodeType type() const override { + return NodeType::BlurFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/ColorMatrixFilter.h b/pagx/include/pagx/nodes/ColorMatrixFilter.h new file mode 100644 index 0000000000..9c25ae7411 --- /dev/null +++ b/pagx/include/pagx/nodes/ColorMatrixFilter.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Color matrix filter. + */ +struct ColorMatrixFilter : public Node { + std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; + + NodeType type() const override { + return NodeType::ColorMatrixFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/ColorStop.h b/pagx/include/pagx/nodes/ColorStop.h new file mode 100644 index 0000000000..88a49587ba --- /dev/null +++ b/pagx/include/pagx/nodes/ColorStop.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * A color stop in a gradient. + */ +struct ColorStop : public Node { + float offset = 0; + Color color = {}; + + NodeType type() const override { + return NodeType::ColorStop; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Composition.h b/pagx/include/pagx/nodes/Composition.h new file mode 100644 index 0000000000..1dc74b6ac9 --- /dev/null +++ b/pagx/include/pagx/nodes/Composition.h @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/nodes/Node.h" + +namespace pagx { + +struct Layer; + +/** + * Composition resource. + */ +struct Composition : public Node { + std::string id = {}; + float width = 0; + float height = 0; + std::vector> layers = {}; + + NodeType type() const override { + return NodeType::Composition; + } +}; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/ConicGradient.h b/pagx/include/pagx/nodes/ConicGradient.h new file mode 100644 index 0000000000..7cd45c5083 --- /dev/null +++ b/pagx/include/pagx/nodes/ConicGradient.h @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/nodes/ColorStop.h" +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * A conic (sweep) gradient. + */ +struct ConicGradient : public Node { + std::string id = {}; + Point center = {}; + float startAngle = 0; + float endAngle = 360; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::ConicGradient; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/DiamondGradient.h b/pagx/include/pagx/nodes/DiamondGradient.h new file mode 100644 index 0000000000..76203115d8 --- /dev/null +++ b/pagx/include/pagx/nodes/DiamondGradient.h @@ -0,0 +1,44 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/nodes/ColorStop.h" +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * A diamond gradient. + */ +struct DiamondGradient : public Node { + std::string id = {}; + Point center = {}; + float halfDiagonal = 0; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::DiamondGradient; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/DropShadowFilter.h b/pagx/include/pagx/nodes/DropShadowFilter.h new file mode 100644 index 0000000000..4b65e264dc --- /dev/null +++ b/pagx/include/pagx/nodes/DropShadowFilter.h @@ -0,0 +1,42 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * Drop shadow filter. + */ +struct DropShadowFilter : public Node { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool shadowOnly = false; + + NodeType type() const override { + return NodeType::DropShadowFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/DropShadowStyle.h b/pagx/include/pagx/nodes/DropShadowStyle.h new file mode 100644 index 0000000000..ef6b2d83bf --- /dev/null +++ b/pagx/include/pagx/nodes/DropShadowStyle.h @@ -0,0 +1,44 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/BlendMode.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * Drop shadow style. + */ +struct DropShadowStyle : public Node { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool showBehindLayer = true; + BlendMode blendMode = BlendMode::Normal; + + NodeType type() const override { + return NodeType::DropShadowStyle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Ellipse.h b/pagx/include/pagx/nodes/Ellipse.h new file mode 100644 index 0000000000..f458116349 --- /dev/null +++ b/pagx/include/pagx/nodes/Ellipse.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * An ellipse shape. + */ +struct Ellipse : public Node { + Point center = {}; + Size size = {100, 100}; + bool reversed = false; + + NodeType type() const override { + return NodeType::Ellipse; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Fill.h b/pagx/include/pagx/nodes/Fill.h new file mode 100644 index 0000000000..db2a9df86c --- /dev/null +++ b/pagx/include/pagx/nodes/Fill.h @@ -0,0 +1,48 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/nodes/Node.h" +#include "pagx/types/BlendMode.h" +#include "pagx/types/FillRule.h" +#include "pagx/types/Placement.h" + +namespace pagx { + +/** + * A fill painter. + * The color can be a simple color string ("#FF0000"), a reference ("#gradientId"), + * or an inline color source node. + */ +struct Fill : public Node { + std::string color = {}; + std::unique_ptr colorSource = nullptr; + float alpha = 1; + BlendMode blendMode = BlendMode::Normal; + FillRule fillRule = FillRule::Winding; + Placement placement = Placement::Background; + + NodeType type() const override { + return NodeType::Fill; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Group.h b/pagx/include/pagx/nodes/Group.h new file mode 100644 index 0000000000..99b831dac8 --- /dev/null +++ b/pagx/include/pagx/nodes/Group.h @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * Group container. + */ +struct Group : public Node { + Point anchorPoint = {}; + Point position = {}; + float rotation = 0; + Point scale = {1, 1}; + float skew = 0; + float skewAxis = 0; + float alpha = 1; + std::vector> elements = {}; + + NodeType type() const override { + return NodeType::Group; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Image.h b/pagx/include/pagx/nodes/Image.h new file mode 100644 index 0000000000..a09e11f39f --- /dev/null +++ b/pagx/include/pagx/nodes/Image.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Image resource. + */ +struct Image : public Node { + std::string id = {}; + std::string source = {}; + + NodeType type() const override { + return NodeType::Image; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/ImagePattern.h b/pagx/include/pagx/nodes/ImagePattern.h new file mode 100644 index 0000000000..2457c916d1 --- /dev/null +++ b/pagx/include/pagx/nodes/ImagePattern.h @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/nodes/Node.h" +#include "pagx/types/SamplingMode.h" +#include "pagx/types/TileMode.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * An image pattern. + */ +struct ImagePattern : public Node { + std::string id = {}; + std::string image = {}; + TileMode tileModeX = TileMode::Clamp; + TileMode tileModeY = TileMode::Clamp; + SamplingMode sampling = SamplingMode::Linear; + Matrix matrix = {}; + + NodeType type() const override { + return NodeType::ImagePattern; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/InnerShadowFilter.h b/pagx/include/pagx/nodes/InnerShadowFilter.h new file mode 100644 index 0000000000..9a3ae2d50e --- /dev/null +++ b/pagx/include/pagx/nodes/InnerShadowFilter.h @@ -0,0 +1,42 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * Inner shadow filter. + */ +struct InnerShadowFilter : public Node { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool shadowOnly = false; + + NodeType type() const override { + return NodeType::InnerShadowFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/InnerShadowStyle.h b/pagx/include/pagx/nodes/InnerShadowStyle.h new file mode 100644 index 0000000000..dcfc84c8cb --- /dev/null +++ b/pagx/include/pagx/nodes/InnerShadowStyle.h @@ -0,0 +1,43 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/BlendMode.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * Inner shadow style. + */ +struct InnerShadowStyle : public Node { + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + BlendMode blendMode = BlendMode::Normal; + + NodeType type() const override { + return NodeType::InnerShadowStyle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Layer.h b/pagx/include/pagx/nodes/Layer.h new file mode 100644 index 0000000000..0c9229a3c4 --- /dev/null +++ b/pagx/include/pagx/nodes/Layer.h @@ -0,0 +1,69 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include "pagx/nodes/Node.h" +#include "pagx/types/BlendMode.h" +#include "pagx/types/MaskType.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * Layer node. + */ +struct Layer : public Node { + std::string id = {}; + std::string name = {}; + bool visible = true; + float alpha = 1; + BlendMode blendMode = BlendMode::Normal; + float x = 0; + float y = 0; + Matrix matrix = {}; + std::vector matrix3D = {}; + bool preserve3D = false; + bool antiAlias = true; + bool groupOpacity = false; + bool passThroughBackground = true; + bool excludeChildEffectsInLayerStyle = false; + Rect scrollRect = {}; + bool hasScrollRect = false; + std::string mask = {}; + MaskType maskType = MaskType::Alpha; + std::string composition = {}; + + std::vector> contents = {}; + std::vector> styles = {}; + std::vector> filters = {}; + std::vector> children = {}; + + // Custom data from SVG data-* attributes (key without "data-" prefix) + std::unordered_map customData = {}; + + NodeType type() const override { + return NodeType::Layer; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/LinearGradient.h b/pagx/include/pagx/nodes/LinearGradient.h new file mode 100644 index 0000000000..12ce673713 --- /dev/null +++ b/pagx/include/pagx/nodes/LinearGradient.h @@ -0,0 +1,44 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/nodes/ColorStop.h" +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * A linear gradient. + */ +struct LinearGradient : public Node { + std::string id = {}; + Point startPoint = {}; + Point endPoint = {}; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::LinearGradient; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/MergePath.h b/pagx/include/pagx/nodes/MergePath.h new file mode 100644 index 0000000000..2164df93bc --- /dev/null +++ b/pagx/include/pagx/nodes/MergePath.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/MergePathMode.h" + +namespace pagx { + +/** + * Merge path modifier. + */ +struct MergePath : public Node { + MergePathMode mode = MergePathMode::Append; + + NodeType type() const override { + return NodeType::MergePath; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Node.h b/pagx/include/pagx/nodes/Node.h new file mode 100644 index 0000000000..d3aa3779df --- /dev/null +++ b/pagx/include/pagx/nodes/Node.h @@ -0,0 +1,113 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Node types in PAGX document. + */ +enum class NodeType { + // Color sources + SolidColor, + LinearGradient, + RadialGradient, + ConicGradient, + DiamondGradient, + ImagePattern, + ColorStop, + + // Geometry elements + Rectangle, + Ellipse, + Polystar, + Path, + TextSpan, + + // Painters + Fill, + Stroke, + + // Shape modifiers + TrimPath, + RoundCorner, + MergePath, + + // Text modifiers + TextModifier, + TextPath, + TextLayout, + RangeSelector, + + // Repeater + Repeater, + + // Container + Group, + + // Layer styles + DropShadowStyle, + InnerShadowStyle, + BackgroundBlurStyle, + + // Layer filters + BlurFilter, + DropShadowFilter, + InnerShadowFilter, + BlendFilter, + ColorMatrixFilter, + + // Resources + Image, + PathData, + Composition, + + // Layer + Layer +}; + +/** + * Returns the string name of a node type. + */ +const char* NodeTypeName(NodeType type); + +/** + * Base class for all PAGX nodes. + */ +class Node { + public: + virtual ~Node() = default; + + /** + * Returns the type of this node. + */ + virtual NodeType type() const = 0; + + /** + * Creates a deep copy of this node. + */ + virtual std::unique_ptr clone() const = 0; + + protected: + Node() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Path.h b/pagx/include/pagx/nodes/Path.h new file mode 100644 index 0000000000..cba34e8131 --- /dev/null +++ b/pagx/include/pagx/nodes/Path.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/PathData.h" +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * A path shape. + */ +struct Path : public Node { + PathData data = {}; + bool reversed = false; + + NodeType type() const override { + return NodeType::Path; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/PathDataResource.h b/pagx/include/pagx/nodes/PathDataResource.h new file mode 100644 index 0000000000..10f73a0e6a --- /dev/null +++ b/pagx/include/pagx/nodes/PathDataResource.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * PathData resource - stores reusable path data. + */ +struct PathDataResource : public Node { + std::string id = {}; + std::string data = {}; // SVG path data string + + NodeType type() const override { + return NodeType::PathData; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Polystar.h b/pagx/include/pagx/nodes/Polystar.h new file mode 100644 index 0000000000..b2cccd9c53 --- /dev/null +++ b/pagx/include/pagx/nodes/Polystar.h @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/PolystarType.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * A polygon or star shape. + */ +struct Polystar : public Node { + Point center = {}; + PolystarType polystarType = PolystarType::Star; + float pointCount = 5; + float outerRadius = 100; + float innerRadius = 50; + float rotation = 0; + float outerRoundness = 0; + float innerRoundness = 0; + bool reversed = false; + + NodeType type() const override { + return NodeType::Polystar; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/RadialGradient.h b/pagx/include/pagx/nodes/RadialGradient.h new file mode 100644 index 0000000000..b6ddd322ee --- /dev/null +++ b/pagx/include/pagx/nodes/RadialGradient.h @@ -0,0 +1,44 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/nodes/ColorStop.h" +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * A radial gradient. + */ +struct RadialGradient : public Node { + std::string id = {}; + Point center = {}; + float radius = 0; + Matrix matrix = {}; + std::vector colorStops = {}; + + NodeType type() const override { + return NodeType::RadialGradient; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/RangeSelector.h b/pagx/include/pagx/nodes/RangeSelector.h new file mode 100644 index 0000000000..b2c88e1c1d --- /dev/null +++ b/pagx/include/pagx/nodes/RangeSelector.h @@ -0,0 +1,49 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/SelectorMode.h" +#include "pagx/types/SelectorShape.h" +#include "pagx/types/SelectorUnit.h" + +namespace pagx { + +/** + * Range selector for text modifier. + */ +struct RangeSelector : public Node { + float start = 0; + float end = 1; + float offset = 0; + SelectorUnit unit = SelectorUnit::Percentage; + SelectorShape shape = SelectorShape::Square; + float easeIn = 0; + float easeOut = 0; + SelectorMode mode = SelectorMode::Add; + float weight = 1; + bool randomizeOrder = false; + int randomSeed = 0; + + NodeType type() const override { + return NodeType::RangeSelector; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Rectangle.h b/pagx/include/pagx/nodes/Rectangle.h new file mode 100644 index 0000000000..448f3490fc --- /dev/null +++ b/pagx/include/pagx/nodes/Rectangle.h @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * A rectangle shape. + */ +struct Rectangle : public Node { + Point center = {}; + Size size = {100, 100}; + float roundness = 0; + bool reversed = false; + + NodeType type() const override { + return NodeType::Rectangle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Repeater.h b/pagx/include/pagx/nodes/Repeater.h new file mode 100644 index 0000000000..b51e3cf068 --- /dev/null +++ b/pagx/include/pagx/nodes/Repeater.h @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/RepeaterOrder.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * Repeater modifier. + */ +struct Repeater : public Node { + float copies = 3; + float offset = 0; + RepeaterOrder order = RepeaterOrder::BelowOriginal; + Point anchorPoint = {}; + Point position = {100, 100}; + float rotation = 0; + Point scale = {1, 1}; + float startAlpha = 1; + float endAlpha = 1; + + NodeType type() const override { + return NodeType::Repeater; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/RoundCorner.h b/pagx/include/pagx/nodes/RoundCorner.h new file mode 100644 index 0000000000..98e9b6e540 --- /dev/null +++ b/pagx/include/pagx/nodes/RoundCorner.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Round corner modifier. + */ +struct RoundCorner : public Node { + float radius = 10; + + NodeType type() const override { + return NodeType::RoundCorner; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/SolidColor.h b/pagx/include/pagx/nodes/SolidColor.h new file mode 100644 index 0000000000..2f314120dc --- /dev/null +++ b/pagx/include/pagx/nodes/SolidColor.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/nodes/Node.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * A solid color. + */ +struct SolidColor : public Node { + std::string id = {}; + Color color = {}; + + NodeType type() const override { + return NodeType::SolidColor; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Stroke.h b/pagx/include/pagx/nodes/Stroke.h new file mode 100644 index 0000000000..a38c6a99f0 --- /dev/null +++ b/pagx/include/pagx/nodes/Stroke.h @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/nodes/Node.h" +#include "pagx/types/BlendMode.h" +#include "pagx/types/LineCap.h" +#include "pagx/types/LineJoin.h" +#include "pagx/types/Placement.h" +#include "pagx/types/StrokeAlign.h" + +namespace pagx { + +/** + * A stroke painter. + */ +struct Stroke : public Node { + std::string color = {}; + std::unique_ptr colorSource = nullptr; + float width = 1; + float alpha = 1; + BlendMode blendMode = BlendMode::Normal; + LineCap cap = LineCap::Butt; + LineJoin join = LineJoin::Miter; + float miterLimit = 4; + std::vector dashes = {}; + float dashOffset = 0; + StrokeAlign align = StrokeAlign::Center; + Placement placement = Placement::Background; + + NodeType type() const override { + return NodeType::Stroke; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/TextLayout.h b/pagx/include/pagx/nodes/TextLayout.h new file mode 100644 index 0000000000..01471bdc59 --- /dev/null +++ b/pagx/include/pagx/nodes/TextLayout.h @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/Overflow.h" +#include "pagx/types/TextAlign.h" +#include "pagx/types/VerticalAlign.h" + +namespace pagx { + +/** + * Text layout modifier. + */ +struct TextLayout : public Node { + float width = 0; + float height = 0; + TextAlign textAlign = TextAlign::Left; + VerticalAlign verticalAlign = VerticalAlign::Top; + float lineHeight = 1.2f; + float indent = 0; + Overflow overflow = Overflow::Clip; + + NodeType type() const override { + return NodeType::TextLayout; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/TextModifier.h b/pagx/include/pagx/nodes/TextModifier.h new file mode 100644 index 0000000000..20c8d466be --- /dev/null +++ b/pagx/include/pagx/nodes/TextModifier.h @@ -0,0 +1,50 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/nodes/Node.h" +#include "pagx/nodes/RangeSelector.h" +#include "pagx/types/Types.h" + +namespace pagx { + +/** + * Text modifier. + */ +struct TextModifier : public Node { + Point anchorPoint = {}; + Point position = {}; + float rotation = 0; + Point scale = {1, 1}; + float skew = 0; + float skewAxis = 0; + float alpha = 1; + std::string fillColor = {}; + std::string strokeColor = {}; + float strokeWidth = -1; + std::vector rangeSelectors = {}; + + NodeType type() const override { + return NodeType::TextModifier; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/TextPath.h b/pagx/include/pagx/nodes/TextPath.h new file mode 100644 index 0000000000..c26030eb4a --- /dev/null +++ b/pagx/include/pagx/nodes/TextPath.h @@ -0,0 +1,44 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/nodes/Node.h" +#include "pagx/types/TextPathAlign.h" + +namespace pagx { + +/** + * Text path modifier. + */ +struct TextPath : public Node { + std::string path = {}; + TextPathAlign pathAlign = TextPathAlign::Start; + float firstMargin = 0; + float lastMargin = 0; + bool perpendicularToPath = true; + bool reversed = false; + bool forceAlignment = false; + + NodeType type() const override { + return NodeType::TextPath; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/TextSpan.h b/pagx/include/pagx/nodes/TextSpan.h new file mode 100644 index 0000000000..4fe16eed4b --- /dev/null +++ b/pagx/include/pagx/nodes/TextSpan.h @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/nodes/Node.h" +#include "pagx/types/FontStyle.h" + +namespace pagx { + +/** + * A text span. + */ +struct TextSpan : public Node { + float x = 0; + float y = 0; + std::string font = {}; + float fontSize = 12; + int fontWeight = 400; + FontStyle fontStyle = FontStyle::Normal; + float tracking = 0; + float baselineShift = 0; + std::string text = {}; + + NodeType type() const override { + return NodeType::TextSpan; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/TrimPath.h b/pagx/include/pagx/nodes/TrimPath.h new file mode 100644 index 0000000000..1c5acd9309 --- /dev/null +++ b/pagx/include/pagx/nodes/TrimPath.h @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" +#include "pagx/types/TrimType.h" + +namespace pagx { + +/** + * Trim path modifier. + */ +struct TrimPath : public Node { + float start = 0; + float end = 1; + float offset = 0; + TrimType trimType = TrimType::Separate; + + NodeType type() const override { + return NodeType::TrimPath; + } +}; + +} // namespace pagx From 624c57d239c5ff93920c859f3ef4a742dace1083 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 15:34:19 +0800 Subject: [PATCH 076/678] Refactor PAGX node structure and fix SVG mask rendering with gradient coordinate conversion. --- pagx/docs/pagx_spec.md | 190 ++---------------- pagx/include/pagx/PAGXDocument.h | 13 +- pagx/include/pagx/PAGXModel.h | 81 ++++++++ pagx/include/pagx/PAGXNode.h | 2 +- pagx/include/pagx/PAGXTypes.h | 4 +- pagx/include/pagx/PathData.h | 2 +- pagx/include/pagx/model/Model.h | 63 ------ pagx/include/pagx/model/nodes/ColorSource.h | 161 --------------- pagx/include/pagx/model/nodes/Geometry.h | 130 ------------ pagx/include/pagx/model/nodes/Group.h | 65 ------ pagx/include/pagx/model/nodes/Layer.h | 113 ----------- pagx/include/pagx/model/nodes/LayerFilter.h | 123 ------------ pagx/include/pagx/model/nodes/LayerStyle.h | 93 --------- pagx/include/pagx/model/nodes/Node.h | 113 ----------- pagx/include/pagx/model/nodes/Painter.h | 107 ---------- pagx/include/pagx/model/nodes/Resource.h | 84 -------- pagx/include/pagx/model/nodes/ShapeModifier.h | 76 ------- pagx/include/pagx/model/nodes/TextModifier.h | 129 ------------ pagx/include/pagx/model/nodes/VectorElement.h | 31 --- pagx/include/pagx/model/types/Enums.h | 55 ----- .../pagx/model/types/enums/TextAnchor.h | 37 ---- pagx/include/pagx/nodes/Composition.h | 1 - pagx/include/pagx/nodes/Node.h | 7 - .../{model/types/enums => types}/BlendMode.h | 0 .../{model/nodes/Repeater.h => types/Enums.h} | 65 +++--- .../{model/types/enums => types}/FillRule.h | 0 .../{model/types/enums => types}/FontStyle.h | 0 .../{model/types/enums => types}/LineCap.h | 0 .../{model/types/enums => types}/LineJoin.h | 0 .../{model/types/enums => types}/MaskType.h | 0 .../types/enums => types}/MergePathMode.h | 0 .../{model/types/enums => types}/Overflow.h | 0 .../{model/types/enums => types}/Placement.h | 0 .../types/enums => types}/PolystarType.h | 0 .../types/enums => types}/RepeaterOrder.h | 0 .../types/enums => types}/SamplingMode.h | 0 .../types/enums => types}/SelectorMode.h | 0 .../types/enums => types}/SelectorShape.h | 0 .../types/enums => types}/SelectorUnit.h | 0 .../types/enums => types}/StrokeAlign.h | 0 .../{model/types/enums => types}/TextAlign.h | 0 .../types/enums => types}/TextPathAlign.h | 0 .../{model/types/enums => types}/TileMode.h | 0 .../{model/types/enums => types}/TrimType.h | 0 pagx/include/pagx/{model => }/types/Types.h | 0 .../types/enums => types}/VerticalAlign.h | 0 pagx/src/PAGXDocument.cpp | 51 +++-- pagx/src/PAGXTypes.cpp | 5 - pagx/src/PAGXXMLParser.cpp | 19 +- pagx/src/PAGXXMLParser.h | 10 +- pagx/src/PAGXXMLWriter.cpp | 52 +++-- pagx/src/svg/PAGXSVGParser.cpp | 146 ++++++++------ pagx/src/svg/SVGParserInternal.h | 28 +-- pagx/src/tgfx/LayerBuilder.cpp | 76 +++++-- test/src/PAGXTest.cpp | 43 +--- 55 files changed, 362 insertions(+), 1813 deletions(-) create mode 100644 pagx/include/pagx/PAGXModel.h delete mode 100644 pagx/include/pagx/model/Model.h delete mode 100644 pagx/include/pagx/model/nodes/ColorSource.h delete mode 100644 pagx/include/pagx/model/nodes/Geometry.h delete mode 100644 pagx/include/pagx/model/nodes/Group.h delete mode 100644 pagx/include/pagx/model/nodes/Layer.h delete mode 100644 pagx/include/pagx/model/nodes/LayerFilter.h delete mode 100644 pagx/include/pagx/model/nodes/LayerStyle.h delete mode 100644 pagx/include/pagx/model/nodes/Node.h delete mode 100644 pagx/include/pagx/model/nodes/Painter.h delete mode 100644 pagx/include/pagx/model/nodes/Resource.h delete mode 100644 pagx/include/pagx/model/nodes/ShapeModifier.h delete mode 100644 pagx/include/pagx/model/nodes/TextModifier.h delete mode 100644 pagx/include/pagx/model/nodes/VectorElement.h delete mode 100644 pagx/include/pagx/model/types/Enums.h delete mode 100644 pagx/include/pagx/model/types/enums/TextAnchor.h rename pagx/include/pagx/{model/types/enums => types}/BlendMode.h (100%) rename pagx/include/pagx/{model/nodes/Repeater.h => types/Enums.h} (51%) rename pagx/include/pagx/{model/types/enums => types}/FillRule.h (100%) rename pagx/include/pagx/{model/types/enums => types}/FontStyle.h (100%) rename pagx/include/pagx/{model/types/enums => types}/LineCap.h (100%) rename pagx/include/pagx/{model/types/enums => types}/LineJoin.h (100%) rename pagx/include/pagx/{model/types/enums => types}/MaskType.h (100%) rename pagx/include/pagx/{model/types/enums => types}/MergePathMode.h (100%) rename pagx/include/pagx/{model/types/enums => types}/Overflow.h (100%) rename pagx/include/pagx/{model/types/enums => types}/Placement.h (100%) rename pagx/include/pagx/{model/types/enums => types}/PolystarType.h (100%) rename pagx/include/pagx/{model/types/enums => types}/RepeaterOrder.h (100%) rename pagx/include/pagx/{model/types/enums => types}/SamplingMode.h (100%) rename pagx/include/pagx/{model/types/enums => types}/SelectorMode.h (100%) rename pagx/include/pagx/{model/types/enums => types}/SelectorShape.h (100%) rename pagx/include/pagx/{model/types/enums => types}/SelectorUnit.h (100%) rename pagx/include/pagx/{model/types/enums => types}/StrokeAlign.h (100%) rename pagx/include/pagx/{model/types/enums => types}/TextAlign.h (100%) rename pagx/include/pagx/{model/types/enums => types}/TextPathAlign.h (100%) rename pagx/include/pagx/{model/types/enums => types}/TileMode.h (100%) rename pagx/include/pagx/{model/types/enums => types}/TrimType.h (100%) rename pagx/include/pagx/{model => }/types/Types.h (100%) rename pagx/include/pagx/{model/types/enums => types}/VerticalAlign.h (100%) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 0288873012..874f31cd47 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -2033,10 +2033,6 @@ Layer.contents / Group #### pagx -```xml -... -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `version` | string | (必填) | @@ -2045,10 +2041,6 @@ Layer.contents / Group #### Composition -```xml -... -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `width` | float | (必填) | @@ -2058,57 +2050,33 @@ Layer.contents / Group #### Image -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `source` | string | (必填) | #### PathData -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `data` | string | (必填) | #### SolidColor -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `color` | color | (必填) | #### LinearGradient -```xml - - - - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `startPoint` | point | (必填) | | `endPoint` | point | (必填) | | `matrix` | string | 单位矩阵 | -#### RadialGradient +子元素:`ColorStop`+ -```xml - - - - -``` +#### RadialGradient | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2116,14 +2084,9 @@ Layer.contents / Group | `radius` | float | (必填) | | `matrix` | string | 单位矩阵 | -#### ConicGradient +子元素:`ColorStop`+ -```xml - - - - -``` +#### ConicGradient | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2132,14 +2095,9 @@ Layer.contents / Group | `endAngle` | float | 360 | | `matrix` | string | 单位矩阵 | -#### DiamondGradient +子元素:`ColorStop`+ -```xml - - - - -``` +#### DiamondGradient | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2147,11 +2105,9 @@ Layer.contents / Group | `halfDiagonal` | float | (必填) | | `matrix` | string | 单位矩阵 | -#### ColorStop +子元素:`ColorStop`+ -```xml - -``` +#### ColorStop | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2160,10 +2116,6 @@ Layer.contents / Group #### ImagePattern -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `image` | idref | (必填) | @@ -2176,14 +2128,6 @@ Layer.contents / Group #### Layer -```xml - - ... - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `name` | string | "" | @@ -2208,11 +2152,6 @@ Layer.contents / Group #### DropShadowStyle -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `offsetX` | float | 0 | @@ -2225,11 +2164,6 @@ Layer.contents / Group #### InnerShadowStyle -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `offsetX` | float | 0 | @@ -2241,10 +2175,6 @@ Layer.contents / Group #### BackgroundBlurStyle -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `blurrinessX` | float | 0 | @@ -2256,10 +2186,6 @@ Layer.contents / Group #### BlurFilter -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `blurrinessX` | float | (必填) | @@ -2268,11 +2194,6 @@ Layer.contents / Group #### DropShadowFilter -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `offsetX` | float | 0 | @@ -2284,11 +2205,6 @@ Layer.contents / Group #### InnerShadowFilter -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `offsetX` | float | 0 | @@ -2300,10 +2216,6 @@ Layer.contents / Group #### BlendFilter -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `color` | color | (必填) | @@ -2311,10 +2223,6 @@ Layer.contents / Group #### ColorMatrixFilter -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `matrix` | string | (必填) | @@ -2323,10 +2231,6 @@ Layer.contents / Group #### Rectangle -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | @@ -2336,10 +2240,6 @@ Layer.contents / Group #### Ellipse -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | @@ -2348,11 +2248,6 @@ Layer.contents / Group #### Polystar -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | @@ -2367,10 +2262,6 @@ Layer.contents / Group #### Path -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `data` | string/idref | (必填) | @@ -2378,11 +2269,6 @@ Layer.contents / Group #### TextSpan -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `x` | float | 0 | @@ -2394,14 +2280,12 @@ Layer.contents / Group | `tracking` | float | 0 | | `baselineShift` | float | 0 | +内容:`CDATA` 文本 + ### C.7 绘制器节点 #### Fill -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `color` | color/idref | #000000 | @@ -2412,11 +2296,6 @@ Layer.contents / Group #### Stroke -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `color` | color/idref | #000000 | @@ -2435,10 +2314,6 @@ Layer.contents / Group #### TrimPath -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `start` | float | 0 | @@ -2448,20 +2323,12 @@ Layer.contents / Group #### RoundCorner -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `radius` | float | 10 | #### MergePath -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `mode` | MergePathOp | append | @@ -2470,13 +2337,6 @@ Layer.contents / Group #### TextModifier -```xml - - - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `anchorPoint` | point | 0,0 | @@ -2490,13 +2350,9 @@ Layer.contents / Group | `strokeColor` | color | - | | `strokeWidth` | float | - | -#### RangeSelector +子元素:`RangeSelector`* -```xml - -``` +#### RangeSelector | 属性 | 类型 | 默认值 | |------|------|--------| @@ -2514,11 +2370,6 @@ Layer.contents / Group #### TextPath -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `path` | idref | (必填) | @@ -2531,11 +2382,6 @@ Layer.contents / Group #### TextLayout -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `width` | float | (必填) | @@ -2550,11 +2396,6 @@ Layer.contents / Group #### Repeater -```xml - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `copies` | float | 3 | @@ -2569,13 +2410,6 @@ Layer.contents / Group #### Group -```xml - - ... - -``` - | 属性 | 类型 | 默认值 | |------|------|--------| | `anchorPoint` | point | 0,0 | diff --git a/pagx/include/pagx/PAGXDocument.h b/pagx/include/pagx/PAGXDocument.h index 580529cc91..a16eae283c 100644 --- a/pagx/include/pagx/PAGXDocument.h +++ b/pagx/include/pagx/PAGXDocument.h @@ -22,7 +22,7 @@ #include #include #include -#include "pagx/PAGXNode.h" +#include "pagx/PAGXModel.h" namespace pagx { @@ -54,7 +54,7 @@ class PAGXDocument { * Resources (images, gradients, compositions, etc.). * These can be referenced by "#id" in the document. */ - std::vector> resources = {}; + std::vector> resources = {}; /** * Top-level layers. @@ -94,16 +94,11 @@ class PAGXDocument { */ std::string toXML() const; - /** - * Returns a deep clone of this document. - */ - std::shared_ptr clone() const; - /** * Finds a resource by ID. * Returns nullptr if not found. */ - Resource* findResource(const std::string& id) const; + Node* findResource(const std::string& id) const; /** * Finds a layer by ID (searches recursively). @@ -115,7 +110,7 @@ class PAGXDocument { friend class PAGXXMLParser; PAGXDocument() = default; - mutable std::unordered_map resourceMap = {}; + mutable std::unordered_map resourceMap = {}; mutable bool resourceMapDirty = true; void rebuildResourceMap() const; diff --git a/pagx/include/pagx/PAGXModel.h b/pagx/include/pagx/PAGXModel.h new file mode 100644 index 0000000000..87b44c5293 --- /dev/null +++ b/pagx/include/pagx/PAGXModel.h @@ -0,0 +1,81 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// Basic types and enums +#include "pagx/types/Enums.h" +#include "pagx/types/Types.h" + +// Base class +#include "pagx/nodes/Node.h" + +// Color sources +#include "pagx/nodes/ColorStop.h" +#include "pagx/nodes/ConicGradient.h" +#include "pagx/nodes/DiamondGradient.h" +#include "pagx/nodes/ImagePattern.h" +#include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/RadialGradient.h" +#include "pagx/nodes/SolidColor.h" + +// Geometry elements +#include "pagx/nodes/Ellipse.h" +#include "pagx/nodes/Path.h" +#include "pagx/nodes/Polystar.h" +#include "pagx/nodes/Rectangle.h" +#include "pagx/nodes/TextSpan.h" + +// Painters +#include "pagx/nodes/Fill.h" +#include "pagx/nodes/Stroke.h" + +// Path modifiers +#include "pagx/nodes/MergePath.h" +#include "pagx/nodes/RoundCorner.h" +#include "pagx/nodes/TrimPath.h" + +// Text modifiers +#include "pagx/nodes/RangeSelector.h" +#include "pagx/nodes/TextLayout.h" +#include "pagx/nodes/TextModifier.h" +#include "pagx/nodes/TextPath.h" + +// Repeater and Group +#include "pagx/nodes/Group.h" +#include "pagx/nodes/Repeater.h" + +// Layer styles +#include "pagx/nodes/BackgroundBlurStyle.h" +#include "pagx/nodes/DropShadowStyle.h" +#include "pagx/nodes/InnerShadowStyle.h" + +// Layer filters +#include "pagx/nodes/BlendFilter.h" +#include "pagx/nodes/BlurFilter.h" +#include "pagx/nodes/ColorMatrixFilter.h" +#include "pagx/nodes/DropShadowFilter.h" +#include "pagx/nodes/InnerShadowFilter.h" + +// Resources +#include "pagx/nodes/Composition.h" +#include "pagx/nodes/Image.h" +#include "pagx/nodes/PathDataResource.h" + +// Layer +#include "pagx/nodes/Layer.h" diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index 4ecddd7909..bcc16fe4f3 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -18,4 +18,4 @@ #pragma once -#include "pagx/model/Model.h" +#include "pagx/PAGXModel.h" diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h index 54c4bb9de8..e6966ec81f 100644 --- a/pagx/include/pagx/PAGXTypes.h +++ b/pagx/include/pagx/PAGXTypes.h @@ -18,5 +18,5 @@ #pragma once -#include "pagx/model/types/Enums.h" -#include "pagx/model/types/Types.h" +#include "pagx/types/Enums.h" +#include "pagx/types/Types.h" diff --git a/pagx/include/pagx/PathData.h b/pagx/include/pagx/PathData.h index d2d8395666..d26d5873d6 100644 --- a/pagx/include/pagx/PathData.h +++ b/pagx/include/pagx/PathData.h @@ -20,7 +20,7 @@ #include #include -#include "pagx/model/types/Types.h" +#include "pagx/types/Types.h" namespace pagx { diff --git a/pagx/include/pagx/model/Model.h b/pagx/include/pagx/model/Model.h deleted file mode 100644 index 21a07d6117..0000000000 --- a/pagx/include/pagx/model/Model.h +++ /dev/null @@ -1,63 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -// Basic types and enums -#include "pagx/model/types/Enums.h" -#include "pagx/model/types/Types.h" - -// Base classes -#include "pagx/model/nodes/Node.h" -#include "pagx/model/nodes/VectorElement.h" - -// Color sources -#include "pagx/model/nodes/ColorSource.h" - -// Layer styles and filters -#include "pagx/model/nodes/LayerFilter.h" -#include "pagx/model/nodes/LayerStyle.h" - -// Vector elements -#include "pagx/model/nodes/Geometry.h" -#include "pagx/model/nodes/Group.h" -#include "pagx/model/nodes/Painter.h" -#include "pagx/model/nodes/Repeater.h" -#include "pagx/model/nodes/ShapeModifier.h" -#include "pagx/model/nodes/TextModifier.h" - -// Resources and Layer -#include "pagx/model/nodes/Layer.h" -#include "pagx/model/nodes/Resource.h" - -namespace pagx { - -// Implementation of Composition::clone (requires Layer to be fully defined) -inline std::unique_ptr Composition::clone() const { - auto node = std::make_unique(); - node->id = id; - node->width = width; - node->height = height; - for (const auto& layer : layers) { - node->layers.push_back( - std::unique_ptr(static_cast(layer->clone().release()))); - } - return node; -} - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/ColorSource.h b/pagx/include/pagx/model/nodes/ColorSource.h deleted file mode 100644 index 06284a594a..0000000000 --- a/pagx/include/pagx/model/nodes/ColorSource.h +++ /dev/null @@ -1,161 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include "pagx/model/nodes/Node.h" -#include "pagx/model/nodes/Resource.h" -#include "pagx/model/types/Types.h" -#include "pagx/model/types/enums/SamplingMode.h" -#include "pagx/model/types/enums/TileMode.h" - -namespace pagx { - -/** - * A color stop in a gradient. - */ -struct ColorStop : public Node { - float offset = 0; - Color color = {}; - - NodeType type() const override { - return NodeType::ColorStop; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Base class for color source nodes. - * Color sources can be stored as resources (with an id) or inline. - */ -class ColorSource : public Resource {}; - -/** - * A solid color. - */ -struct SolidColor : public ColorSource { - Color color = {}; - - NodeType type() const override { - return NodeType::SolidColor; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A linear gradient. - */ -struct LinearGradient : public ColorSource { - Point startPoint = {}; - Point endPoint = {}; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::LinearGradient; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A radial gradient. - */ -struct RadialGradient : public ColorSource { - Point center = {}; - float radius = 0; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::RadialGradient; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A conic (sweep) gradient. - */ -struct ConicGradient : public ColorSource { - Point center = {}; - float startAngle = 0; - float endAngle = 360; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::ConicGradient; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A diamond gradient. - */ -struct DiamondGradient : public ColorSource { - Point center = {}; - float halfDiagonal = 0; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::DiamondGradient; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * An image pattern. - */ -struct ImagePattern : public ColorSource { - std::string image = {}; - TileMode tileModeX = TileMode::Clamp; - TileMode tileModeY = TileMode::Clamp; - SamplingMode sampling = SamplingMode::Linear; - Matrix matrix = {}; - - NodeType type() const override { - return NodeType::ImagePattern; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/Geometry.h b/pagx/include/pagx/model/nodes/Geometry.h deleted file mode 100644 index a3797dff9a..0000000000 --- a/pagx/include/pagx/model/nodes/Geometry.h +++ /dev/null @@ -1,130 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/PathData.h" -#include "pagx/model/types/Types.h" -#include "pagx/model/nodes/VectorElement.h" -#include "pagx/model/types/enums/FontStyle.h" -#include "pagx/model/types/enums/PolystarType.h" -#include "pagx/model/types/enums/TextAnchor.h" - -namespace pagx { - -/** - * A rectangle shape. - */ -struct Rectangle : public VectorElement { - Point center = {}; - Size size = {100, 100}; - float roundness = 0; - bool reversed = false; - - NodeType type() const override { - return NodeType::Rectangle; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * An ellipse shape. - */ -struct Ellipse : public VectorElement { - Point center = {}; - Size size = {100, 100}; - bool reversed = false; - - NodeType type() const override { - return NodeType::Ellipse; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A polygon or star shape. - */ -struct Polystar : public VectorElement { - Point center = {}; - PolystarType polystarType = PolystarType::Star; - float pointCount = 5; - float outerRadius = 100; - float innerRadius = 50; - float rotation = 0; - float outerRoundness = 0; - float innerRoundness = 0; - bool reversed = false; - - NodeType type() const override { - return NodeType::Polystar; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A path shape. - */ -struct Path : public VectorElement { - PathData data = {}; - bool reversed = false; - - NodeType type() const override { - return NodeType::Path; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * A text span. - */ -struct TextSpan : public VectorElement { - float x = 0; - float y = 0; - std::string font = {}; - float fontSize = 12; - int fontWeight = 400; - FontStyle fontStyle = FontStyle::Normal; - float tracking = 0; - float baselineShift = 0; - TextAnchor textAnchor = TextAnchor::Start; - std::string text = {}; - - NodeType type() const override { - return NodeType::TextSpan; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/Group.h b/pagx/include/pagx/model/nodes/Group.h deleted file mode 100644 index 6aba5aca25..0000000000 --- a/pagx/include/pagx/model/nodes/Group.h +++ /dev/null @@ -1,65 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include "pagx/model/types/Types.h" -#include "pagx/model/nodes/VectorElement.h" - -namespace pagx { - -/** - * Group container. - */ -struct Group : public VectorElement { - std::string name = {}; - Point anchorPoint = {}; - Point position = {}; - float rotation = 0; - Point scale = {1, 1}; - float skew = 0; - float skewAxis = 0; - float alpha = 1; - std::vector> elements = {}; - - NodeType type() const override { - return NodeType::Group; - } - - std::unique_ptr clone() const override { - auto node = std::make_unique(); - node->name = name; - node->anchorPoint = anchorPoint; - node->position = position; - node->rotation = rotation; - node->scale = scale; - node->skew = skew; - node->skewAxis = skewAxis; - node->alpha = alpha; - for (const auto& element : elements) { - node->elements.push_back( - std::unique_ptr(static_cast(element->clone().release()))); - } - return node; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/Layer.h b/pagx/include/pagx/model/nodes/Layer.h deleted file mode 100644 index 159f750600..0000000000 --- a/pagx/include/pagx/model/nodes/Layer.h +++ /dev/null @@ -1,113 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include -#include "pagx/model/nodes/LayerFilter.h" -#include "pagx/model/nodes/LayerStyle.h" -#include "pagx/model/nodes/Node.h" -#include "pagx/model/types/Types.h" -#include "pagx/model/nodes/VectorElement.h" -#include "pagx/model/types/enums/BlendMode.h" -#include "pagx/model/types/enums/MaskType.h" - -namespace pagx { - -/** - * Layer node. - */ -struct Layer : public Node { - std::string id = {}; - std::string name = {}; - bool visible = true; - float alpha = 1; - BlendMode blendMode = BlendMode::Normal; - float x = 0; - float y = 0; - Matrix matrix = {}; - std::vector matrix3D = {}; - bool preserve3D = false; - bool antiAlias = true; - bool groupOpacity = false; - bool passThroughBackground = true; - bool excludeChildEffectsInLayerStyle = false; - Rect scrollRect = {}; - bool hasScrollRect = false; - std::string mask = {}; - MaskType maskType = MaskType::Alpha; - std::string composition = {}; - - std::vector> contents = {}; - std::vector> styles = {}; - std::vector> filters = {}; - std::vector> children = {}; - - // Custom data from SVG data-* attributes (key without "data-" prefix) - std::unordered_map customData = {}; - - NodeType type() const override { - return NodeType::Layer; - } - - std::unique_ptr clone() const override { - auto node = std::make_unique(); - node->id = id; - node->name = name; - node->visible = visible; - node->alpha = alpha; - node->blendMode = blendMode; - node->x = x; - node->y = y; - node->matrix = matrix; - node->matrix3D = matrix3D; - node->preserve3D = preserve3D; - node->antiAlias = antiAlias; - node->groupOpacity = groupOpacity; - node->passThroughBackground = passThroughBackground; - node->excludeChildEffectsInLayerStyle = excludeChildEffectsInLayerStyle; - node->scrollRect = scrollRect; - node->hasScrollRect = hasScrollRect; - node->mask = mask; - node->maskType = maskType; - node->composition = composition; - for (const auto& element : contents) { - node->contents.push_back( - std::unique_ptr(static_cast(element->clone().release()))); - } - for (const auto& style : styles) { - node->styles.push_back( - std::unique_ptr(static_cast(style->clone().release()))); - } - for (const auto& filter : filters) { - node->filters.push_back( - std::unique_ptr(static_cast(filter->clone().release()))); - } - for (const auto& child : children) { - node->children.push_back( - std::unique_ptr(static_cast(child->clone().release()))); - } - node->customData = customData; - return node; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/LayerFilter.h b/pagx/include/pagx/model/nodes/LayerFilter.h deleted file mode 100644 index 6e99bf4f45..0000000000 --- a/pagx/include/pagx/model/nodes/LayerFilter.h +++ /dev/null @@ -1,123 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/model/nodes/Node.h" -#include "pagx/model/types/Types.h" -#include "pagx/model/types/enums/BlendMode.h" -#include "pagx/model/types/enums/TileMode.h" - -namespace pagx { - -/** - * Base class for layer filter nodes. - */ -class LayerFilter : public Node {}; - -/** - * Blur filter. - */ -struct BlurFilter : public LayerFilter { - float blurrinessX = 0; - float blurrinessY = 0; - TileMode tileMode = TileMode::Decal; - - NodeType type() const override { - return NodeType::BlurFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Drop shadow filter. - */ -struct DropShadowFilter : public LayerFilter { - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - bool shadowOnly = false; - - NodeType type() const override { - return NodeType::DropShadowFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Inner shadow filter. - */ -struct InnerShadowFilter : public LayerFilter { - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - bool shadowOnly = false; - - NodeType type() const override { - return NodeType::InnerShadowFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Blend filter. - */ -struct BlendFilter : public LayerFilter { - Color color = {}; - BlendMode filterBlendMode = BlendMode::Normal; - - NodeType type() const override { - return NodeType::BlendFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Color matrix filter. - */ -struct ColorMatrixFilter : public LayerFilter { - std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; - - NodeType type() const override { - return NodeType::ColorMatrixFilter; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/LayerStyle.h b/pagx/include/pagx/model/nodes/LayerStyle.h deleted file mode 100644 index 72b7b3f0f5..0000000000 --- a/pagx/include/pagx/model/nodes/LayerStyle.h +++ /dev/null @@ -1,93 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include "pagx/model/nodes/Node.h" -#include "pagx/model/types/Types.h" -#include "pagx/model/types/enums/BlendMode.h" -#include "pagx/model/types/enums/TileMode.h" - -namespace pagx { - -/** - * Base class for layer style nodes. - */ -class LayerStyle : public Node { - public: - BlendMode blendMode = BlendMode::Normal; -}; - -/** - * Drop shadow style. - */ -struct DropShadowStyle : public LayerStyle { - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - bool showBehindLayer = true; - - NodeType type() const override { - return NodeType::DropShadowStyle; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Inner shadow style. - */ -struct InnerShadowStyle : public LayerStyle { - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - - NodeType type() const override { - return NodeType::InnerShadowStyle; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Background blur style. - */ -struct BackgroundBlurStyle : public LayerStyle { - float blurrinessX = 0; - float blurrinessY = 0; - TileMode tileMode = TileMode::Mirror; - - NodeType type() const override { - return NodeType::BackgroundBlurStyle; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/Node.h b/pagx/include/pagx/model/nodes/Node.h deleted file mode 100644 index ce51db8f04..0000000000 --- a/pagx/include/pagx/model/nodes/Node.h +++ /dev/null @@ -1,113 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Node types in PAGX document. - */ -enum class NodeType { - // Color sources - SolidColor, - LinearGradient, - RadialGradient, - ConicGradient, - DiamondGradient, - ImagePattern, - ColorStop, - - // Geometry elements - Rectangle, - Ellipse, - Polystar, - Path, - TextSpan, - - // Painters - Fill, - Stroke, - - // Shape modifiers - TrimPath, - RoundCorner, - MergePath, - - // Text modifiers - TextModifier, - TextPath, - TextLayout, - RangeSelector, - - // Repeater - Repeater, - - // Container - Group, - - // Layer styles - DropShadowStyle, - InnerShadowStyle, - BackgroundBlurStyle, - - // Layer filters - BlurFilter, - DropShadowFilter, - InnerShadowFilter, - BlendFilter, - ColorMatrixFilter, - - // Resources - Image, - PathData, - Composition, - - // Layer - Layer -}; - -/** - * Returns the string name of a node type. - */ -const char* NodeTypeName(NodeType type); - -/** - * Base class for all PAGX nodes. - */ -class Node { - public: - virtual ~Node() = default; - - /** - * Returns the type of this node. - */ - virtual NodeType type() const = 0; - - /** - * Returns a deep clone of this node. - */ - virtual std::unique_ptr clone() const = 0; - - protected: - Node() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/Painter.h b/pagx/include/pagx/model/nodes/Painter.h deleted file mode 100644 index 0009c4ba62..0000000000 --- a/pagx/include/pagx/model/nodes/Painter.h +++ /dev/null @@ -1,107 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include "pagx/model/nodes/ColorSource.h" -#include "pagx/model/nodes/VectorElement.h" -#include "pagx/model/types/enums/BlendMode.h" -#include "pagx/model/types/enums/FillRule.h" -#include "pagx/model/types/enums/LineCap.h" -#include "pagx/model/types/enums/LineJoin.h" -#include "pagx/model/types/enums/Placement.h" -#include "pagx/model/types/enums/StrokeAlign.h" - -namespace pagx { - -/** - * A fill painter. - * The color can be a simple color string ("#FF0000"), a reference ("#gradientId"), - * or an inline color source node. - */ -struct Fill : public VectorElement { - std::string color = {}; - std::unique_ptr colorSource = nullptr; - float alpha = 1; - BlendMode blendMode = BlendMode::Normal; - FillRule fillRule = FillRule::Winding; - Placement placement = Placement::Background; - - NodeType type() const override { - return NodeType::Fill; - } - - std::unique_ptr clone() const override { - auto node = std::make_unique(); - node->color = color; - if (colorSource) { - node->colorSource.reset(static_cast(colorSource->clone().release())); - } - node->alpha = alpha; - node->blendMode = blendMode; - node->fillRule = fillRule; - node->placement = placement; - return node; - } -}; - -/** - * A stroke painter. - */ -struct Stroke : public VectorElement { - std::string color = {}; - std::unique_ptr colorSource = nullptr; - float strokeWidth = 1; - float alpha = 1; - BlendMode blendMode = BlendMode::Normal; - LineCap cap = LineCap::Butt; - LineJoin join = LineJoin::Miter; - float miterLimit = 4; - std::vector dashes = {}; - float dashOffset = 0; - StrokeAlign align = StrokeAlign::Center; - Placement placement = Placement::Background; - - NodeType type() const override { - return NodeType::Stroke; - } - - std::unique_ptr clone() const override { - auto node = std::make_unique(); - node->color = color; - if (colorSource) { - node->colorSource.reset(static_cast(colorSource->clone().release())); - } - node->strokeWidth = strokeWidth; - node->alpha = alpha; - node->blendMode = blendMode; - node->cap = cap; - node->join = join; - node->miterLimit = miterLimit; - node->dashes = dashes; - node->dashOffset = dashOffset; - node->align = align; - node->placement = placement; - return node; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/Resource.h b/pagx/include/pagx/model/nodes/Resource.h deleted file mode 100644 index 0dc10d1417..0000000000 --- a/pagx/include/pagx/model/nodes/Resource.h +++ /dev/null @@ -1,84 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include "pagx/model/nodes/Node.h" - -namespace pagx { - -struct Layer; - -/** - * Base class for resource nodes. - * Resources are nodes that can be defined in the Resources section and referenced by id. - */ -class Resource : public Node { - public: - std::string id = {}; -}; - -/** - * Image resource. - */ -struct Image : public Resource { - std::string source = {}; - - NodeType type() const override { - return NodeType::Image; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * PathData resource - stores reusable path data. - */ -struct PathDataResource : public Resource { - std::string data = {}; // SVG path data string - - NodeType type() const override { - return NodeType::PathData; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Composition resource. - */ -struct Composition : public Resource { - float width = 0; - float height = 0; - std::vector> layers = {}; - - NodeType type() const override { - return NodeType::Composition; - } - - std::unique_ptr clone() const override; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/ShapeModifier.h b/pagx/include/pagx/model/nodes/ShapeModifier.h deleted file mode 100644 index d4319ae7d3..0000000000 --- a/pagx/include/pagx/model/nodes/ShapeModifier.h +++ /dev/null @@ -1,76 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include "pagx/model/nodes/VectorElement.h" -#include "pagx/model/types/enums/MergePathMode.h" -#include "pagx/model/types/enums/TrimType.h" - -namespace pagx { - -/** - * Trim path modifier. - */ -struct TrimPath : public VectorElement { - float start = 0; - float end = 1; - float offset = 0; - TrimType trimType = TrimType::Separate; - - NodeType type() const override { - return NodeType::TrimPath; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Round corner modifier. - */ -struct RoundCorner : public VectorElement { - float radius = 0; - - NodeType type() const override { - return NodeType::RoundCorner; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Merge path modifier. - */ -struct MergePath : public VectorElement { - MergePathMode mode = MergePathMode::Append; - - NodeType type() const override { - return NodeType::MergePath; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/TextModifier.h b/pagx/include/pagx/model/nodes/TextModifier.h deleted file mode 100644 index 78d3c18bfd..0000000000 --- a/pagx/include/pagx/model/nodes/TextModifier.h +++ /dev/null @@ -1,129 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include "pagx/model/nodes/Node.h" -#include "pagx/model/types/Types.h" -#include "pagx/model/nodes/VectorElement.h" -#include "pagx/model/types/enums/Overflow.h" -#include "pagx/model/types/enums/SelectorMode.h" -#include "pagx/model/types/enums/SelectorShape.h" -#include "pagx/model/types/enums/SelectorUnit.h" -#include "pagx/model/types/enums/TextAlign.h" -#include "pagx/model/types/enums/TextPathAlign.h" -#include "pagx/model/types/enums/VerticalAlign.h" - -namespace pagx { - -/** - * Range selector for text modifier. - */ -struct RangeSelector : public Node { - float start = 0; - float end = 1; - float offset = 0; - SelectorUnit unit = SelectorUnit::Percentage; - SelectorShape shape = SelectorShape::Square; - float easeIn = 0; - float easeOut = 0; - SelectorMode mode = SelectorMode::Add; - float weight = 1; - bool randomizeOrder = false; - int randomSeed = 0; - - NodeType type() const override { - return NodeType::RangeSelector; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Text modifier. - */ -struct TextModifier : public VectorElement { - Point anchorPoint = {}; - Point position = {}; - float rotation = 0; - Point scale = {1, 1}; - float skew = 0; - float skewAxis = 0; - float alpha = 1; - std::string fillColor = {}; - std::string strokeColor = {}; - float strokeWidth = -1; - std::vector rangeSelectors = {}; - - NodeType type() const override { - return NodeType::TextModifier; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Text path modifier. - */ -struct TextPath : public VectorElement { - std::string path = {}; - TextPathAlign textPathAlign = TextPathAlign::Start; - float firstMargin = 0; - float lastMargin = 0; - bool perpendicularToPath = true; - bool reversed = false; - bool forceAlignment = false; - - NodeType type() const override { - return NodeType::TextPath; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -/** - * Text layout modifier. - */ -struct TextLayout : public VectorElement { - float width = 0; - float height = 0; - TextAlign textAlign = TextAlign::Left; - VerticalAlign verticalAlign = VerticalAlign::Top; - float lineHeight = 1.2f; - float indent = 0; - Overflow overflow = Overflow::Clip; - - NodeType type() const override { - return NodeType::TextLayout; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/nodes/VectorElement.h b/pagx/include/pagx/model/nodes/VectorElement.h deleted file mode 100644 index f2b2860977..0000000000 --- a/pagx/include/pagx/model/nodes/VectorElement.h +++ /dev/null @@ -1,31 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/model/nodes/Node.h" - -namespace pagx { - -/** - * Base class for vector element nodes. - * VectorElements are nodes that can appear inside layer contents. - */ -class VectorElement : public Node {}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/Enums.h b/pagx/include/pagx/model/types/Enums.h deleted file mode 100644 index 7f5f562d28..0000000000 --- a/pagx/include/pagx/model/types/Enums.h +++ /dev/null @@ -1,55 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -// Layer related -#include "pagx/model/types/enums/BlendMode.h" -#include "pagx/model/types/enums/MaskType.h" - -// Painter related -#include "pagx/model/types/enums/FillRule.h" -#include "pagx/model/types/enums/LineCap.h" -#include "pagx/model/types/enums/LineJoin.h" -#include "pagx/model/types/enums/Placement.h" -#include "pagx/model/types/enums/StrokeAlign.h" - -// Color source related -#include "pagx/model/types/enums/SamplingMode.h" -#include "pagx/model/types/enums/TileMode.h" - -// Geometry related -#include "pagx/model/types/enums/PolystarType.h" -#include "pagx/model/types/enums/TextAnchor.h" - -// Shape modifier related -#include "pagx/model/types/enums/MergePathMode.h" -#include "pagx/model/types/enums/TrimType.h" - -// Text modifier related -#include "pagx/model/types/enums/FontStyle.h" -#include "pagx/model/types/enums/Overflow.h" -#include "pagx/model/types/enums/SelectorMode.h" -#include "pagx/model/types/enums/SelectorShape.h" -#include "pagx/model/types/enums/SelectorUnit.h" -#include "pagx/model/types/enums/TextAlign.h" -#include "pagx/model/types/enums/TextPathAlign.h" -#include "pagx/model/types/enums/VerticalAlign.h" - -// Repeater related -#include "pagx/model/types/enums/RepeaterOrder.h" diff --git a/pagx/include/pagx/model/types/enums/TextAnchor.h b/pagx/include/pagx/model/types/enums/TextAnchor.h deleted file mode 100644 index 654298aab6..0000000000 --- a/pagx/include/pagx/model/types/enums/TextAnchor.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Text anchor for horizontal alignment. - */ -enum class TextAnchor { - Start, - Middle, - End -}; - -std::string TextAnchorToString(TextAnchor anchor); -TextAnchor TextAnchorFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Composition.h b/pagx/include/pagx/nodes/Composition.h index 1dc74b6ac9..78da834577 100644 --- a/pagx/include/pagx/nodes/Composition.h +++ b/pagx/include/pagx/nodes/Composition.h @@ -40,6 +40,5 @@ struct Composition : public Node { return NodeType::Composition; } }; -}; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Node.h b/pagx/include/pagx/nodes/Node.h index d3aa3779df..38b64d9185 100644 --- a/pagx/include/pagx/nodes/Node.h +++ b/pagx/include/pagx/nodes/Node.h @@ -18,8 +18,6 @@ #pragma once -#include - namespace pagx { /** @@ -101,11 +99,6 @@ class Node { */ virtual NodeType type() const = 0; - /** - * Creates a deep copy of this node. - */ - virtual std::unique_ptr clone() const = 0; - protected: Node() = default; }; diff --git a/pagx/include/pagx/model/types/enums/BlendMode.h b/pagx/include/pagx/types/BlendMode.h similarity index 100% rename from pagx/include/pagx/model/types/enums/BlendMode.h rename to pagx/include/pagx/types/BlendMode.h diff --git a/pagx/include/pagx/model/nodes/Repeater.h b/pagx/include/pagx/types/Enums.h similarity index 51% rename from pagx/include/pagx/model/nodes/Repeater.h rename to pagx/include/pagx/types/Enums.h index 366aa191ff..1fbc598ac2 100644 --- a/pagx/include/pagx/model/nodes/Repeater.h +++ b/pagx/include/pagx/types/Enums.h @@ -18,34 +18,37 @@ #pragma once -#include -#include "pagx/model/types/Types.h" -#include "pagx/model/nodes/VectorElement.h" -#include "pagx/model/types/enums/RepeaterOrder.h" - -namespace pagx { - -/** - * Repeater modifier. - */ -struct Repeater : public VectorElement { - float copies = 3; - float offset = 0; - RepeaterOrder order = RepeaterOrder::BelowOriginal; - Point anchorPoint = {}; - Point position = {100, 100}; - float rotation = 0; - Point scale = {1, 1}; - float startAlpha = 1; - float endAlpha = 1; - - NodeType type() const override { - return NodeType::Repeater; - } - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } -}; - -} // namespace pagx +// Layer related +#include "pagx/types/BlendMode.h" +#include "pagx/types/MaskType.h" + +// Painter related +#include "pagx/types/FillRule.h" +#include "pagx/types/LineCap.h" +#include "pagx/types/LineJoin.h" +#include "pagx/types/Placement.h" +#include "pagx/types/StrokeAlign.h" + +// Color source related +#include "pagx/types/SamplingMode.h" +#include "pagx/types/TileMode.h" + +// Geometry related +#include "pagx/types/PolystarType.h" + +// Path modifier related +#include "pagx/types/MergePathMode.h" +#include "pagx/types/TrimType.h" + +// Text modifier related +#include "pagx/types/FontStyle.h" +#include "pagx/types/Overflow.h" +#include "pagx/types/SelectorMode.h" +#include "pagx/types/SelectorShape.h" +#include "pagx/types/SelectorUnit.h" +#include "pagx/types/TextAlign.h" +#include "pagx/types/TextPathAlign.h" +#include "pagx/types/VerticalAlign.h" + +// Repeater related +#include "pagx/types/RepeaterOrder.h" diff --git a/pagx/include/pagx/model/types/enums/FillRule.h b/pagx/include/pagx/types/FillRule.h similarity index 100% rename from pagx/include/pagx/model/types/enums/FillRule.h rename to pagx/include/pagx/types/FillRule.h diff --git a/pagx/include/pagx/model/types/enums/FontStyle.h b/pagx/include/pagx/types/FontStyle.h similarity index 100% rename from pagx/include/pagx/model/types/enums/FontStyle.h rename to pagx/include/pagx/types/FontStyle.h diff --git a/pagx/include/pagx/model/types/enums/LineCap.h b/pagx/include/pagx/types/LineCap.h similarity index 100% rename from pagx/include/pagx/model/types/enums/LineCap.h rename to pagx/include/pagx/types/LineCap.h diff --git a/pagx/include/pagx/model/types/enums/LineJoin.h b/pagx/include/pagx/types/LineJoin.h similarity index 100% rename from pagx/include/pagx/model/types/enums/LineJoin.h rename to pagx/include/pagx/types/LineJoin.h diff --git a/pagx/include/pagx/model/types/enums/MaskType.h b/pagx/include/pagx/types/MaskType.h similarity index 100% rename from pagx/include/pagx/model/types/enums/MaskType.h rename to pagx/include/pagx/types/MaskType.h diff --git a/pagx/include/pagx/model/types/enums/MergePathMode.h b/pagx/include/pagx/types/MergePathMode.h similarity index 100% rename from pagx/include/pagx/model/types/enums/MergePathMode.h rename to pagx/include/pagx/types/MergePathMode.h diff --git a/pagx/include/pagx/model/types/enums/Overflow.h b/pagx/include/pagx/types/Overflow.h similarity index 100% rename from pagx/include/pagx/model/types/enums/Overflow.h rename to pagx/include/pagx/types/Overflow.h diff --git a/pagx/include/pagx/model/types/enums/Placement.h b/pagx/include/pagx/types/Placement.h similarity index 100% rename from pagx/include/pagx/model/types/enums/Placement.h rename to pagx/include/pagx/types/Placement.h diff --git a/pagx/include/pagx/model/types/enums/PolystarType.h b/pagx/include/pagx/types/PolystarType.h similarity index 100% rename from pagx/include/pagx/model/types/enums/PolystarType.h rename to pagx/include/pagx/types/PolystarType.h diff --git a/pagx/include/pagx/model/types/enums/RepeaterOrder.h b/pagx/include/pagx/types/RepeaterOrder.h similarity index 100% rename from pagx/include/pagx/model/types/enums/RepeaterOrder.h rename to pagx/include/pagx/types/RepeaterOrder.h diff --git a/pagx/include/pagx/model/types/enums/SamplingMode.h b/pagx/include/pagx/types/SamplingMode.h similarity index 100% rename from pagx/include/pagx/model/types/enums/SamplingMode.h rename to pagx/include/pagx/types/SamplingMode.h diff --git a/pagx/include/pagx/model/types/enums/SelectorMode.h b/pagx/include/pagx/types/SelectorMode.h similarity index 100% rename from pagx/include/pagx/model/types/enums/SelectorMode.h rename to pagx/include/pagx/types/SelectorMode.h diff --git a/pagx/include/pagx/model/types/enums/SelectorShape.h b/pagx/include/pagx/types/SelectorShape.h similarity index 100% rename from pagx/include/pagx/model/types/enums/SelectorShape.h rename to pagx/include/pagx/types/SelectorShape.h diff --git a/pagx/include/pagx/model/types/enums/SelectorUnit.h b/pagx/include/pagx/types/SelectorUnit.h similarity index 100% rename from pagx/include/pagx/model/types/enums/SelectorUnit.h rename to pagx/include/pagx/types/SelectorUnit.h diff --git a/pagx/include/pagx/model/types/enums/StrokeAlign.h b/pagx/include/pagx/types/StrokeAlign.h similarity index 100% rename from pagx/include/pagx/model/types/enums/StrokeAlign.h rename to pagx/include/pagx/types/StrokeAlign.h diff --git a/pagx/include/pagx/model/types/enums/TextAlign.h b/pagx/include/pagx/types/TextAlign.h similarity index 100% rename from pagx/include/pagx/model/types/enums/TextAlign.h rename to pagx/include/pagx/types/TextAlign.h diff --git a/pagx/include/pagx/model/types/enums/TextPathAlign.h b/pagx/include/pagx/types/TextPathAlign.h similarity index 100% rename from pagx/include/pagx/model/types/enums/TextPathAlign.h rename to pagx/include/pagx/types/TextPathAlign.h diff --git a/pagx/include/pagx/model/types/enums/TileMode.h b/pagx/include/pagx/types/TileMode.h similarity index 100% rename from pagx/include/pagx/model/types/enums/TileMode.h rename to pagx/include/pagx/types/TileMode.h diff --git a/pagx/include/pagx/model/types/enums/TrimType.h b/pagx/include/pagx/types/TrimType.h similarity index 100% rename from pagx/include/pagx/model/types/enums/TrimType.h rename to pagx/include/pagx/types/TrimType.h diff --git a/pagx/include/pagx/model/types/Types.h b/pagx/include/pagx/types/Types.h similarity index 100% rename from pagx/include/pagx/model/types/Types.h rename to pagx/include/pagx/types/Types.h diff --git a/pagx/include/pagx/model/types/enums/VerticalAlign.h b/pagx/include/pagx/types/VerticalAlign.h similarity index 100% rename from pagx/include/pagx/model/types/enums/VerticalAlign.h rename to pagx/include/pagx/types/VerticalAlign.h diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp index fd17531943..7f309a82fa 100644 --- a/pagx/src/PAGXDocument.cpp +++ b/pagx/src/PAGXDocument.cpp @@ -24,6 +24,32 @@ namespace pagx { +// Helper function to get id from a resource node +static std::string getResourceId(const Node* node) { + switch (node->type()) { + case NodeType::Image: + return static_cast(node)->id; + case NodeType::PathData: + return static_cast(node)->id; + case NodeType::SolidColor: + return static_cast(node)->id; + case NodeType::LinearGradient: + return static_cast(node)->id; + case NodeType::RadialGradient: + return static_cast(node)->id; + case NodeType::ConicGradient: + return static_cast(node)->id; + case NodeType::DiamondGradient: + return static_cast(node)->id; + case NodeType::ImagePattern: + return static_cast(node)->id; + case NodeType::Composition: + return static_cast(node)->id; + default: + return {}; + } +} + std::shared_ptr PAGXDocument::Make(float docWidth, float docHeight) { auto doc = std::shared_ptr(new PAGXDocument()); doc->width = docWidth; @@ -60,25 +86,7 @@ std::string PAGXDocument::toXML() const { return PAGXXMLWriter::Write(*this); } -std::shared_ptr PAGXDocument::clone() const { - auto doc = std::shared_ptr(new PAGXDocument()); - doc->version = version; - doc->width = width; - doc->height = height; - doc->basePath = basePath; - for (const auto& resource : resources) { - doc->resources.push_back( - std::unique_ptr(static_cast(resource->clone().release()))); - } - for (const auto& layer : layers) { - doc->layers.push_back( - std::unique_ptr(static_cast(layer->clone().release()))); - } - doc->resourceMapDirty = true; - return doc; -} - -Resource* PAGXDocument::findResource(const std::string& id) const { +Node* PAGXDocument::findResource(const std::string& id) const { if (resourceMapDirty) { rebuildResourceMap(); } @@ -108,8 +116,9 @@ Layer* PAGXDocument::findLayer(const std::string& id) const { void PAGXDocument::rebuildResourceMap() const { resourceMap.clear(); for (const auto& resource : resources) { - if (!resource->id.empty()) { - resourceMap[resource->id] = resource.get(); + auto id = getResourceId(resource.get()); + if (!id.empty()) { + resourceMap[id] = resource.get(); } } resourceMapDirty = false; diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp index 778a074264..9628e0c293 100644 --- a/pagx/src/PAGXTypes.cpp +++ b/pagx/src/PAGXTypes.cpp @@ -352,11 +352,6 @@ DEFINE_ENUM_CONVERSION(RepeaterOrder, {RepeaterOrder::BelowOriginal, "belowOriginal"}, {RepeaterOrder::AboveOriginal, "aboveOriginal"}) -DEFINE_ENUM_CONVERSION(TextAnchor, - {TextAnchor::Start, "start"}, - {TextAnchor::Middle, "middle"}, - {TextAnchor::End, "end"}) - #undef DEFINE_ENUM_CONVERSION } // namespace pagx diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 25c6d04a17..8aac3db848 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -300,7 +300,7 @@ void PAGXXMLParser::parseResources(const XMLNode* node, PAGXDocument* doc) { } } -std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { if (node->tag == "Image") { return parseImage(node); } @@ -433,7 +433,7 @@ void PAGXXMLParser::parseFilters(const XMLNode* node, Layer* layer) { } } -std::unique_ptr PAGXXMLParser::parseVectorElement(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseVectorElement(const XMLNode* node) { if (node->tag == "Rectangle") { return parseRectangle(node); } @@ -482,7 +482,7 @@ std::unique_ptr PAGXXMLParser::parseVectorElement(const XMLNode* return nullptr; } -std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node) { if (node->tag == "SolidColor") { return parseSolidColor(node); } @@ -504,7 +504,7 @@ std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) { if (node->tag == "DropShadowStyle") { return parseDropShadowStyle(node); } @@ -517,7 +517,7 @@ std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* node) { if (node->tag == "BlurFilter") { return parseBlurFilter(node); } @@ -596,7 +596,6 @@ std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) { textSpan->fontStyle = FontStyleFromString(getAttribute(node, "fontStyle", "normal")); textSpan->tracking = getFloatAttribute(node, "tracking", 0); textSpan->baselineShift = getFloatAttribute(node, "baselineShift", 0); - textSpan->textAnchor = TextAnchorFromString(getAttribute(node, "textAnchor", "start")); textSpan->text = node->text; return textSpan; } @@ -627,7 +626,7 @@ std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { std::unique_ptr PAGXXMLParser::parseStroke(const XMLNode* node) { auto stroke = std::make_unique(); stroke->color = getAttribute(node, "color"); - stroke->strokeWidth = getFloatAttribute(node, "width", 1); + stroke->width = getFloatAttribute(node, "width", 1); stroke->alpha = getFloatAttribute(node, "alpha", 1); stroke->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); stroke->cap = LineCapFromString(getAttribute(node, "cap", "butt")); @@ -705,7 +704,7 @@ std::unique_ptr PAGXXMLParser::parseTextModifier(const XMLNode* no std::unique_ptr PAGXXMLParser::parseTextPath(const XMLNode* node) { auto textPath = std::make_unique(); textPath->path = getAttribute(node, "path"); - textPath->textPathAlign = TextPathAlignFromString(getAttribute(node, "align", "start")); + textPath->pathAlign = TextPathAlignFromString(getAttribute(node, "align", "start")); textPath->firstMargin = getFloatAttribute(node, "firstMargin", 0); textPath->lastMargin = getFloatAttribute(node, "lastMargin", 0); textPath->perpendicularToPath = getBoolAttribute(node, "perpendicularToPath", true); @@ -745,7 +744,7 @@ std::unique_ptr PAGXXMLParser::parseRepeater(const XMLNode* node) { std::unique_ptr PAGXXMLParser::parseGroup(const XMLNode* node) { auto group = std::make_unique(); - group->name = getAttribute(node, "name"); + // group->name (removed) = getAttribute(node, "name"); auto anchorStr = getAttribute(node, "anchorPoint", "0,0"); group->anchorPoint = parsePoint(anchorStr); auto positionStr = getAttribute(node, "position", "0,0"); @@ -1018,7 +1017,7 @@ std::unique_ptr PAGXXMLParser::parseBlendFilter(const XMLNode* node if (!colorStr.empty()) { filter->color = Color::Parse(colorStr); } - filter->filterBlendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); + filter->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); return filter; } diff --git a/pagx/src/PAGXXMLParser.h b/pagx/src/PAGXXMLParser.h index d8189c886e..f049a6cd3d 100644 --- a/pagx/src/PAGXXMLParser.h +++ b/pagx/src/PAGXXMLParser.h @@ -51,16 +51,16 @@ class PAGXXMLParser { static void parseDocument(const XMLNode* root, PAGXDocument* doc); static void parseResources(const XMLNode* node, PAGXDocument* doc); - static std::unique_ptr parseResource(const XMLNode* node); + static std::unique_ptr parseResource(const XMLNode* node); static std::unique_ptr parseLayer(const XMLNode* node); static void parseContents(const XMLNode* node, Layer* layer); static void parseStyles(const XMLNode* node, Layer* layer); static void parseFilters(const XMLNode* node, Layer* layer); - static std::unique_ptr parseVectorElement(const XMLNode* node); - static std::unique_ptr parseColorSource(const XMLNode* node); - static std::unique_ptr parseLayerStyle(const XMLNode* node); - static std::unique_ptr parseLayerFilter(const XMLNode* node); + static std::unique_ptr parseVectorElement(const XMLNode* node); + static std::unique_ptr parseColorSource(const XMLNode* node); + static std::unique_ptr parseLayerStyle(const XMLNode* node); + static std::unique_ptr parseLayerFilter(const XMLNode* node); static std::unique_ptr parseRectangle(const XMLNode* node); static std::unique_ptr parseEllipse(const XMLNode* node); diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index 5cb430b88c..7b87d63c18 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -187,7 +187,7 @@ static std::string floatListToString(const std::vector& values) { // ColorSource serialization helper //============================================================================== -static std::string colorSourceToKey(const ColorSource* node) { +static std::string colorSourceToKey(const Node* node) { if (!node) { return ""; } @@ -263,7 +263,7 @@ class ResourceContext { std::vector> pathDataResources = {}; // id -> svg string // All extracted ColorSource resources (ordered) - std::vector> colorSourceResources = {}; + std::vector> colorSourceResources = {}; int nextPathId = 1; int nextColorId = 1; @@ -296,7 +296,7 @@ class ResourceContext { } // Register ColorSource usage (for counting) - void registerColorSource(const ColorSource* node) { + void registerColorSource(const Node* node) { if (!node) { return; } @@ -322,7 +322,7 @@ class ResourceContext { } // Check if ColorSource should be extracted to Resources - bool shouldExtractColorSource(const ColorSource* node) const { + bool shouldExtractColorSource(const Node* node) const { if (!node) { return false; } @@ -332,7 +332,7 @@ class ResourceContext { } // Get ColorSource resource id (empty if should inline) - std::string getColorSourceId(const ColorSource* node) const { + std::string getColorSourceId(const Node* node) const { if (!node) { return ""; } @@ -354,7 +354,7 @@ class ResourceContext { } } - void collectFromVectorElement(const VectorElement* element) { + void collectFromVectorElement(const Node* element) { switch (element->type()) { case NodeType::Path: { auto path = static_cast(element); @@ -394,12 +394,12 @@ class ResourceContext { // Forward declarations //============================================================================== -static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId); -static void writeVectorElement(XMLBuilder& xml, const VectorElement* node, +static void writeColorSource(XMLBuilder& xml, const Node* node, bool writeId); +static void writeVectorElement(XMLBuilder& xml, const Node* node, const ResourceContext& ctx); -static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node); -static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node); -static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceContext& ctx); +static void writeLayerStyle(XMLBuilder& xml, const Node* node); +static void writeLayerFilter(XMLBuilder& xml, const Node* node); +static void writeResource(XMLBuilder& xml, const Node* node, const ResourceContext& ctx); static void writeLayer(XMLBuilder& xml, const Layer* node, const ResourceContext& ctx); //============================================================================== @@ -415,7 +415,7 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& stops } } -static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId) { +static void writeColorSource(XMLBuilder& xml, const Node* node, bool writeId) { switch (node->type()) { case NodeType::SolidColor: { auto solid = static_cast(node); @@ -544,7 +544,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ } // Write ColorSource with assigned id (for Resources section) -static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, +static void writeColorSourceWithId(XMLBuilder& xml, const Node* node, const std::string& id) { switch (node->type()) { case NodeType::SolidColor: { @@ -665,7 +665,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, // VectorElement writing //============================================================================== -static void writeVectorElement(XMLBuilder& xml, const VectorElement* node, +static void writeVectorElement(XMLBuilder& xml, const Node* node, const ResourceContext& ctx) { switch (node->type()) { case NodeType::Rectangle: { @@ -743,9 +743,6 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElement* node, } xml.addAttribute("tracking", text->tracking); xml.addAttribute("baselineShift", text->baselineShift); - if (text->textAnchor != TextAnchor::Start) { - xml.addAttribute("textAnchor", TextAnchorToString(text->textAnchor)); - } xml.closeElementStart(); xml.addTextContent(text->text); xml.closeElement(); @@ -799,7 +796,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElement* node, } else { xml.addAttribute("color", stroke->color); } - xml.addAttribute("width", stroke->strokeWidth, 1.0f); + xml.addAttribute("width", stroke->width, 1.0f); xml.addAttribute("alpha", stroke->alpha, 1.0f); if (stroke->blendMode != BlendMode::Normal) { xml.addAttribute("blendMode", BlendModeToString(stroke->blendMode)); @@ -913,8 +910,8 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElement* node, auto textPath = static_cast(node); xml.openElement("TextPath"); xml.addAttribute("path", textPath->path); - if (textPath->textPathAlign != TextPathAlign::Start) { - xml.addAttribute("align", TextPathAlignToString(textPath->textPathAlign)); + if (textPath->pathAlign != TextPathAlign::Start) { + xml.addAttribute("align", TextPathAlignToString(textPath->pathAlign)); } xml.addAttribute("firstMargin", textPath->firstMargin); xml.addAttribute("lastMargin", textPath->lastMargin); @@ -969,7 +966,6 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElement* node, case NodeType::Group: { auto group = static_cast(node); xml.openElement("Group"); - xml.addAttribute("name", group->name); if (group->anchorPoint.x != 0 || group->anchorPoint.y != 0) { xml.addAttribute("anchorPoint", pointToString(group->anchorPoint)); } @@ -1003,7 +999,7 @@ static void writeVectorElement(XMLBuilder& xml, const VectorElement* node, // LayerStyle writing //============================================================================== -static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { +static void writeLayerStyle(XMLBuilder& xml, const Node* node) { switch (node->type()) { case NodeType::DropShadowStyle: { auto style = static_cast(node); @@ -1057,7 +1053,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { // LayerFilter writing //============================================================================== -static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { +static void writeLayerFilter(XMLBuilder& xml, const Node* node) { switch (node->type()) { case NodeType::BlurFilter: { auto filter = static_cast(node); @@ -1098,8 +1094,8 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { auto filter = static_cast(node); xml.openElement("BlendFilter"); xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); - if (filter->filterBlendMode != BlendMode::Normal) { - xml.addAttribute("blendMode", BlendModeToString(filter->filterBlendMode)); + if (filter->blendMode != BlendMode::Normal) { + xml.addAttribute("blendMode", BlendModeToString(filter->blendMode)); } xml.closeElementSelfClosing(); break; @@ -1121,7 +1117,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { // Resource writing //============================================================================== -static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceContext& ctx) { +static void writeResource(XMLBuilder& xml, const Node* node, const ResourceContext& ctx) { switch (node->type()) { case NodeType::Image: { auto image = static_cast(node); @@ -1162,7 +1158,7 @@ static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceC case NodeType::ConicGradient: case NodeType::DiamondGradient: case NodeType::ImagePattern: - writeColorSource(xml, static_cast(node), true); + writeColorSource(xml, static_cast(node), true); break; default: break; @@ -1266,7 +1262,7 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { // Build ColorSource resources list (only those with multiple references) // We need to store pointers to actual ColorSource nodes for writing - std::unordered_map colorSourceByKey = {}; + std::unordered_map colorSourceByKey = {}; for (const auto& layer : doc.layers) { std::function collectColorSources = [&](const Layer* layer) { for (const auto& element : layer->contents) { diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 20ea273248..ac3bd91ba8 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -294,6 +294,8 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrsecond, inheritedStyle); if (maskLayer) { layer->mask = "#" + maskLayer->id; + // SVG masks use luminance by default. + layer->maskType = MaskType::Luminance; // Add mask layer as invisible layer to the document. _maskLayers.push_back(std::move(maskLayer)); } @@ -330,7 +332,7 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle) { const auto& tag = element->name; @@ -351,7 +353,7 @@ void SVGParserImpl::convertChildren(const std::shared_ptr& element, addFillStroke(element, contents, inheritedStyle); } -std::unique_ptr SVGParserImpl::convertElement( +std::unique_ptr SVGParserImpl::convertElement( const std::shared_ptr& element) { const auto& tag = element->name; @@ -383,7 +385,7 @@ std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& e auto group = std::make_unique(); - group->name = getAttribute(element, "id"); + // group->name (removed) = getAttribute(element, "id"); std::string transform = getAttribute(element, "transform"); if (!transform.empty()) { @@ -412,7 +414,7 @@ std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& e return group; } -std::unique_ptr SVGParserImpl::convertRect( +std::unique_ptr SVGParserImpl::convertRect( const std::shared_ptr& element) { float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); @@ -435,7 +437,7 @@ std::unique_ptr SVGParserImpl::convertRect( return rect; } -std::unique_ptr SVGParserImpl::convertCircle( +std::unique_ptr SVGParserImpl::convertCircle( const std::shared_ptr& element) { float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); @@ -450,7 +452,7 @@ std::unique_ptr SVGParserImpl::convertCircle( return ellipse; } -std::unique_ptr SVGParserImpl::convertEllipse( +std::unique_ptr SVGParserImpl::convertEllipse( const std::shared_ptr& element) { float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); @@ -466,7 +468,7 @@ std::unique_ptr SVGParserImpl::convertEllipse( return ellipse; } -std::unique_ptr SVGParserImpl::convertLine( +std::unique_ptr SVGParserImpl::convertLine( const std::shared_ptr& element) { float x1 = parseLength(getAttribute(element, "x1"), _viewBoxWidth); float y1 = parseLength(getAttribute(element, "y1"), _viewBoxHeight); @@ -480,21 +482,21 @@ std::unique_ptr SVGParserImpl::convertLine( return path; } -std::unique_ptr SVGParserImpl::convertPolyline( +std::unique_ptr SVGParserImpl::convertPolyline( const std::shared_ptr& element) { auto path = std::make_unique(); path->data = parsePoints(getAttribute(element, "points"), false); return path; } -std::unique_ptr SVGParserImpl::convertPolygon( +std::unique_ptr SVGParserImpl::convertPolygon( const std::shared_ptr& element) { auto path = std::make_unique(); path->data = parsePoints(getAttribute(element, "points"), true); return path; } -std::unique_ptr SVGParserImpl::convertPath( +std::unique_ptr SVGParserImpl::convertPath( const std::shared_ptr& element) { auto path = std::make_unique(); std::string d = getAttribute(element, "d"); @@ -511,14 +513,14 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); - // Parse text-anchor attribute. - TextAnchor textAnchor = TextAnchor::Start; + // Parse text-anchor attribute for x position adjustment. + // SVG text-anchor affects horizontal alignment: start (default), middle, end. + // Since PAGX TextSpan doesn't have text-anchor, we'll note this for future + // position adjustment after text shaping (requires knowing text width). std::string anchor = getAttribute(element, "text-anchor"); - if (anchor == "middle") { - textAnchor = TextAnchor::Middle; - } else if (anchor == "end") { - textAnchor = TextAnchor::End; - } + // Note: text-anchor adjustment would require knowing the text width after shaping. + // For now, we store the x position as-is. A full implementation would need to + // adjust x based on anchor after calculating the text bounds. // Get text content from child text nodes. std::string textContent; @@ -535,7 +537,6 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr textSpan->x = x; textSpan->y = y; textSpan->text = textContent; - textSpan->textAnchor = textAnchor; std::string fontFamily = getAttribute(element, "font-family"); if (!fontFamily.empty()) { @@ -554,7 +555,7 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr return group; } -std::unique_ptr SVGParserImpl::convertUse( +std::unique_ptr SVGParserImpl::convertUse( const std::shared_ptr& element) { std::string href = getAttribute(element, "xlink:href"); if (href.empty()) { @@ -585,12 +586,12 @@ std::unique_ptr SVGParserImpl::convertUse( // For non-expanded use references, just create an empty group for now. auto group = std::make_unique(); - group->name = "_useRef:" + refId; + // group->name (removed) = "_useRef:" + refId; return group; } std::unique_ptr SVGParserImpl::convertLinearGradient( - const std::shared_ptr& element) { + const std::shared_ptr& element, const Rect& shapeBounds) { auto gradient = std::make_unique(); gradient->id = getAttribute(element, "id"); @@ -613,9 +614,10 @@ std::unique_ptr SVGParserImpl::convertLinearGradient( if (useOBB) { // For objectBoundingBox, coordinates are normalized 0-1. - // Apply gradient transform to normalized points. - Point start = {x1, y1}; - Point end = {x2, y2}; + // Convert to actual coordinates based on shape bounds. + Point start = {shapeBounds.x + x1 * shapeBounds.width, shapeBounds.y + y1 * shapeBounds.height}; + Point end = {shapeBounds.x + x2 * shapeBounds.width, shapeBounds.y + y2 * shapeBounds.height}; + // Apply gradient transform after converting to actual coordinates. start = transformMatrix.mapPoint(start); end = transformMatrix.mapPoint(end); gradient->startPoint = start; @@ -649,7 +651,7 @@ std::unique_ptr SVGParserImpl::convertLinearGradient( } std::unique_ptr SVGParserImpl::convertRadialGradient( - const std::shared_ptr& element) { + const std::shared_ptr& element, const Rect& shapeBounds) { auto gradient = std::make_unique(); gradient->id = getAttribute(element, "id"); @@ -668,28 +670,50 @@ std::unique_ptr SVGParserImpl::convertRadialGradient( Matrix transformMatrix = gradientTransform.empty() ? Matrix::Identity() : parseTransform(gradientTransform); - if (useOBB || !gradientTransform.empty()) { - // Apply gradientTransform to center point. - Point center = {cx, cy}; - center = transformMatrix.mapPoint(center); - gradient->center = center; + if (useOBB) { + // For objectBoundingBox, convert normalized coordinates to actual coordinates. + Point center = {shapeBounds.x + cx * shapeBounds.width, + shapeBounds.y + cy * shapeBounds.height}; + // Radius is scaled by the average of width and height. + float actualRadius = r * (shapeBounds.width + shapeBounds.height) / 2.0f; - // For radius, we need to account for scaling in the transform. - // Use the average of X and Y scale factors. + // Apply gradientTransform after converting to actual coordinates. + center = transformMatrix.mapPoint(center); + // For radius, account for scaling in the transform. float scaleX = std::sqrt(transformMatrix.a * transformMatrix.a + transformMatrix.b * transformMatrix.b); float scaleY = std::sqrt(transformMatrix.c * transformMatrix.c + transformMatrix.d * transformMatrix.d); - gradient->radius = r * (scaleX + scaleY) / 2.0f; + actualRadius *= (scaleX + scaleY) / 2.0f; + + gradient->center = center; + gradient->radius = actualRadius; // Store the matrix for non-uniform scaling (rotation, skew, etc.). if (!transformMatrix.isIdentity()) { gradient->matrix = transformMatrix; } } else { - gradient->center.x = cx; - gradient->center.y = cy; - gradient->radius = r; + // For userSpaceOnUse, coordinates are in user space. + if (!gradientTransform.empty()) { + Point center = {cx, cy}; + center = transformMatrix.mapPoint(center); + gradient->center = center; + + float scaleX = std::sqrt(transformMatrix.a * transformMatrix.a + + transformMatrix.b * transformMatrix.b); + float scaleY = std::sqrt(transformMatrix.c * transformMatrix.c + + transformMatrix.d * transformMatrix.d); + gradient->radius = r * (scaleX + scaleY) / 2.0f; + + if (!transformMatrix.isIdentity()) { + gradient->matrix = transformMatrix; + } + } else { + gradient->center.x = cx; + gradient->center.y = cy; + gradient->radius = r; + } } // Parse stops. @@ -829,7 +853,7 @@ std::unique_ptr SVGParserImpl::convertPattern( } void SVGParserImpl::addFillStroke(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle) { // Get shape bounds for pattern calculations (computed once, used if needed). Rect shapeBounds = getShapeBounds(element); @@ -858,9 +882,9 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, auto it = _defs.find(refId); if (it != _defs.end()) { if (it->second->name == "linearGradient") { - fillNode->colorSource = convertLinearGradient(it->second); + fillNode->colorSource = convertLinearGradient(it->second, shapeBounds); } else if (it->second->name == "radialGradient") { - fillNode->colorSource = convertRadialGradient(it->second); + fillNode->colorSource = convertRadialGradient(it->second, shapeBounds); } else if (it->second->name == "pattern") { fillNode->colorSource = convertPattern(it->second, shapeBounds); } @@ -909,9 +933,9 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, auto it = _defs.find(refId); if (it != _defs.end()) { if (it->second->name == "linearGradient") { - strokeNode->colorSource = convertLinearGradient(it->second); + strokeNode->colorSource = convertLinearGradient(it->second, shapeBounds); } else if (it->second->name == "radialGradient") { - strokeNode->colorSource = convertRadialGradient(it->second); + strokeNode->colorSource = convertRadialGradient(it->second, shapeBounds); } else if (it->second->name == "pattern") { strokeNode->colorSource = convertPattern(it->second, shapeBounds); } @@ -932,7 +956,7 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, std::string strokeWidth = getAttribute(element, "stroke-width"); if (!strokeWidth.empty()) { - strokeNode->strokeWidth = parseLength(strokeWidth, _viewBoxWidth); + strokeNode->width = parseLength(strokeWidth, _viewBoxWidth); } std::string strokeLinecap = getAttribute(element, "stroke-linecap"); @@ -1477,7 +1501,7 @@ std::string SVGParserImpl::registerImageResource(const std::string& imageSource) } // Helper function to check if two VectorElement nodes are the same geometry. -static bool isSameGeometry(const VectorElement* a, const VectorElement* b) { +static bool isSameGeometry(const Node* a, const Node* b) { if (!a || !b || a->type() != b->type()) { return false; } @@ -1508,8 +1532,8 @@ static bool isSameGeometry(const VectorElement* a, const VectorElement* b) { } // Check if a layer is a simple shape layer (contains exactly one geometry and one Fill or Stroke). -static bool isSimpleShapeLayer(const Layer* layer, const VectorElement*& outGeometry, - const VectorElement*& outPainter) { +static bool isSimpleShapeLayer(const Layer* layer, const Node*& outGeometry, + const Node*& outPainter) { if (!layer || layer->contents.size() != 2) { return false; } @@ -1546,14 +1570,14 @@ void SVGParserImpl::mergeAdjacentLayers(std::vector>& lay size_t i = 0; while (i < layers.size()) { - const VectorElement* geomA = nullptr; - const VectorElement* painterA = nullptr; + const Node* geomA = nullptr; + const Node* painterA = nullptr; if (isSimpleShapeLayer(layers[i].get(), geomA, painterA)) { // Check if the next layer has the same geometry. if (i + 1 < layers.size()) { - const VectorElement* geomB = nullptr; - const VectorElement* painterB = nullptr; + const Node* geomB = nullptr; + const Node* painterB = nullptr; if (isSimpleShapeLayer(layers[i + 1].get(), geomB, painterB) && isSameGeometry(geomA, geomB)) { @@ -1565,26 +1589,16 @@ void SVGParserImpl::mergeAdjacentLayers(std::vector>& lay // Create merged layer. auto mergedLayer = std::make_unique(); - // Keep geometry from first layer. - auto geomClone = layers[i]->contents[0]->clone(); - mergedLayer->contents.push_back( - std::unique_ptr(static_cast(geomClone.release()))); + // Move geometry from first layer. + mergedLayer->contents.push_back(std::move(layers[i]->contents[0])); // Add Fill first, then Stroke (standard order). if (aHasFill) { - auto fillClone = layers[i]->contents[1]->clone(); - mergedLayer->contents.push_back( - std::unique_ptr(static_cast(fillClone.release()))); - auto strokeClone = layers[i + 1]->contents[1]->clone(); - mergedLayer->contents.push_back( - std::unique_ptr(static_cast(strokeClone.release()))); + mergedLayer->contents.push_back(std::move(layers[i]->contents[1])); + mergedLayer->contents.push_back(std::move(layers[i + 1]->contents[1])); } else { - auto fillClone = layers[i + 1]->contents[1]->clone(); - mergedLayer->contents.push_back( - std::unique_ptr(static_cast(fillClone.release()))); - auto strokeClone = layers[i]->contents[1]->clone(); - mergedLayer->contents.push_back( - std::unique_ptr(static_cast(strokeClone.release()))); + mergedLayer->contents.push_back(std::move(layers[i + 1]->contents[1])); + mergedLayer->contents.push_back(std::move(layers[i]->contents[1])); } merged.push_back(std::move(mergedLayer)); @@ -1634,7 +1648,7 @@ std::unique_ptr SVGParserImpl::convertMaskElement( void SVGParserImpl::convertFilterElement( const std::shared_ptr& filterElement, - std::vector>& filters) { + std::vector>& filters) { // Parse filter children to find effect elements. auto child = filterElement->getFirstChild(); while (child) { diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 425ce95973..8f2981f76b 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -58,36 +58,36 @@ class SVGParserImpl { std::unique_ptr convertToLayer(const std::shared_ptr& element, const InheritedStyle& parentStyle); void convertChildren(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle); - std::unique_ptr convertElement(const std::shared_ptr& element); + std::unique_ptr convertElement(const std::shared_ptr& element); std::unique_ptr convertG(const std::shared_ptr& element, const InheritedStyle& inheritedStyle); - std::unique_ptr convertRect(const std::shared_ptr& element); - std::unique_ptr convertCircle(const std::shared_ptr& element); - std::unique_ptr convertEllipse(const std::shared_ptr& element); - std::unique_ptr convertLine(const std::shared_ptr& element); - std::unique_ptr convertPolyline(const std::shared_ptr& element); - std::unique_ptr convertPolygon(const std::shared_ptr& element); - std::unique_ptr convertPath(const std::shared_ptr& element); + std::unique_ptr convertRect(const std::shared_ptr& element); + std::unique_ptr convertCircle(const std::shared_ptr& element); + std::unique_ptr convertEllipse(const std::shared_ptr& element); + std::unique_ptr convertLine(const std::shared_ptr& element); + std::unique_ptr convertPolyline(const std::shared_ptr& element); + std::unique_ptr convertPolygon(const std::shared_ptr& element); + std::unique_ptr convertPath(const std::shared_ptr& element); std::unique_ptr convertText(const std::shared_ptr& element, const InheritedStyle& inheritedStyle); - std::unique_ptr convertUse(const std::shared_ptr& element); + std::unique_ptr convertUse(const std::shared_ptr& element); std::unique_ptr convertLinearGradient( - const std::shared_ptr& element); + const std::shared_ptr& element, const Rect& shapeBounds); std::unique_ptr convertRadialGradient( - const std::shared_ptr& element); + const std::shared_ptr& element, const Rect& shapeBounds); std::unique_ptr convertPattern(const std::shared_ptr& element, const Rect& shapeBounds); std::unique_ptr convertMaskElement(const std::shared_ptr& maskElement, const InheritedStyle& parentStyle); void convertFilterElement(const std::shared_ptr& filterElement, - std::vector>& filters); + std::vector>& filters); void addFillStroke(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle); // Compute shape bounds from SVG element attributes. diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index eee0a099ef..25158a68b4 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -18,6 +18,8 @@ #include "pagx/LayerBuilder.h" #include +#include +#include #include "pagx/PAGXSVGParser.h" #include "tgfx/core/Data.h" #include "tgfx/core/Font.h" @@ -25,6 +27,7 @@ #include "tgfx/core/Path.h" #include "tgfx/core/TextBlob.h" #include "tgfx/layers/Layer.h" +#include "tgfx/layers/LayerMaskType.h" #include "tgfx/layers/VectorLayer.h" #include "tgfx/layers/filters/BlurFilter.h" #include "tgfx/layers/filters/DropShadowFilter.h" @@ -187,6 +190,18 @@ static tgfx::LineJoin ToTGFX(LineJoin join) { return tgfx::LineJoin::Miter; } +static tgfx::LayerMaskType ToTGFXMaskType(MaskType type) { + switch (type) { + case MaskType::Alpha: + return tgfx::LayerMaskType::Alpha; + case MaskType::Luminance: + return tgfx::LayerMaskType::Luminance; + case MaskType::Contour: + return tgfx::LayerMaskType::Contour; + } + return tgfx::LayerMaskType::Alpha; +} + // Internal builder class class LayerBuilderImpl { public: @@ -203,11 +218,15 @@ class LayerBuilderImpl { // Cache resources for later lookup. _resources = &document.resources; + // Clear layer mappings from previous builds. + _layerById.clear(); + _pendingMasks.clear(); + PAGXContent content; content.width = document.width; content.height = document.height; - // Build layer tree + // Build layer tree. auto rootLayer = tgfx::Layer::Make(); for (const auto& layer : document.layers) { auto childLayer = convertLayer(layer.get()); @@ -216,8 +235,23 @@ class LayerBuilderImpl { } } + // Apply masks after all layers are built (second pass). + for (const auto& [layer, maskId, maskType] : _pendingMasks) { + auto it = _layerById.find(maskId); + if (it != _layerById.end()) { + auto maskLayer = it->second; + // tgfx requires mask layer to be visible for hasValidMask() check. + // The mask layer won't be drawn because maskOwner is set. + maskLayer->setVisible(true); + layer->setMask(maskLayer); + layer->setMaskType(maskType); + } + } + content.root = rootLayer; _resources = nullptr; + _layerById.clear(); + _pendingMasks.clear(); return content; } @@ -236,8 +270,23 @@ class LayerBuilderImpl { } if (layer) { + // Register layer by ID for mask lookups. + if (!node->id.empty()) { + _layerById[node->id] = layer; + } + applyLayerAttributes(node, layer.get()); + // Queue mask to be applied in second pass. + if (!node->mask.empty()) { + std::string maskId = node->mask; + // Remove leading '#' if present. + if (!maskId.empty() && maskId[0] == '#') { + maskId = maskId.substr(1); + } + _pendingMasks.emplace_back(layer, maskId, ToTGFXMaskType(node->maskType)); + } + for (const auto& child : node->children) { auto childLayer = convertLayer(child.get()); if (childLayer) { @@ -264,7 +313,7 @@ class LayerBuilderImpl { return layer; } - std::shared_ptr convertVectorElement(const VectorElement* node) { + std::shared_ptr convertVectorElement(const Node* node) { if (!node) { return nullptr; } @@ -368,16 +417,6 @@ class LayerBuilderImpl { textBlob = tgfx::TextBlob::MakeFrom(node->text, font); } textSpan->setTextBlob(textBlob); - - // Apply text-anchor offset based on text width. - if (textBlob && node->textAnchor != TextAnchor::Start) { - auto bounds = textBlob->getTightBounds(); - if (node->textAnchor == TextAnchor::Middle) { - xOffset = -bounds.width() * 0.5f; - } else if (node->textAnchor == TextAnchor::End) { - xOffset = -bounds.width(); - } - } } textSpan->setPosition(tgfx::Point::Make(node->x + xOffset, node->y)); @@ -418,7 +457,7 @@ class LayerBuilderImpl { stroke->setColorSource(colorSource); } - stroke->setStrokeWidth(node->strokeWidth); + stroke->setStrokeWidth(node->width); stroke->setAlpha(node->alpha); stroke->setLineCap(ToTGFX(node->cap)); stroke->setLineJoin(ToTGFX(node->join)); @@ -431,7 +470,7 @@ class LayerBuilderImpl { return stroke; } - std::shared_ptr convertColorSource(const ColorSource* node) { + std::shared_ptr convertColorSource(const Node* node) { if (!node) { return nullptr; } @@ -637,7 +676,7 @@ class LayerBuilderImpl { } } - std::shared_ptr convertLayerStyle(const LayerStyle* node) { + std::shared_ptr convertLayerStyle(const Node* node) { if (!node) { return nullptr; } @@ -658,7 +697,7 @@ class LayerBuilderImpl { } } - std::shared_ptr convertLayerFilter(const LayerFilter* node) { + std::shared_ptr convertLayerFilter(const Node* node) { if (!node) { return nullptr; } @@ -679,8 +718,11 @@ class LayerBuilderImpl { } LayerBuilder::Options _options = {}; - const std::vector>* _resources = nullptr; + const std::vector>* _resources = nullptr; std::shared_ptr _textShaper = nullptr; + std::unordered_map> _layerById = {}; + std::vector, std::string, tgfx::LayerMaskType>> + _pendingMasks = {}; }; // Public API implementation diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 2af3a07cae..bf14e4322d 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -20,12 +20,13 @@ #include "pagx/LayerBuilder.h" #include "pagx/PAGXDocument.h" #include "pagx/PAGXSVGParser.h" -#include "pagx/model/Model.h" +#include "pagx/PAGXModel.h" #include "pagx/PathData.h" #include "tgfx/core/Data.h" #include "tgfx/core/Stream.h" #include "tgfx/core/Surface.h" #include "tgfx/core/Typeface.h" +#include "tgfx/layers/DisplayList.h" #include "tgfx/svg/SVGDOM.h" #include "tgfx/svg/TextShaper.h" #include "utils/Baseline.h" @@ -137,10 +138,11 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { SaveFile(pagxData, "PAGXTest/" + baseName + ".pagx"); } - // Render PAGX + // Render PAGX using DisplayList (required for mask to work). auto pagxSurface = Surface::Make(context, width, height); - auto pagxCanvas = pagxSurface->getCanvas(); - content.root->draw(pagxCanvas); + DisplayList displayList; + displayList.root()->addChild(content.root); + displayList.render(pagxSurface.get(), false); EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName + "_pagx")); } @@ -214,7 +216,6 @@ PAG_TEST(PAGXTest, PAGXNodeBasic) { // Test Group with children auto group = std::make_unique(); - group->name = "testGroup"; group->elements.push_back(std::move(rect)); group->elements.push_back(std::move(fill)); EXPECT_EQ(group->type(), pagx::NodeType::Group); @@ -237,7 +238,6 @@ PAG_TEST(PAGXTest, PAGXDocumentXMLExport) { // Add a group with rectangle and fill auto group = std::make_unique(); - group->name = "testGroup"; auto rect = std::make_unique(); rect->center.x = 50; @@ -428,35 +428,4 @@ PAG_TEST(PAGXTest, LayerStylesFilters) { EXPECT_EQ(layer->filters[0]->type(), pagx::NodeType::BlurFilter); } -/** - * Test case: Node cloning - */ -PAG_TEST(PAGXTest, NodeClone) { - // Test simple node clone - auto rect = std::make_unique(); - rect->center.x = 50; - rect->center.y = 50; - rect->size.width = 100; - rect->size.height = 80; - - auto cloned = rect->clone(); - ASSERT_TRUE(cloned != nullptr); - EXPECT_EQ(cloned->type(), pagx::NodeType::Rectangle); - - auto clonedRect = static_cast(cloned.get()); - EXPECT_FLOAT_EQ(clonedRect->center.x, 50); - EXPECT_FLOAT_EQ(clonedRect->size.width, 100); - - // Test group with children clone - auto group = std::make_unique(); - group->name = "testGroup"; - group->elements.push_back(std::move(rect)); - - auto clonedGroup = group->clone(); - ASSERT_TRUE(clonedGroup != nullptr); - auto clonedGroupPtr = static_cast(clonedGroup.get()); - EXPECT_EQ(clonedGroupPtr->name, "testGroup"); - EXPECT_EQ(clonedGroupPtr->elements.size(), 1u); -} - } // namespace pag From 82c1ecf6eea9716380b78d18b696c6f49b0052b1 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 15:41:40 +0800 Subject: [PATCH 077/678] Refactor PAGX spec: restructure chapters and reduce cross-references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move Color Source definitions (2.12) to Chapter 3 Resources (3.3.3) - Move Image resource definition to Chapter 3 (3.3.1) - Add PathData resource definition to Chapter 3 (3.3.2) - Remove Chapter 6 Conformance (content was minimal) - Simplify cross-references by removing "(见下方)" annotations - Add Layer transform priority description (x/y, matrix, matrix3D) - Update PathData section to focus on syntax only (2.9) - Fix outdated references to removed sections --- pagx/docs/pagx_spec.md | 241 +++++++++++++------------------ pagx/include/pagx/types/Color.h | 170 ++++++++++++++++++++++ pagx/include/pagx/types/Matrix.h | 160 ++++++++++++++++++++ pagx/include/pagx/types/Point.h | 39 +++++ pagx/include/pagx/types/Rect.h | 79 ++++++++++ pagx/include/pagx/types/Size.h | 39 +++++ pagx/src/PAGXTypes.cpp | 74 ++++++++++ pagx/src/svg/PAGXSVGParser.cpp | 15 +- 8 files changed, 673 insertions(+), 144 deletions(-) create mode 100644 pagx/include/pagx/types/Color.h create mode 100644 pagx/include/pagx/types/Matrix.h create mode 100644 pagx/include/pagx/types/Point.h create mode 100644 pagx/include/pagx/types/Rect.h create mode 100644 pagx/include/pagx/types/Size.h diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 874f31cd47..0fd61682ce 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -160,19 +160,11 @@ PAGX 支持多种颜色格式: | 色域 | `color(display-p3 1 0 0)` | 广色域颜色 | | 引用 | `#resourceId` | 引用 Resources 中定义的颜色源 | -### 2.9 PathData(路径数据) +### 2.9 路径数据语法(Path Data Syntax) -PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素和 TextPath 修改器引用。 +路径数据使用 SVG 路径语法,由一系列命令和坐标组成。 -```xml - -``` - -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `data` | string | (必填) | SVG 路径数据 | - -**路径命令**(SVG 路径语法): +**路径命令**: | 命令 | 参数 | 说明 | |------|------|------| @@ -206,7 +198,64 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 - **数据 URI**:以 `data:` 开头,格式为 `data:;base64,`,仅支持 base64 编码 - 路径分隔符统一使用 `/`(正斜杠),不支持 `\`(反斜杠) -### 2.11 Image(图片) +--- + +## 3. Document Structure(文档结构) + +本节定义 PAGX 文档的整体结构。 + +### 3.1 坐标系统 + +PAGX 使用标准的 2D 笛卡尔坐标系: + +- **原点**:位于画布左上角 +- **X 轴**:向右为正方向 +- **Y 轴**:向下为正方向 +- **角度**:顺时针方向为正(0° 指向 X 轴正方向) +- **单位**:所有长度值默认为像素,角度值默认为度 + +### 3.2 根元素(pagx) + +`` 是 PAGX 文档的根元素,定义画布尺寸并直接包含图层列表。 + +```xml + + + + + + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `version` | string | (必填) | 格式版本 | +| `width` | float | (必填) | 画布宽度 | +| `height` | float | (必填) | 画布高度 | + +**图层渲染顺序**:图层按文档顺序依次渲染,文档中靠前的图层先渲染(位于下方),靠后的图层后渲染(位于上方)。 + +### 3.3 Resources(资源区) + +`` 定义可复用的资源,包括图片、路径数据、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `#id` 形式引用。 + +**元素位置**:Resources 元素可放置在根元素内的任意位置,对位置没有限制。解析器必须支持元素引用在文档后面定义的资源或图层(即前向引用)。 + +```xml + + + + + + + + + + + +``` + +#### 3.3.1 Image(图片) 图片资源定义可在文档中引用的位图数据。 @@ -217,18 +266,30 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `source` | string | (必填) | 文件路径或数据 URI(见 2.10 节) | +| `source` | string | (必填) | 文件路径或数据 URI | **支持格式**:PNG、JPEG、WebP、GIF -### 2.12 颜色源(Color Source) +#### 3.3.2 PathData(路径数据) + +PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器引用。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `data` | string | (必填) | SVG 路径数据 | + +#### 3.3.3 颜色源(Color Source) -颜色源定义可用于渲染的颜色,支持两种定义方式: +颜色源定义可用于填充和描边的颜色,支持两种使用方式: 1. **共享定义**:在 `` 中预定义,通过 `#id` 引用。适用于**被多处引用**的颜色源。 2. **内联定义**:直接嵌套在 `` 或 `` 元素内部。适用于**仅使用一次**的颜色源,更简洁。 -#### 2.12.1 SolidColor(纯色) +##### SolidColor(纯色) ```xml @@ -238,7 +299,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 |------|------|--------|------| | `color` | color | (必填) | 颜色值 | -#### 2.12.2 LinearGradient(线性渐变) +##### LinearGradient(线性渐变) 线性渐变沿起点到终点的方向插值。 @@ -257,7 +318,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **计算**:对于点 P,其颜色由 P 在起点-终点连线上的投影位置决定。 -#### 2.12.3 RadialGradient(径向渐变) +##### RadialGradient(径向渐变) 径向渐变从中心向外辐射。 @@ -276,7 +337,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **计算**:对于点 P,其颜色由 `distance(P, center) / radius` 决定。 -#### 2.12.4 ConicGradient(锥形渐变) +##### ConicGradient(锥形渐变) 锥形渐变(也称扫描渐变)沿圆周方向插值。 @@ -296,7 +357,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **计算**:对于点 P,其颜色由 `atan2(P.y - center.y, P.x - center.x)` 在 `[startAngle, endAngle]` 范围内的比例决定。 -#### 2.12.5 DiamondGradient(菱形渐变) +##### DiamondGradient(菱形渐变) 菱形渐变从中心向四角辐射。 @@ -315,7 +376,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 **计算**:对于点 P,其颜色由曼哈顿距离 `(|P.x - center.x| + |P.y - center.y|) / halfDiagonal` 决定。 -#### 2.12.6 ColorStop(渐变色标) +##### ColorStop(渐变色标) ```xml @@ -336,7 +397,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 - 如果没有 `offset = 1` 的色标,使用最后一个色标的颜色填充 - **渐变变换**:`matrix` 属性对渐变坐标系应用变换 -#### 2.12.7 颜色源坐标系统 +##### 颜色源坐标系统 所有颜色源(渐变、图案)的坐标系是**相对于几何元素的局部坐标系原点**。 @@ -367,7 +428,7 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 - 对该图层应用 `scale(2, 2)` 变换:矩形变为 200×200,渐变也随之放大,视觉效果保持一致 - 直接将 Rectangle 的 size 改为 200,200:矩形变为 200×200,但渐变坐标不变,只覆盖矩形的左半部分 -#### 2.12.8 ImagePattern(图片图案) +##### ImagePattern(图片图案) 图片图案使用图片作为颜色源。 @@ -378,107 +439,18 @@ PathData 定义可复用的路径数据,放置在 Resources 中供 Path 元素 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `image` | idref | (必填) | 图片引用 "#id" | -| `tileModeX` | TileMode | clamp | X 方向平铺模式(见下方) | -| `tileModeY` | TileMode | clamp | Y 方向平铺模式(见下方) | -| `sampling` | SamplingMode | linear | 采样模式(见下方) | +| `tileModeX` | TileMode | clamp | X 方向平铺模式 | +| `tileModeY` | TileMode | clamp | Y 方向平铺模式 | +| `sampling` | SamplingMode | linear | 采样模式 | | `matrix` | string | 单位矩阵 | 变换矩阵 | -**TileMode(平铺模式)**: +**TileMode(平铺模式)**:`clamp`(钳制)、`repeat`(重复)、`mirror`(镜像)、`decal`(贴花) -| 值 | 说明 | -|------|------| -| `clamp` | 钳制:超出边界使用边缘像素颜色 | -| `repeat` | 重复:平铺图片 | -| `mirror` | 镜像:交替翻转平铺 | -| `decal` | 贴花:超出边界为透明 | - -**SamplingMode(采样模式)**: - -| 值 | 说明 | -|------|------| -| `nearest` | 最近邻:取最近像素,锐利但有锯齿 | -| `linear` | 双线性:平滑插值 | -| `mipmap` | 多级渐远:根据缩放级别选择合适的 mip 层 | - -**图案变换**: - -`matrix` 属性对图案应用变换,效果与对普通图形元素的变换一致: - -- `matrix="2,0,0,2,0,0"`(缩放 2 倍):图案视觉上**放大** 2 倍 -- `matrix="0.5,0,0,0.5,0,0"`(缩放 0.5 倍):图案视觉上**缩小**到原来的 1/2 - -**示例**:假设有一个 12×12 像素的棋盘格图片,希望每个 tile 显示为 24×24 像素: - -```xml - - -``` - ---- - -## 3. Document Structure(文档结构) - -本节定义 PAGX 文档的整体结构。 +**SamplingMode(采样模式)**:`nearest`(最近邻)、`linear`(双线性)、`mipmap`(多级渐远) -### 3.1 坐标系统 - -PAGX 使用标准的 2D 笛卡尔坐标系: - -- **原点**:位于画布左上角 -- **X 轴**:向右为正方向 -- **Y 轴**:向下为正方向 -- **角度**:顺时针方向为正(0° 指向 X 轴正方向) -- **单位**:所有长度值默认为像素,角度值默认为度 +**图案变换**:`matrix` 属性对图案应用变换,`matrix="2,0,0,2,0,0"` 使图案视觉上放大 2 倍。 -### 3.2 根元素(pagx) - -`` 是 PAGX 文档的根元素,定义画布尺寸并直接包含图层列表。 - -```xml - - - - - - -``` - -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `version` | string | (必填) | 格式版本 | -| `width` | float | (必填) | 画布宽度 | -| `height` | float | (必填) | 画布高度 | - -**图层渲染顺序**:图层按文档顺序依次渲染,文档中靠前的图层先渲染(位于下方),靠后的图层后渲染(位于上方)。 - -### 3.3 Resources(资源区) - -`` 定义可复用的资源,包括图片、路径数据、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `#id` 形式引用。 - -**元素位置**:Resources 元素可放置在根元素内的任意位置,对位置没有限制。解析器必须支持元素引用在文档后面定义的资源或图层(即前向引用)。 - -```xml - - - - - - - - - - - -``` - -**支持的资源类型**: - -- `Image`:图片资源(见 2.11 节) -- `PathData`:路径数据(见 2.9 节),可被 Path 元素和 TextPath 修改器引用 -- 颜色源(见 2.12 节):`SolidColor`、`LinearGradient`、`RadialGradient`、`ConicGradient`、`DiamondGradient`、`ImagePattern` -- `Composition`:合成(见下方) - -#### 3.3.1 Composition(合成) +#### 3.3.4 Composition(合成) 合成用于内容复用(类似 After Effects 的 Pre-comp)。 @@ -573,7 +545,7 @@ PAGX 文档采用层级结构组织内容: | `name` | string | "" | 显示名称 | | `visible` | bool | true | 是否可见 | | `alpha` | float | 1 | 透明度 0~1 | -| `blendMode` | BlendMode | normal | 混合模式(见下方) | +| `blendMode` | BlendMode | normal | 混合模式 | | `x` | float | 0 | X 位置 | | `y` | float | 0 | Y 位置 | | `matrix` | string | 单位矩阵 | 2D 变换 "a,b,c,d,tx,ty" | @@ -585,9 +557,14 @@ PAGX 文档采用层级结构组织内容: | `excludeChildEffectsInLayerStyle` | bool | false | 图层样式是否排除子图层效果 | | `scrollRect` | string | - | 滚动裁剪区域 "x,y,w,h" | | `mask` | idref | - | 遮罩图层引用 "#id" | -| `maskType` | MaskType | alpha | 遮罩类型(见下方) | +| `maskType` | MaskType | alpha | 遮罩类型 | | `composition` | idref | - | 合成引用 "#id" | +**变换属性优先级**:`x`/`y`、`matrix`、`matrix3D` 三者存在覆盖关系: +- 仅设置 `x`/`y`:使用 `x`/`y` 作为平移 +- 设置 `matrix`:`matrix` 覆盖 `x`/`y` 的值 +- 设置 `matrix3D`:`matrix3D` 覆盖 `matrix` 和 `x`/`y` 的值 + **MaskType(遮罩类型)**: | 值 | 说明 | @@ -727,7 +704,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | 0 | X 模糊半径 | | `blurrinessY` | float | 0 | Y 模糊半径 | -| `tileMode` | TileMode | mirror | 平铺模式(见 2.12.8) | +| `tileMode` | TileMode | mirror | 平铺模式 | **渲染步骤**: 1. 获取图层边界下方的背景内容 @@ -752,7 +729,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blurrinessX` | float | (必填) | X 模糊半径 | | `blurrinessY` | float | (必填) | Y 模糊半径 | -| `tileMode` | TileMode | decal | 平铺模式(见 2.12.8) | +| `tileMode` | TileMode | decal | 平铺模式 | #### 4.3.2 DropShadowFilter(投影阴影滤镜) @@ -836,7 +813,7 @@ PAGX 文档采用层级结构组织内容: ``` -**遮罩类型**:MaskType 定义见 4.1 节。 +**遮罩类型**:MaskType 枚举见 Layer 属性定义。 **遮罩规则**: - 遮罩图层自身不渲染(`visible` 属性被忽略) @@ -1035,7 +1012,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `data` | string/idref | (必填) | SVG 路径数据或 PathData 资源引用 "#id"(语法见 2.9 节) | +| `data` | string/idref | (必填) | SVG 路径数据或 PathData 资源引用 "#id" | | `reversed` | bool | false | 反转路径方向 | #### 5.2.5 TextSpan(文本片段) @@ -1462,7 +1439,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `path` | idref | (必填) | PathData 资源引用 "#id"(见 2.9 节) | +| `path` | idref | (必填) | PathData 资源引用 "#id" | | `align` | TextPathAlign | start | 对齐模式(见下方) | | `firstMargin` | float | 0 | 起始边距 | | `lastMargin` | float | 0 | 结束边距 | @@ -1762,15 +1739,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: --- -## 6. Conformance(一致性) - -### 6.1 解析规则 - -- 未知元素跳过不报错(向前兼容) -- 未知属性忽略 -- 缺失的可选属性使用默认值 -- 无效值尽可能回退到默认值 - --- ## Appendix A. Node Hierarchy(节点层级与包含关系) @@ -1809,8 +1777,7 @@ pagx │ └── Layer* ├── contents - │ └── VectorElement*(见下方) - ├── styles + │ └── VectorElement* ├── styles │ ├── DropShadowStyle │ ├── InnerShadowStyle │ └── BackgroundBlurStyle diff --git a/pagx/include/pagx/types/Color.h b/pagx/include/pagx/types/Color.h new file mode 100644 index 0000000000..04eaf24c27 --- /dev/null +++ b/pagx/include/pagx/types/Color.h @@ -0,0 +1,170 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace pagx { + +namespace detail { +inline int ParseHexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return 0; +} +} // namespace detail + +/** + * An RGBA color with floating-point components in [0, 1]. + */ +struct Color { + float red = 0; + float green = 0; + float blue = 0; + float alpha = 1; + + /** + * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). + */ + static Color FromHex(uint32_t hex, bool hasAlpha = false) { + Color color = {}; + if (hasAlpha) { + color.red = static_cast((hex >> 24) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.blue = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.alpha = static_cast(hex & 0xFF) / 255.0f; + } else { + color.red = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.blue = static_cast(hex & 0xFF) / 255.0f; + color.alpha = 1.0f; + } + return color; + } + + /** + * Returns a Color from RGBA components in [0, 1]. + */ + static Color FromRGBA(float r, float g, float b, float a = 1) { + return {r, g, b, a}; + } + + /** + * Parses a color string. Supports: + * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" + * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" + * Returns black if parsing fails. + */ + static Color Parse(const std::string& str) { + if (str.empty()) { + return {}; + } + if (str[0] == '#') { + auto hex = str.substr(1); + if (hex.size() == 3) { + int r = detail::ParseHexDigit(hex[0]); + int g = detail::ParseHexDigit(hex[1]); + int b = detail::ParseHexDigit(hex[2]); + return Color::FromRGBA(static_cast(r * 17) / 255.0f, + static_cast(g * 17) / 255.0f, + static_cast(b * 17) / 255.0f, 1.0f); + } + if (hex.size() == 6) { + int r = detail::ParseHexDigit(hex[0]) * 16 + detail::ParseHexDigit(hex[1]); + int g = detail::ParseHexDigit(hex[2]) * 16 + detail::ParseHexDigit(hex[3]); + int b = detail::ParseHexDigit(hex[4]) * 16 + detail::ParseHexDigit(hex[5]); + return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, + static_cast(b) / 255.0f, 1.0f); + } + if (hex.size() == 8) { + int r = detail::ParseHexDigit(hex[0]) * 16 + detail::ParseHexDigit(hex[1]); + int g = detail::ParseHexDigit(hex[2]) * 16 + detail::ParseHexDigit(hex[3]); + int b = detail::ParseHexDigit(hex[4]) * 16 + detail::ParseHexDigit(hex[5]); + int a = detail::ParseHexDigit(hex[6]) * 16 + detail::ParseHexDigit(hex[7]); + return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, + static_cast(b) / 255.0f, static_cast(a) / 255.0f); + } + } + if (str.substr(0, 4) == "rgb(" || str.substr(0, 5) == "rgba(") { + auto start = str.find('('); + auto end = str.find(')'); + if (start != std::string::npos && end != std::string::npos) { + auto values = str.substr(start + 1, end - start - 1); + std::istringstream iss(values); + std::string token = {}; + std::vector components = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + components.push_back(std::stof(trimmed)); + } + if (components.size() >= 3) { + float r = components[0] / 255.0f; + float g = components[1] / 255.0f; + float b = components[2] / 255.0f; + float a = components.size() >= 4 ? components[3] : 1.0f; + return Color::FromRGBA(r, g, b, a); + } + } + } + return {}; + } + + /** + * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". + */ + std::string toHexString(bool includeAlpha = false) const { + auto toHex = [](float v) { + int i = static_cast(std::round(v * 255.0f)); + i = std::max(0, std::min(255, i)); + char buf[3] = {}; + snprintf(buf, sizeof(buf), "%02X", i); + return std::string(buf); + }; + std::string result = "#" + toHex(red) + toHex(green) + toHex(blue); + if (includeAlpha && alpha < 1.0f) { + result += toHex(alpha); + } + return result; + } + + bool operator==(const Color& other) const { + return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; + } + + bool operator!=(const Color& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/types/Matrix.h b/pagx/include/pagx/types/Matrix.h new file mode 100644 index 0000000000..2d911d5a8c --- /dev/null +++ b/pagx/include/pagx/types/Matrix.h @@ -0,0 +1,160 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include "pagx/types/Point.h" + +namespace pagx { + +/** + * A 2D affine transformation matrix. + * Matrix form: + * | a c tx | + * | b d ty | + * | 0 0 1 | + */ +struct Matrix { + float a = 1; // scaleX + float b = 0; // skewY + float c = 0; // skewX + float d = 1; // scaleY + float tx = 0; // transX + float ty = 0; // transY + + /** + * Returns the identity matrix. + */ + static Matrix Identity() { + return {}; + } + + /** + * Returns a translation matrix. + */ + static Matrix Translate(float x, float y) { + Matrix m = {}; + m.tx = x; + m.ty = y; + return m; + } + + /** + * Returns a scale matrix. + */ + static Matrix Scale(float sx, float sy) { + Matrix m = {}; + m.a = sx; + m.d = sy; + return m; + } + + /** + * Returns a rotation matrix (angle in degrees). + */ + static Matrix Rotate(float degrees) { + Matrix m = {}; + float radians = degrees * 3.14159265358979323846f / 180.0f; + float cosVal = std::cos(radians); + float sinVal = std::sin(radians); + m.a = cosVal; + m.b = sinVal; + m.c = -sinVal; + m.d = cosVal; + return m; + } + + /** + * Parses a matrix string "a,b,c,d,tx,ty". + */ + static Matrix Parse(const std::string& str) { + Matrix m = {}; + std::istringstream iss(str); + std::string token = {}; + std::vector values = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + if (!trimmed.empty()) { + values.push_back(std::stof(trimmed)); + } + } + if (values.size() >= 6) { + m.a = values[0]; + m.b = values[1]; + m.c = values[2]; + m.d = values[3]; + m.tx = values[4]; + m.ty = values[5]; + } + return m; + } + + /** + * Returns the matrix as a string "a,b,c,d,tx,ty". + */ + std::string toString() const { + std::ostringstream oss = {}; + oss << a << "," << b << "," << c << "," << d << "," << tx << "," << ty; + return oss.str(); + } + + /** + * Returns true if this is the identity matrix. + */ + bool isIdentity() const { + return a == 1 && b == 0 && c == 0 && d == 1 && tx == 0 && ty == 0; + } + + /** + * Concatenates this matrix with another. + */ + Matrix operator*(const Matrix& other) const { + Matrix result = {}; + result.a = a * other.a + c * other.b; + result.b = b * other.a + d * other.b; + result.c = a * other.c + c * other.d; + result.d = b * other.c + d * other.d; + result.tx = a * other.tx + c * other.ty + tx; + result.ty = b * other.tx + d * other.ty + ty; + return result; + } + + /** + * Transforms a point by this matrix. + */ + Point mapPoint(const Point& point) const { + return {a * point.x + c * point.y + tx, b * point.x + d * point.y + ty}; + } + + bool operator==(const Matrix& other) const { + return a == other.a && b == other.b && c == other.c && d == other.d && tx == other.tx && + ty == other.ty; + } + + bool operator!=(const Matrix& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/types/Point.h b/pagx/include/pagx/types/Point.h new file mode 100644 index 0000000000..e4996dcf21 --- /dev/null +++ b/pagx/include/pagx/types/Point.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * A point with x and y coordinates. + */ +struct Point { + float x = 0; + float y = 0; + + bool operator==(const Point& other) const { + return x == other.x && y == other.y; + } + + bool operator!=(const Point& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/types/Rect.h b/pagx/include/pagx/types/Rect.h new file mode 100644 index 0000000000..5adea6206d --- /dev/null +++ b/pagx/include/pagx/types/Rect.h @@ -0,0 +1,79 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * A rectangle defined by position and size. + */ +struct Rect { + float x = 0; + float y = 0; + float width = 0; + float height = 0; + + /** + * Returns a Rect from left, top, right, bottom coordinates. + */ + static Rect MakeLTRB(float l, float t, float r, float b) { + return {l, t, r - l, b - t}; + } + + /** + * Returns a Rect from position and size. + */ + static Rect MakeXYWH(float x, float y, float w, float h) { + return {x, y, w, h}; + } + + float left() const { + return x; + } + + float top() const { + return y; + } + + float right() const { + return x + width; + } + + float bottom() const { + return y + height; + } + + bool isEmpty() const { + return width <= 0 || height <= 0; + } + + void setEmpty() { + x = y = width = height = 0; + } + + bool operator==(const Rect& other) const { + return x == other.x && y == other.y && width == other.width && height == other.height; + } + + bool operator!=(const Rect& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/types/Size.h b/pagx/include/pagx/types/Size.h new file mode 100644 index 0000000000..bb0ea3e110 --- /dev/null +++ b/pagx/include/pagx/types/Size.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * A size with width and height. + */ +struct Size { + float width = 0; + float height = 0; + + bool operator==(const Size& other) const { + return width == other.width && height == other.height; + } + + bool operator!=(const Size& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp index 9628e0c293..549702d10c 100644 --- a/pagx/src/PAGXTypes.cpp +++ b/pagx/src/PAGXTypes.cpp @@ -114,6 +114,80 @@ Color Color::Parse(const std::string& str) { } } } + // CSS Color Level 4: color(colorspace r g b) or color(colorspace r g b / a) + // Supported colorspaces: display-p3, a98-rgb, rec2020, srgb + if (str.substr(0, 6) == "color(") { + auto start = str.find('('); + auto end = str.find(')'); + if (start != std::string::npos && end != std::string::npos) { + auto inner = str.substr(start + 1, end - start - 1); + // Trim whitespace + inner.erase(0, inner.find_first_not_of(" \t")); + inner.erase(inner.find_last_not_of(" \t") + 1); + + // Parse colorspace and values (space-separated) + std::istringstream iss(inner); + std::string colorspace = {}; + iss >> colorspace; + + std::vector components = {}; + std::string token = {}; + float alpha = 1.0f; + bool foundSlash = false; + + while (iss >> token) { + if (token == "/") { + foundSlash = true; + continue; + } + float value = std::stof(token); + if (foundSlash) { + alpha = value; + } else { + components.push_back(value); + } + } + + if (components.size() >= 3) { + float r = components[0]; + float g = components[1]; + float b = components[2]; + + // Convert from wide gamut colorspace to sRGB (approximate clipping). + // For display-p3, a98-rgb, rec2020: values are in 0-1 range. + // We do a simplified conversion by clamping to sRGB gamut. + // A proper implementation would use ICC profiles or matrix transforms. + if (colorspace == "display-p3") { + // Display P3 to sRGB approximate conversion matrix. + float sR = 1.2249f * r - 0.2247f * g - 0.0002f * b; + float sG = -0.0420f * r + 1.0419f * g + 0.0001f * b; + float sB = -0.0197f * r - 0.0786f * g + 1.0983f * b; + r = std::max(0.0f, std::min(1.0f, sR)); + g = std::max(0.0f, std::min(1.0f, sG)); + b = std::max(0.0f, std::min(1.0f, sB)); + } else if (colorspace == "a98-rgb") { + // Adobe RGB to sRGB approximate conversion. + float sR = 1.3982f * r - 0.3982f * g + 0.0f * b; + float sG = 0.0f * r + 1.0f * g + 0.0f * b; + float sB = 0.0f * r - 0.0429f * g + 1.0429f * b; + r = std::max(0.0f, std::min(1.0f, sR)); + g = std::max(0.0f, std::min(1.0f, sG)); + b = std::max(0.0f, std::min(1.0f, sB)); + } else if (colorspace == "rec2020") { + // Rec.2020 to sRGB approximate conversion. + float sR = 1.6605f * r - 0.5877f * g - 0.0728f * b; + float sG = -0.1246f * r + 1.1330f * g - 0.0084f * b; + float sB = -0.0182f * r - 0.1006f * g + 1.1188f * b; + r = std::max(0.0f, std::min(1.0f, sR)); + g = std::max(0.0f, std::min(1.0f, sG)); + b = std::max(0.0f, std::min(1.0f, sB)); + } + // For "srgb" or unknown colorspaces, use values directly. + + return Color::FromRGBA(r, g, b, alpha); + } + } + } return {}; } diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index ac3bd91ba8..bf8fba8fbf 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -892,7 +892,6 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, contents.push_back(std::move(fillNode)); } else { auto fillNode = std::make_unique(); - Color color = parseColor(fill); // Determine effective fill-opacity. std::string fillOpacity = getAttribute(element, "fill-opacity"); @@ -900,9 +899,11 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, fillOpacity = inheritedStyle.fillOpacity; } if (!fillOpacity.empty()) { - color.alpha = std::stof(fillOpacity); + fillNode->alpha = std::stof(fillOpacity); } - fillNode->color = color.toHexString(color.alpha < 1); + + // Store the original color string for later parsing in LayerBuilder. + fillNode->color = fill; // Determine effective fill-rule. std::string fillRule = getAttribute(element, "fill-rule"); @@ -941,17 +942,17 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } } } else { - Color color = parseColor(stroke); - // Determine effective stroke-opacity. std::string strokeOpacity = getAttribute(element, "stroke-opacity"); if (strokeOpacity.empty()) { strokeOpacity = inheritedStyle.strokeOpacity; } if (!strokeOpacity.empty()) { - color.alpha = std::stof(strokeOpacity); + strokeNode->alpha = std::stof(strokeOpacity); } - strokeNode->color = color.toHexString(color.alpha < 1); + + // Store the original color string for later parsing in LayerBuilder. + strokeNode->color = stroke; } std::string strokeWidth = getAttribute(element, "stroke-width"); From e2e280f54d5a6d20552ebe7c03e1adff950fa2be Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 15:46:53 +0800 Subject: [PATCH 078/678] Remove redundant MaskType reference in masking section --- pagx/docs/pagx_spec.md | 5 ++- pagx/include/pagx/nodes/BackgroundBlurStyle.h | 4 +-- pagx/include/pagx/nodes/BlendFilter.h | 4 +-- pagx/include/pagx/nodes/BlurFilter.h | 4 +-- pagx/include/pagx/nodes/ColorMatrixFilter.h | 4 +-- pagx/include/pagx/nodes/ColorSource.h | 33 +++++++++++++++++++ pagx/include/pagx/nodes/ConicGradient.h | 4 +-- pagx/include/pagx/nodes/DiamondGradient.h | 4 +-- pagx/include/pagx/nodes/DropShadowFilter.h | 4 +-- pagx/include/pagx/nodes/DropShadowStyle.h | 4 +-- pagx/include/pagx/nodes/Ellipse.h | 4 +-- pagx/include/pagx/nodes/Fill.h | 7 ++-- pagx/include/pagx/nodes/Filter.h | 33 +++++++++++++++++++ pagx/include/pagx/nodes/Geometry.h | 33 +++++++++++++++++++ pagx/include/pagx/nodes/ImagePattern.h | 4 +-- pagx/include/pagx/nodes/InnerShadowFilter.h | 4 +-- pagx/include/pagx/nodes/InnerShadowStyle.h | 4 +-- pagx/include/pagx/nodes/Layer.h | 6 ++-- pagx/include/pagx/nodes/LayerStyle.h | 33 +++++++++++++++++++ pagx/include/pagx/nodes/LinearGradient.h | 4 +-- pagx/include/pagx/nodes/MergePath.h | 4 +-- pagx/include/pagx/nodes/Painter.h | 33 +++++++++++++++++++ pagx/include/pagx/nodes/Path.h | 4 +-- pagx/include/pagx/nodes/PathModifier.h | 33 +++++++++++++++++++ pagx/include/pagx/nodes/Polystar.h | 4 +-- pagx/include/pagx/nodes/RadialGradient.h | 4 +-- pagx/include/pagx/nodes/Rectangle.h | 4 +-- pagx/include/pagx/nodes/RoundCorner.h | 4 +-- pagx/include/pagx/nodes/SolidColor.h | 4 +-- pagx/include/pagx/nodes/Stroke.h | 7 ++-- pagx/include/pagx/nodes/TextSpan.h | 4 +-- pagx/include/pagx/nodes/TrimPath.h | 4 +-- 32 files changed, 256 insertions(+), 55 deletions(-) create mode 100644 pagx/include/pagx/nodes/ColorSource.h create mode 100644 pagx/include/pagx/nodes/Filter.h create mode 100644 pagx/include/pagx/nodes/Geometry.h create mode 100644 pagx/include/pagx/nodes/LayerStyle.h create mode 100644 pagx/include/pagx/nodes/Painter.h create mode 100644 pagx/include/pagx/nodes/PathModifier.h diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 0fd61682ce..fdde4ccd5b 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -813,8 +813,6 @@ PAGX 文档采用层级结构组织内容: ``` -**遮罩类型**:MaskType 枚举见 Layer 属性定义。 - **遮罩规则**: - 遮罩图层自身不渲染(`visible` 属性被忽略) - 遮罩图层的变换不影响被遮罩图层 @@ -1777,7 +1775,8 @@ pagx │ └── Layer* ├── contents - │ └── VectorElement* ├── styles + │ └── VectorElement*(见下方) + ├── styles │ ├── DropShadowStyle │ ├── InnerShadowStyle │ └── BackgroundBlurStyle diff --git a/pagx/include/pagx/nodes/BackgroundBlurStyle.h b/pagx/include/pagx/nodes/BackgroundBlurStyle.h index 1718f99e85..3211d46e58 100644 --- a/pagx/include/pagx/nodes/BackgroundBlurStyle.h +++ b/pagx/include/pagx/nodes/BackgroundBlurStyle.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/LayerStyle.h" #include "pagx/types/BlendMode.h" #include "pagx/types/TileMode.h" @@ -27,7 +27,7 @@ namespace pagx { /** * Background blur style. */ -struct BackgroundBlurStyle : public Node { +struct BackgroundBlurStyle : public LayerStyle { float blurrinessX = 0; float blurrinessY = 0; TileMode tileMode = TileMode::Mirror; diff --git a/pagx/include/pagx/nodes/BlendFilter.h b/pagx/include/pagx/nodes/BlendFilter.h index 36835a18f5..c890f6bfc7 100644 --- a/pagx/include/pagx/nodes/BlendFilter.h +++ b/pagx/include/pagx/nodes/BlendFilter.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Filter.h" #include "pagx/types/BlendMode.h" #include "pagx/types/Types.h" @@ -27,7 +27,7 @@ namespace pagx { /** * Blend filter. */ -struct BlendFilter : public Node { +struct BlendFilter : public Filter { Color color = {}; BlendMode blendMode = BlendMode::Normal; diff --git a/pagx/include/pagx/nodes/BlurFilter.h b/pagx/include/pagx/nodes/BlurFilter.h index ed20482bed..b3461f2562 100644 --- a/pagx/include/pagx/nodes/BlurFilter.h +++ b/pagx/include/pagx/nodes/BlurFilter.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Filter.h" #include "pagx/types/TileMode.h" namespace pagx { @@ -26,7 +26,7 @@ namespace pagx { /** * Blur filter. */ -struct BlurFilter : public Node { +struct BlurFilter : public Filter { float blurrinessX = 0; float blurrinessY = 0; TileMode tileMode = TileMode::Decal; diff --git a/pagx/include/pagx/nodes/ColorMatrixFilter.h b/pagx/include/pagx/nodes/ColorMatrixFilter.h index 9c25ae7411..7cf9e8b3e5 100644 --- a/pagx/include/pagx/nodes/ColorMatrixFilter.h +++ b/pagx/include/pagx/nodes/ColorMatrixFilter.h @@ -19,14 +19,14 @@ #pragma once #include -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Filter.h" namespace pagx { /** * Color matrix filter. */ -struct ColorMatrixFilter : public Node { +struct ColorMatrixFilter : public Filter { std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; NodeType type() const override { diff --git a/pagx/include/pagx/nodes/ColorSource.h b/pagx/include/pagx/nodes/ColorSource.h new file mode 100644 index 0000000000..22673b8757 --- /dev/null +++ b/pagx/include/pagx/nodes/ColorSource.h @@ -0,0 +1,33 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Base class for color sources (SolidColor, gradients, ImagePattern). + */ +class ColorSource : public Node { + protected: + ColorSource() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/ConicGradient.h b/pagx/include/pagx/nodes/ConicGradient.h index 7cd45c5083..e8b62d8b39 100644 --- a/pagx/include/pagx/nodes/ConicGradient.h +++ b/pagx/include/pagx/nodes/ConicGradient.h @@ -20,8 +20,8 @@ #include #include +#include "pagx/nodes/ColorSource.h" #include "pagx/nodes/ColorStop.h" -#include "pagx/nodes/Node.h" #include "pagx/types/Types.h" namespace pagx { @@ -29,7 +29,7 @@ namespace pagx { /** * A conic (sweep) gradient. */ -struct ConicGradient : public Node { +struct ConicGradient : public ColorSource { std::string id = {}; Point center = {}; float startAngle = 0; diff --git a/pagx/include/pagx/nodes/DiamondGradient.h b/pagx/include/pagx/nodes/DiamondGradient.h index 76203115d8..269da2bf14 100644 --- a/pagx/include/pagx/nodes/DiamondGradient.h +++ b/pagx/include/pagx/nodes/DiamondGradient.h @@ -20,8 +20,8 @@ #include #include +#include "pagx/nodes/ColorSource.h" #include "pagx/nodes/ColorStop.h" -#include "pagx/nodes/Node.h" #include "pagx/types/Types.h" namespace pagx { @@ -29,7 +29,7 @@ namespace pagx { /** * A diamond gradient. */ -struct DiamondGradient : public Node { +struct DiamondGradient : public ColorSource { std::string id = {}; Point center = {}; float halfDiagonal = 0; diff --git a/pagx/include/pagx/nodes/DropShadowFilter.h b/pagx/include/pagx/nodes/DropShadowFilter.h index 4b65e264dc..9b71e755a9 100644 --- a/pagx/include/pagx/nodes/DropShadowFilter.h +++ b/pagx/include/pagx/nodes/DropShadowFilter.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Filter.h" #include "pagx/types/Types.h" namespace pagx { @@ -26,7 +26,7 @@ namespace pagx { /** * Drop shadow filter. */ -struct DropShadowFilter : public Node { +struct DropShadowFilter : public Filter { float offsetX = 0; float offsetY = 0; float blurrinessX = 0; diff --git a/pagx/include/pagx/nodes/DropShadowStyle.h b/pagx/include/pagx/nodes/DropShadowStyle.h index ef6b2d83bf..5b3c1159d1 100644 --- a/pagx/include/pagx/nodes/DropShadowStyle.h +++ b/pagx/include/pagx/nodes/DropShadowStyle.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/LayerStyle.h" #include "pagx/types/BlendMode.h" #include "pagx/types/Types.h" @@ -27,7 +27,7 @@ namespace pagx { /** * Drop shadow style. */ -struct DropShadowStyle : public Node { +struct DropShadowStyle : public LayerStyle { float offsetX = 0; float offsetY = 0; float blurrinessX = 0; diff --git a/pagx/include/pagx/nodes/Ellipse.h b/pagx/include/pagx/nodes/Ellipse.h index f458116349..f7a36ef23f 100644 --- a/pagx/include/pagx/nodes/Ellipse.h +++ b/pagx/include/pagx/nodes/Ellipse.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Geometry.h" #include "pagx/types/Types.h" namespace pagx { @@ -26,7 +26,7 @@ namespace pagx { /** * An ellipse shape. */ -struct Ellipse : public Node { +struct Ellipse : public Geometry { Point center = {}; Size size = {100, 100}; bool reversed = false; diff --git a/pagx/include/pagx/nodes/Fill.h b/pagx/include/pagx/nodes/Fill.h index db2a9df86c..b7d3271c46 100644 --- a/pagx/include/pagx/nodes/Fill.h +++ b/pagx/include/pagx/nodes/Fill.h @@ -20,7 +20,8 @@ #include #include -#include "pagx/nodes/Node.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/Painter.h" #include "pagx/types/BlendMode.h" #include "pagx/types/FillRule.h" #include "pagx/types/Placement.h" @@ -32,9 +33,9 @@ namespace pagx { * The color can be a simple color string ("#FF0000"), a reference ("#gradientId"), * or an inline color source node. */ -struct Fill : public Node { +struct Fill : public Painter { std::string color = {}; - std::unique_ptr colorSource = nullptr; + std::unique_ptr colorSource = nullptr; float alpha = 1; BlendMode blendMode = BlendMode::Normal; FillRule fillRule = FillRule::Winding; diff --git a/pagx/include/pagx/nodes/Filter.h b/pagx/include/pagx/nodes/Filter.h new file mode 100644 index 0000000000..a8a21b30b0 --- /dev/null +++ b/pagx/include/pagx/nodes/Filter.h @@ -0,0 +1,33 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Base class for filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, ColorMatrixFilter). + */ +class Filter : public Node { + protected: + Filter() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Geometry.h b/pagx/include/pagx/nodes/Geometry.h new file mode 100644 index 0000000000..cd7400743f --- /dev/null +++ b/pagx/include/pagx/nodes/Geometry.h @@ -0,0 +1,33 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Base class for geometry elements (Rectangle, Ellipse, Polystar, Path, TextSpan). + */ +class Geometry : public Node { + protected: + Geometry() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/ImagePattern.h b/pagx/include/pagx/nodes/ImagePattern.h index 2457c916d1..b15c9b5d9c 100644 --- a/pagx/include/pagx/nodes/ImagePattern.h +++ b/pagx/include/pagx/nodes/ImagePattern.h @@ -19,7 +19,7 @@ #pragma once #include -#include "pagx/nodes/Node.h" +#include "pagx/nodes/ColorSource.h" #include "pagx/types/SamplingMode.h" #include "pagx/types/TileMode.h" #include "pagx/types/Types.h" @@ -29,7 +29,7 @@ namespace pagx { /** * An image pattern. */ -struct ImagePattern : public Node { +struct ImagePattern : public ColorSource { std::string id = {}; std::string image = {}; TileMode tileModeX = TileMode::Clamp; diff --git a/pagx/include/pagx/nodes/InnerShadowFilter.h b/pagx/include/pagx/nodes/InnerShadowFilter.h index 9a3ae2d50e..a62b1cd71d 100644 --- a/pagx/include/pagx/nodes/InnerShadowFilter.h +++ b/pagx/include/pagx/nodes/InnerShadowFilter.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Filter.h" #include "pagx/types/Types.h" namespace pagx { @@ -26,7 +26,7 @@ namespace pagx { /** * Inner shadow filter. */ -struct InnerShadowFilter : public Node { +struct InnerShadowFilter : public Filter { float offsetX = 0; float offsetY = 0; float blurrinessX = 0; diff --git a/pagx/include/pagx/nodes/InnerShadowStyle.h b/pagx/include/pagx/nodes/InnerShadowStyle.h index dcfc84c8cb..07627b3dba 100644 --- a/pagx/include/pagx/nodes/InnerShadowStyle.h +++ b/pagx/include/pagx/nodes/InnerShadowStyle.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/LayerStyle.h" #include "pagx/types/BlendMode.h" #include "pagx/types/Types.h" @@ -27,7 +27,7 @@ namespace pagx { /** * Inner shadow style. */ -struct InnerShadowStyle : public Node { +struct InnerShadowStyle : public LayerStyle { float offsetX = 0; float offsetY = 0; float blurrinessX = 0; diff --git a/pagx/include/pagx/nodes/Layer.h b/pagx/include/pagx/nodes/Layer.h index 0c9229a3c4..2d39a7dd11 100644 --- a/pagx/include/pagx/nodes/Layer.h +++ b/pagx/include/pagx/nodes/Layer.h @@ -22,6 +22,8 @@ #include #include #include +#include "pagx/nodes/Filter.h" +#include "pagx/nodes/LayerStyle.h" #include "pagx/nodes/Node.h" #include "pagx/types/BlendMode.h" #include "pagx/types/MaskType.h" @@ -54,8 +56,8 @@ struct Layer : public Node { std::string composition = {}; std::vector> contents = {}; - std::vector> styles = {}; - std::vector> filters = {}; + std::vector> styles = {}; + std::vector> filters = {}; std::vector> children = {}; // Custom data from SVG data-* attributes (key without "data-" prefix) diff --git a/pagx/include/pagx/nodes/LayerStyle.h b/pagx/include/pagx/nodes/LayerStyle.h new file mode 100644 index 0000000000..ccf0f36c5d --- /dev/null +++ b/pagx/include/pagx/nodes/LayerStyle.h @@ -0,0 +1,33 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle). + */ +class LayerStyle : public Node { + protected: + LayerStyle() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/LinearGradient.h b/pagx/include/pagx/nodes/LinearGradient.h index 12ce673713..ea486fcbc8 100644 --- a/pagx/include/pagx/nodes/LinearGradient.h +++ b/pagx/include/pagx/nodes/LinearGradient.h @@ -20,8 +20,8 @@ #include #include +#include "pagx/nodes/ColorSource.h" #include "pagx/nodes/ColorStop.h" -#include "pagx/nodes/Node.h" #include "pagx/types/Types.h" namespace pagx { @@ -29,7 +29,7 @@ namespace pagx { /** * A linear gradient. */ -struct LinearGradient : public Node { +struct LinearGradient : public ColorSource { std::string id = {}; Point startPoint = {}; Point endPoint = {}; diff --git a/pagx/include/pagx/nodes/MergePath.h b/pagx/include/pagx/nodes/MergePath.h index 2164df93bc..18bf2ecf01 100644 --- a/pagx/include/pagx/nodes/MergePath.h +++ b/pagx/include/pagx/nodes/MergePath.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/PathModifier.h" #include "pagx/types/MergePathMode.h" namespace pagx { @@ -26,7 +26,7 @@ namespace pagx { /** * Merge path modifier. */ -struct MergePath : public Node { +struct MergePath : public PathModifier { MergePathMode mode = MergePathMode::Append; NodeType type() const override { diff --git a/pagx/include/pagx/nodes/Painter.h b/pagx/include/pagx/nodes/Painter.h new file mode 100644 index 0000000000..769eab9814 --- /dev/null +++ b/pagx/include/pagx/nodes/Painter.h @@ -0,0 +1,33 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Base class for painters (Fill, Stroke). + */ +class Painter : public Node { + protected: + Painter() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Path.h b/pagx/include/pagx/nodes/Path.h index cba34e8131..ada9f864e7 100644 --- a/pagx/include/pagx/nodes/Path.h +++ b/pagx/include/pagx/nodes/Path.h @@ -19,14 +19,14 @@ #pragma once #include "pagx/PathData.h" -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Geometry.h" namespace pagx { /** * A path shape. */ -struct Path : public Node { +struct Path : public Geometry { PathData data = {}; bool reversed = false; diff --git a/pagx/include/pagx/nodes/PathModifier.h b/pagx/include/pagx/nodes/PathModifier.h new file mode 100644 index 0000000000..4b346453e5 --- /dev/null +++ b/pagx/include/pagx/nodes/PathModifier.h @@ -0,0 +1,33 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Base class for path modifiers (TrimPath, RoundCorner, MergePath). + */ +class PathModifier : public Node { + protected: + PathModifier() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Polystar.h b/pagx/include/pagx/nodes/Polystar.h index b2cccd9c53..2a746af995 100644 --- a/pagx/include/pagx/nodes/Polystar.h +++ b/pagx/include/pagx/nodes/Polystar.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Geometry.h" #include "pagx/types/PolystarType.h" #include "pagx/types/Types.h" @@ -27,7 +27,7 @@ namespace pagx { /** * A polygon or star shape. */ -struct Polystar : public Node { +struct Polystar : public Geometry { Point center = {}; PolystarType polystarType = PolystarType::Star; float pointCount = 5; diff --git a/pagx/include/pagx/nodes/RadialGradient.h b/pagx/include/pagx/nodes/RadialGradient.h index b6ddd322ee..06158c9774 100644 --- a/pagx/include/pagx/nodes/RadialGradient.h +++ b/pagx/include/pagx/nodes/RadialGradient.h @@ -20,8 +20,8 @@ #include #include +#include "pagx/nodes/ColorSource.h" #include "pagx/nodes/ColorStop.h" -#include "pagx/nodes/Node.h" #include "pagx/types/Types.h" namespace pagx { @@ -29,7 +29,7 @@ namespace pagx { /** * A radial gradient. */ -struct RadialGradient : public Node { +struct RadialGradient : public ColorSource { std::string id = {}; Point center = {}; float radius = 0; diff --git a/pagx/include/pagx/nodes/Rectangle.h b/pagx/include/pagx/nodes/Rectangle.h index 448f3490fc..ae22e702a2 100644 --- a/pagx/include/pagx/nodes/Rectangle.h +++ b/pagx/include/pagx/nodes/Rectangle.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Geometry.h" #include "pagx/types/Types.h" namespace pagx { @@ -26,7 +26,7 @@ namespace pagx { /** * A rectangle shape. */ -struct Rectangle : public Node { +struct Rectangle : public Geometry { Point center = {}; Size size = {100, 100}; float roundness = 0; diff --git a/pagx/include/pagx/nodes/RoundCorner.h b/pagx/include/pagx/nodes/RoundCorner.h index 98e9b6e540..41e63064df 100644 --- a/pagx/include/pagx/nodes/RoundCorner.h +++ b/pagx/include/pagx/nodes/RoundCorner.h @@ -18,14 +18,14 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/PathModifier.h" namespace pagx { /** * Round corner modifier. */ -struct RoundCorner : public Node { +struct RoundCorner : public PathModifier { float radius = 10; NodeType type() const override { diff --git a/pagx/include/pagx/nodes/SolidColor.h b/pagx/include/pagx/nodes/SolidColor.h index 2f314120dc..ce1cad497b 100644 --- a/pagx/include/pagx/nodes/SolidColor.h +++ b/pagx/include/pagx/nodes/SolidColor.h @@ -19,7 +19,7 @@ #pragma once #include -#include "pagx/nodes/Node.h" +#include "pagx/nodes/ColorSource.h" #include "pagx/types/Types.h" namespace pagx { @@ -27,7 +27,7 @@ namespace pagx { /** * A solid color. */ -struct SolidColor : public Node { +struct SolidColor : public ColorSource { std::string id = {}; Color color = {}; diff --git a/pagx/include/pagx/nodes/Stroke.h b/pagx/include/pagx/nodes/Stroke.h index a38c6a99f0..a9b4bede89 100644 --- a/pagx/include/pagx/nodes/Stroke.h +++ b/pagx/include/pagx/nodes/Stroke.h @@ -21,7 +21,8 @@ #include #include #include -#include "pagx/nodes/Node.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/Painter.h" #include "pagx/types/BlendMode.h" #include "pagx/types/LineCap.h" #include "pagx/types/LineJoin.h" @@ -33,9 +34,9 @@ namespace pagx { /** * A stroke painter. */ -struct Stroke : public Node { +struct Stroke : public Painter { std::string color = {}; - std::unique_ptr colorSource = nullptr; + std::unique_ptr colorSource = nullptr; float width = 1; float alpha = 1; BlendMode blendMode = BlendMode::Normal; diff --git a/pagx/include/pagx/nodes/TextSpan.h b/pagx/include/pagx/nodes/TextSpan.h index 4fe16eed4b..36bd2bf637 100644 --- a/pagx/include/pagx/nodes/TextSpan.h +++ b/pagx/include/pagx/nodes/TextSpan.h @@ -19,7 +19,7 @@ #pragma once #include -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Geometry.h" #include "pagx/types/FontStyle.h" namespace pagx { @@ -27,7 +27,7 @@ namespace pagx { /** * A text span. */ -struct TextSpan : public Node { +struct TextSpan : public Geometry { float x = 0; float y = 0; std::string font = {}; diff --git a/pagx/include/pagx/nodes/TrimPath.h b/pagx/include/pagx/nodes/TrimPath.h index 1c5acd9309..e7369cda55 100644 --- a/pagx/include/pagx/nodes/TrimPath.h +++ b/pagx/include/pagx/nodes/TrimPath.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/PathModifier.h" #include "pagx/types/TrimType.h" namespace pagx { @@ -26,7 +26,7 @@ namespace pagx { /** * Trim path modifier. */ -struct TrimPath : public Node { +struct TrimPath : public PathModifier { float start = 0; float end = 1; float offset = 0; From 074b36c03dcc0117d84d52fd00f33835913cfaf2 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 16:09:35 +0800 Subject: [PATCH 079/678] Simplify pagx CMakeLists.txt to use automatic file discovery Replace explicit file listings with file(GLOB) and file(GLOB_RECURSE) to automatically find source files in each directory: - src/*.cpp for core sources - src/xml/*.cpp for XML parser - src/svg/*.cpp for SVG parser - src/tgfx/*.cpp for tgfx adapter This approach is consistent with libpag's CMake configuration style. --- pagx/CMakeLists.txt | 17 +-- pagx/include/pagx/PAGXDocument.h | 7 +- pagx/include/pagx/PAGXModel.h | 11 +- pagx/include/pagx/nodes/BackgroundBlurStyle.h | 7 +- pagx/include/pagx/nodes/BlendFilter.h | 9 +- pagx/include/pagx/nodes/BlurFilter.h | 9 +- pagx/include/pagx/nodes/ColorMatrixFilter.h | 9 +- pagx/include/pagx/nodes/ColorSource.h | 23 ++++ pagx/include/pagx/nodes/ColorStop.h | 8 +- pagx/include/pagx/nodes/Composition.h | 12 +- pagx/include/pagx/nodes/ConicGradient.h | 7 +- pagx/include/pagx/nodes/DiamondGradient.h | 7 +- pagx/include/pagx/nodes/DropShadowFilter.h | 9 +- pagx/include/pagx/nodes/DropShadowStyle.h | 7 +- pagx/include/pagx/nodes/Element.h | 74 +++++++++++ pagx/include/pagx/nodes/Ellipse.h | 7 +- pagx/include/pagx/nodes/Fill.h | 7 +- pagx/include/pagx/nodes/Geometry.h | 4 +- pagx/include/pagx/nodes/Group.h | 11 +- pagx/include/pagx/nodes/Image.h | 10 +- pagx/include/pagx/nodes/ImagePattern.h | 7 +- pagx/include/pagx/nodes/InnerShadowFilter.h | 9 +- pagx/include/pagx/nodes/InnerShadowStyle.h | 7 +- pagx/include/pagx/nodes/Layer.h | 10 +- pagx/include/pagx/nodes/LayerFilter.h | 55 ++++++++ pagx/include/pagx/nodes/LayerStyle.h | 20 +++ pagx/include/pagx/nodes/LinearGradient.h | 7 +- pagx/include/pagx/nodes/MergePath.h | 7 +- pagx/include/pagx/nodes/Painter.h | 4 +- pagx/include/pagx/nodes/Path.h | 7 +- pagx/include/pagx/nodes/PathDataResource.h | 10 +- pagx/include/pagx/nodes/PathModifier.h | 4 +- pagx/include/pagx/nodes/Polystar.h | 7 +- pagx/include/pagx/nodes/RadialGradient.h | 7 +- pagx/include/pagx/nodes/RangeSelector.h | 8 +- pagx/include/pagx/nodes/Rectangle.h | 7 +- pagx/include/pagx/nodes/Repeater.h | 9 +- pagx/include/pagx/nodes/Resource.h | 123 ++++++++++++++++++ pagx/include/pagx/nodes/RoundCorner.h | 7 +- pagx/include/pagx/nodes/SolidColor.h | 7 +- pagx/include/pagx/nodes/Stroke.h | 7 +- .../pagx/nodes/{Filter.h => TextAnimator.h} | 10 +- pagx/include/pagx/nodes/TextLayout.h | 9 +- pagx/include/pagx/nodes/TextModifier.h | 9 +- pagx/include/pagx/nodes/TextPath.h | 9 +- pagx/include/pagx/nodes/TextSpan.h | 7 +- pagx/include/pagx/nodes/TrimPath.h | 7 +- pagx/src/PAGXElement.cpp | 60 +++++++++ pagx/src/PAGXNode.cpp | 55 +++++++- pagx/src/PAGXResource.cpp | 92 +++++++++++++ 50 files changed, 739 insertions(+), 103 deletions(-) create mode 100644 pagx/include/pagx/nodes/Element.h create mode 100644 pagx/include/pagx/nodes/LayerFilter.h create mode 100644 pagx/include/pagx/nodes/Resource.h rename pagx/include/pagx/nodes/{Filter.h => TextAnimator.h} (78%) create mode 100644 pagx/src/PAGXElement.cpp create mode 100644 pagx/src/PAGXResource.cpp diff --git a/pagx/CMakeLists.txt b/pagx/CMakeLists.txt index 46436d3551..9f422b87a6 100644 --- a/pagx/CMakeLists.txt +++ b/pagx/CMakeLists.txt @@ -9,24 +9,17 @@ option(PAGX_BUILD_TGFX_ADAPTER "Build PAGX to tgfx adapter (LayerBuilder)" ON) # ============== Core PAGX Library (Independent, no tgfx dependency) ============== -# Core sources -file(GLOB PAGX_CORE_SOURCES - src/PAGXNode.cpp - src/PAGXTypes.cpp - src/PAGXDocument.cpp - src/PAGXXMLParser.cpp - src/PAGXXMLWriter.cpp - src/PathData.cpp -) +# Collect core sources (excluding subdirectories) +file(GLOB PAGX_CORE_SOURCES src/*.cpp) list(APPEND PAGX_SOURCES ${PAGX_CORE_SOURCES}) # XML parser sources (based on expat) -file(GLOB PAGX_XML_SOURCES src/xml/*.cpp) +file(GLOB_RECURSE PAGX_XML_SOURCES src/xml/*.cpp) list(APPEND PAGX_SOURCES ${PAGX_XML_SOURCES}) # SVG parser (also independent of tgfx) if (PAGX_BUILD_SVG) - file(GLOB PAGX_SVG_SOURCES src/svg/*.cpp) + file(GLOB_RECURSE PAGX_SVG_SOURCES src/svg/*.cpp) list(APPEND PAGX_SOURCES ${PAGX_SVG_SOURCES}) endif() @@ -35,7 +28,7 @@ if (PAGX_BUILD_TGFX_ADAPTER) if (NOT TARGET tgfx) message(FATAL_ERROR "tgfx target not found. Set PAGX_BUILD_TGFX_ADAPTER=OFF to build without tgfx adapter.") endif() - file(GLOB PAGX_TGFX_SOURCES src/tgfx/*.cpp) + file(GLOB_RECURSE PAGX_TGFX_SOURCES src/tgfx/*.cpp) list(APPEND PAGX_SOURCES ${PAGX_TGFX_SOURCES}) endif() diff --git a/pagx/include/pagx/PAGXDocument.h b/pagx/include/pagx/PAGXDocument.h index a16eae283c..c52d32774e 100644 --- a/pagx/include/pagx/PAGXDocument.h +++ b/pagx/include/pagx/PAGXDocument.h @@ -23,6 +23,7 @@ #include #include #include "pagx/PAGXModel.h" +#include "pagx/nodes/Resource.h" namespace pagx { @@ -54,7 +55,7 @@ class PAGXDocument { * Resources (images, gradients, compositions, etc.). * These can be referenced by "#id" in the document. */ - std::vector> resources = {}; + std::vector> resources = {};; /** * Top-level layers. @@ -98,7 +99,7 @@ class PAGXDocument { * Finds a resource by ID. * Returns nullptr if not found. */ - Node* findResource(const std::string& id) const; + Resource* findResource(const std::string& id) const; /** * Finds a layer by ID (searches recursively). @@ -110,7 +111,7 @@ class PAGXDocument { friend class PAGXXMLParser; PAGXDocument() = default; - mutable std::unordered_map resourceMap = {}; + mutable std::unordered_map resourceMap = {}; mutable bool resourceMapDirty = true; void rebuildResourceMap() const; diff --git a/pagx/include/pagx/PAGXModel.h b/pagx/include/pagx/PAGXModel.h index 87b44c5293..da73034786 100644 --- a/pagx/include/pagx/PAGXModel.h +++ b/pagx/include/pagx/PAGXModel.h @@ -22,8 +22,16 @@ #include "pagx/types/Enums.h" #include "pagx/types/Types.h" -// Base class +// Base classes +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/Geometry.h" +#include "pagx/nodes/LayerFilter.h" +#include "pagx/nodes/LayerStyle.h" #include "pagx/nodes/Node.h" +#include "pagx/nodes/Painter.h" +#include "pagx/nodes/PathModifier.h" +#include "pagx/nodes/TextAnimator.h" // Color sources #include "pagx/nodes/ColorStop.h" @@ -76,6 +84,7 @@ #include "pagx/nodes/Composition.h" #include "pagx/nodes/Image.h" #include "pagx/nodes/PathDataResource.h" +#include "pagx/nodes/Resource.h" // Layer #include "pagx/nodes/Layer.h" diff --git a/pagx/include/pagx/nodes/BackgroundBlurStyle.h b/pagx/include/pagx/nodes/BackgroundBlurStyle.h index 3211d46e58..33b77c4ce7 100644 --- a/pagx/include/pagx/nodes/BackgroundBlurStyle.h +++ b/pagx/include/pagx/nodes/BackgroundBlurStyle.h @@ -27,7 +27,8 @@ namespace pagx { /** * Background blur style. */ -struct BackgroundBlurStyle : public LayerStyle { +class BackgroundBlurStyle : public LayerStyle { + public: float blurrinessX = 0; float blurrinessY = 0; TileMode tileMode = TileMode::Mirror; @@ -36,6 +37,10 @@ struct BackgroundBlurStyle : public LayerStyle { NodeType type() const override { return NodeType::BackgroundBlurStyle; } + + LayerStyleType layerStyleType() const override { + return LayerStyleType::BackgroundBlurStyle; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/BlendFilter.h b/pagx/include/pagx/nodes/BlendFilter.h index c890f6bfc7..42dad0d306 100644 --- a/pagx/include/pagx/nodes/BlendFilter.h +++ b/pagx/include/pagx/nodes/BlendFilter.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Filter.h" +#include "pagx/nodes/LayerFilter.h" #include "pagx/types/BlendMode.h" #include "pagx/types/Types.h" @@ -27,13 +27,18 @@ namespace pagx { /** * Blend filter. */ -struct BlendFilter : public Filter { +class BlendFilter : public LayerFilter { + public: Color color = {}; BlendMode blendMode = BlendMode::Normal; NodeType type() const override { return NodeType::BlendFilter; } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::BlendFilter; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/BlurFilter.h b/pagx/include/pagx/nodes/BlurFilter.h index b3461f2562..825c0a9e67 100644 --- a/pagx/include/pagx/nodes/BlurFilter.h +++ b/pagx/include/pagx/nodes/BlurFilter.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Filter.h" +#include "pagx/nodes/LayerFilter.h" #include "pagx/types/TileMode.h" namespace pagx { @@ -26,7 +26,8 @@ namespace pagx { /** * Blur filter. */ -struct BlurFilter : public Filter { +class BlurFilter : public LayerFilter { + public: float blurrinessX = 0; float blurrinessY = 0; TileMode tileMode = TileMode::Decal; @@ -34,6 +35,10 @@ struct BlurFilter : public Filter { NodeType type() const override { return NodeType::BlurFilter; } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::BlurFilter; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/ColorMatrixFilter.h b/pagx/include/pagx/nodes/ColorMatrixFilter.h index 7cf9e8b3e5..82be2947b7 100644 --- a/pagx/include/pagx/nodes/ColorMatrixFilter.h +++ b/pagx/include/pagx/nodes/ColorMatrixFilter.h @@ -19,19 +19,24 @@ #pragma once #include -#include "pagx/nodes/Filter.h" +#include "pagx/nodes/LayerFilter.h" namespace pagx { /** * Color matrix filter. */ -struct ColorMatrixFilter : public Filter { +class ColorMatrixFilter : public LayerFilter { + public: std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; NodeType type() const override { return NodeType::ColorMatrixFilter; } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::ColorMatrixFilter; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/ColorSource.h b/pagx/include/pagx/nodes/ColorSource.h index 22673b8757..f351244ac8 100644 --- a/pagx/include/pagx/nodes/ColorSource.h +++ b/pagx/include/pagx/nodes/ColorSource.h @@ -22,10 +22,33 @@ namespace pagx { +/** + * Color source types. + */ +enum class ColorSourceType { + SolidColor, + LinearGradient, + RadialGradient, + ConicGradient, + DiamondGradient, + ImagePattern +}; + +/** + * Returns the string name of a color source type. + */ +const char* ColorSourceTypeName(ColorSourceType type); + /** * Base class for color sources (SolidColor, gradients, ImagePattern). */ class ColorSource : public Node { + public: + /** + * Returns the color source type of this color source. + */ + virtual ColorSourceType colorSourceType() const = 0; + protected: ColorSource() = default; }; diff --git a/pagx/include/pagx/nodes/ColorStop.h b/pagx/include/pagx/nodes/ColorStop.h index 88a49587ba..3c4d39dc04 100644 --- a/pagx/include/pagx/nodes/ColorStop.h +++ b/pagx/include/pagx/nodes/ColorStop.h @@ -18,7 +18,6 @@ #pragma once -#include "pagx/nodes/Node.h" #include "pagx/types/Types.h" namespace pagx { @@ -26,13 +25,10 @@ namespace pagx { /** * A color stop in a gradient. */ -struct ColorStop : public Node { +class ColorStop { + public: float offset = 0; Color color = {}; - - NodeType type() const override { - return NodeType::ColorStop; - } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Composition.h b/pagx/include/pagx/nodes/Composition.h index 78da834577..72435463e7 100644 --- a/pagx/include/pagx/nodes/Composition.h +++ b/pagx/include/pagx/nodes/Composition.h @@ -21,24 +21,22 @@ #include #include #include -#include "pagx/nodes/Node.h" namespace pagx { -struct Layer; +class Layer; /** * Composition resource. */ -struct Composition : public Node { +class Composition { + public: + virtual ~Composition() = default; + std::string id = {}; float width = 0; float height = 0; std::vector> layers = {}; - - NodeType type() const override { - return NodeType::Composition; - } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/ConicGradient.h b/pagx/include/pagx/nodes/ConicGradient.h index e8b62d8b39..65713dd135 100644 --- a/pagx/include/pagx/nodes/ConicGradient.h +++ b/pagx/include/pagx/nodes/ConicGradient.h @@ -29,7 +29,8 @@ namespace pagx { /** * A conic (sweep) gradient. */ -struct ConicGradient : public ColorSource { +class ConicGradient : public ColorSource { + public: std::string id = {}; Point center = {}; float startAngle = 0; @@ -40,6 +41,10 @@ struct ConicGradient : public ColorSource { NodeType type() const override { return NodeType::ConicGradient; } + + ColorSourceType colorSourceType() const override { + return ColorSourceType::ConicGradient; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/DiamondGradient.h b/pagx/include/pagx/nodes/DiamondGradient.h index 269da2bf14..1acecab9a1 100644 --- a/pagx/include/pagx/nodes/DiamondGradient.h +++ b/pagx/include/pagx/nodes/DiamondGradient.h @@ -29,7 +29,8 @@ namespace pagx { /** * A diamond gradient. */ -struct DiamondGradient : public ColorSource { +class DiamondGradient : public ColorSource { + public: std::string id = {}; Point center = {}; float halfDiagonal = 0; @@ -39,6 +40,10 @@ struct DiamondGradient : public ColorSource { NodeType type() const override { return NodeType::DiamondGradient; } + + ColorSourceType colorSourceType() const override { + return ColorSourceType::DiamondGradient; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/DropShadowFilter.h b/pagx/include/pagx/nodes/DropShadowFilter.h index 9b71e755a9..c2df906fd2 100644 --- a/pagx/include/pagx/nodes/DropShadowFilter.h +++ b/pagx/include/pagx/nodes/DropShadowFilter.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Filter.h" +#include "pagx/nodes/LayerFilter.h" #include "pagx/types/Types.h" namespace pagx { @@ -26,7 +26,8 @@ namespace pagx { /** * Drop shadow filter. */ -struct DropShadowFilter : public Filter { +class DropShadowFilter : public LayerFilter { + public: float offsetX = 0; float offsetY = 0; float blurrinessX = 0; @@ -37,6 +38,10 @@ struct DropShadowFilter : public Filter { NodeType type() const override { return NodeType::DropShadowFilter; } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::DropShadowFilter; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/DropShadowStyle.h b/pagx/include/pagx/nodes/DropShadowStyle.h index 5b3c1159d1..8dd0bc74c2 100644 --- a/pagx/include/pagx/nodes/DropShadowStyle.h +++ b/pagx/include/pagx/nodes/DropShadowStyle.h @@ -27,7 +27,8 @@ namespace pagx { /** * Drop shadow style. */ -struct DropShadowStyle : public LayerStyle { +class DropShadowStyle : public LayerStyle { + public: float offsetX = 0; float offsetY = 0; float blurrinessX = 0; @@ -39,6 +40,10 @@ struct DropShadowStyle : public LayerStyle { NodeType type() const override { return NodeType::DropShadowStyle; } + + LayerStyleType layerStyleType() const override { + return LayerStyleType::DropShadowStyle; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Element.h b/pagx/include/pagx/nodes/Element.h new file mode 100644 index 0000000000..1e3394eae6 --- /dev/null +++ b/pagx/include/pagx/nodes/Element.h @@ -0,0 +1,74 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Element types that can be placed in Layer.contents or Group.elements. + */ +enum class ElementType { + // Geometry elements + Rectangle, + Ellipse, + Polystar, + Path, + TextSpan, + + // Painters + Fill, + Stroke, + + // Path modifiers + TrimPath, + RoundCorner, + MergePath, + + // Text animators + TextModifier, + TextPath, + TextLayout, + + // Containers + Group, + Repeater +}; + +/** + * Returns the string name of an element type. + */ +const char* ElementTypeName(ElementType type); + +/** + * Base class for elements that can be placed in Layer.contents or Group.elements. + */ +class Element : public Node { + public: + /** + * Returns the element type of this element. + */ + virtual ElementType elementType() const = 0; + + protected: + Element() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Ellipse.h b/pagx/include/pagx/nodes/Ellipse.h index f7a36ef23f..09aa2e7279 100644 --- a/pagx/include/pagx/nodes/Ellipse.h +++ b/pagx/include/pagx/nodes/Ellipse.h @@ -26,7 +26,8 @@ namespace pagx { /** * An ellipse shape. */ -struct Ellipse : public Geometry { +class Ellipse : public Geometry { + public: Point center = {}; Size size = {100, 100}; bool reversed = false; @@ -34,6 +35,10 @@ struct Ellipse : public Geometry { NodeType type() const override { return NodeType::Ellipse; } + + ElementType elementType() const override { + return ElementType::Ellipse; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Fill.h b/pagx/include/pagx/nodes/Fill.h index b7d3271c46..16bfff891b 100644 --- a/pagx/include/pagx/nodes/Fill.h +++ b/pagx/include/pagx/nodes/Fill.h @@ -33,7 +33,8 @@ namespace pagx { * The color can be a simple color string ("#FF0000"), a reference ("#gradientId"), * or an inline color source node. */ -struct Fill : public Painter { +class Fill : public Painter { + public: std::string color = {}; std::unique_ptr colorSource = nullptr; float alpha = 1; @@ -44,6 +45,10 @@ struct Fill : public Painter { NodeType type() const override { return NodeType::Fill; } + + ElementType elementType() const override { + return ElementType::Fill; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Geometry.h b/pagx/include/pagx/nodes/Geometry.h index cd7400743f..7f4f3d2506 100644 --- a/pagx/include/pagx/nodes/Geometry.h +++ b/pagx/include/pagx/nodes/Geometry.h @@ -18,14 +18,14 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Element.h" namespace pagx { /** * Base class for geometry elements (Rectangle, Ellipse, Polystar, Path, TextSpan). */ -class Geometry : public Node { +class Geometry : public Element { protected: Geometry() = default; }; diff --git a/pagx/include/pagx/nodes/Group.h b/pagx/include/pagx/nodes/Group.h index 99b831dac8..d0af6a7771 100644 --- a/pagx/include/pagx/nodes/Group.h +++ b/pagx/include/pagx/nodes/Group.h @@ -20,7 +20,7 @@ #include #include -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Element.h" #include "pagx/types/Types.h" namespace pagx { @@ -28,7 +28,8 @@ namespace pagx { /** * Group container. */ -struct Group : public Node { +class Group : public Element { + public: Point anchorPoint = {}; Point position = {}; float rotation = 0; @@ -36,11 +37,15 @@ struct Group : public Node { float skew = 0; float skewAxis = 0; float alpha = 1; - std::vector> elements = {}; + std::vector> elements = {}; NodeType type() const override { return NodeType::Group; } + + ElementType elementType() const override { + return ElementType::Group; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Image.h b/pagx/include/pagx/nodes/Image.h index a09e11f39f..eed29ff01d 100644 --- a/pagx/include/pagx/nodes/Image.h +++ b/pagx/include/pagx/nodes/Image.h @@ -19,20 +19,18 @@ #pragma once #include -#include "pagx/nodes/Node.h" namespace pagx { /** * Image resource. */ -struct Image : public Node { +class Image { + public: + virtual ~Image() = default; + std::string id = {}; std::string source = {}; - - NodeType type() const override { - return NodeType::Image; - } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/ImagePattern.h b/pagx/include/pagx/nodes/ImagePattern.h index b15c9b5d9c..612227dc32 100644 --- a/pagx/include/pagx/nodes/ImagePattern.h +++ b/pagx/include/pagx/nodes/ImagePattern.h @@ -29,7 +29,8 @@ namespace pagx { /** * An image pattern. */ -struct ImagePattern : public ColorSource { +class ImagePattern : public ColorSource { + public: std::string id = {}; std::string image = {}; TileMode tileModeX = TileMode::Clamp; @@ -40,6 +41,10 @@ struct ImagePattern : public ColorSource { NodeType type() const override { return NodeType::ImagePattern; } + + ColorSourceType colorSourceType() const override { + return ColorSourceType::ImagePattern; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/InnerShadowFilter.h b/pagx/include/pagx/nodes/InnerShadowFilter.h index a62b1cd71d..06bdf6b139 100644 --- a/pagx/include/pagx/nodes/InnerShadowFilter.h +++ b/pagx/include/pagx/nodes/InnerShadowFilter.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Filter.h" +#include "pagx/nodes/LayerFilter.h" #include "pagx/types/Types.h" namespace pagx { @@ -26,7 +26,8 @@ namespace pagx { /** * Inner shadow filter. */ -struct InnerShadowFilter : public Filter { +class InnerShadowFilter : public LayerFilter { + public: float offsetX = 0; float offsetY = 0; float blurrinessX = 0; @@ -37,6 +38,10 @@ struct InnerShadowFilter : public Filter { NodeType type() const override { return NodeType::InnerShadowFilter; } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::InnerShadowFilter; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/InnerShadowStyle.h b/pagx/include/pagx/nodes/InnerShadowStyle.h index 07627b3dba..3e717db10e 100644 --- a/pagx/include/pagx/nodes/InnerShadowStyle.h +++ b/pagx/include/pagx/nodes/InnerShadowStyle.h @@ -27,7 +27,8 @@ namespace pagx { /** * Inner shadow style. */ -struct InnerShadowStyle : public LayerStyle { +class InnerShadowStyle : public LayerStyle { + public: float offsetX = 0; float offsetY = 0; float blurrinessX = 0; @@ -38,6 +39,10 @@ struct InnerShadowStyle : public LayerStyle { NodeType type() const override { return NodeType::InnerShadowStyle; } + + LayerStyleType layerStyleType() const override { + return LayerStyleType::InnerShadowStyle; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Layer.h b/pagx/include/pagx/nodes/Layer.h index 2d39a7dd11..cae6efb3c9 100644 --- a/pagx/include/pagx/nodes/Layer.h +++ b/pagx/include/pagx/nodes/Layer.h @@ -22,7 +22,8 @@ #include #include #include -#include "pagx/nodes/Filter.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/LayerFilter.h" #include "pagx/nodes/LayerStyle.h" #include "pagx/nodes/Node.h" #include "pagx/types/BlendMode.h" @@ -34,7 +35,8 @@ namespace pagx { /** * Layer node. */ -struct Layer : public Node { +class Layer : public Node { + public: std::string id = {}; std::string name = {}; bool visible = true; @@ -55,9 +57,9 @@ struct Layer : public Node { MaskType maskType = MaskType::Alpha; std::string composition = {}; - std::vector> contents = {}; + std::vector> contents = {}; std::vector> styles = {}; - std::vector> filters = {}; + std::vector> filters = {}; std::vector> children = {}; // Custom data from SVG data-* attributes (key without "data-" prefix) diff --git a/pagx/include/pagx/nodes/LayerFilter.h b/pagx/include/pagx/nodes/LayerFilter.h new file mode 100644 index 0000000000..7b5e076a1b --- /dev/null +++ b/pagx/include/pagx/nodes/LayerFilter.h @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Layer filter types. + */ +enum class LayerFilterType { + BlurFilter, + DropShadowFilter, + InnerShadowFilter, + BlendFilter, + ColorMatrixFilter +}; + +/** + * Returns the string name of a layer filter type. + */ +const char* LayerFilterTypeName(LayerFilterType type); + +/** + * Base class for layer filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, ColorMatrixFilter). + */ +class LayerFilter : public Node { + public: + /** + * Returns the layer filter type of this layer filter. + */ + virtual LayerFilterType layerFilterType() const = 0; + + protected: + LayerFilter() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/LayerStyle.h b/pagx/include/pagx/nodes/LayerStyle.h index ccf0f36c5d..f737bcc11d 100644 --- a/pagx/include/pagx/nodes/LayerStyle.h +++ b/pagx/include/pagx/nodes/LayerStyle.h @@ -22,10 +22,30 @@ namespace pagx { +/** + * Layer style types. + */ +enum class LayerStyleType { + DropShadowStyle, + InnerShadowStyle, + BackgroundBlurStyle +}; + +/** + * Returns the string name of a layer style type. + */ +const char* LayerStyleTypeName(LayerStyleType type); + /** * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle). */ class LayerStyle : public Node { + public: + /** + * Returns the layer style type of this layer style. + */ + virtual LayerStyleType layerStyleType() const = 0; + protected: LayerStyle() = default; }; diff --git a/pagx/include/pagx/nodes/LinearGradient.h b/pagx/include/pagx/nodes/LinearGradient.h index ea486fcbc8..bcc343fe32 100644 --- a/pagx/include/pagx/nodes/LinearGradient.h +++ b/pagx/include/pagx/nodes/LinearGradient.h @@ -29,7 +29,8 @@ namespace pagx { /** * A linear gradient. */ -struct LinearGradient : public ColorSource { +class LinearGradient : public ColorSource { + public: std::string id = {}; Point startPoint = {}; Point endPoint = {}; @@ -39,6 +40,10 @@ struct LinearGradient : public ColorSource { NodeType type() const override { return NodeType::LinearGradient; } + + ColorSourceType colorSourceType() const override { + return ColorSourceType::LinearGradient; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/MergePath.h b/pagx/include/pagx/nodes/MergePath.h index 18bf2ecf01..7a73725a97 100644 --- a/pagx/include/pagx/nodes/MergePath.h +++ b/pagx/include/pagx/nodes/MergePath.h @@ -26,12 +26,17 @@ namespace pagx { /** * Merge path modifier. */ -struct MergePath : public PathModifier { +class MergePath : public PathModifier { + public: MergePathMode mode = MergePathMode::Append; NodeType type() const override { return NodeType::MergePath; } + + ElementType elementType() const override { + return ElementType::MergePath; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Painter.h b/pagx/include/pagx/nodes/Painter.h index 769eab9814..adaddab76b 100644 --- a/pagx/include/pagx/nodes/Painter.h +++ b/pagx/include/pagx/nodes/Painter.h @@ -18,14 +18,14 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Element.h" namespace pagx { /** * Base class for painters (Fill, Stroke). */ -class Painter : public Node { +class Painter : public Element { protected: Painter() = default; }; diff --git a/pagx/include/pagx/nodes/Path.h b/pagx/include/pagx/nodes/Path.h index ada9f864e7..001ce799ea 100644 --- a/pagx/include/pagx/nodes/Path.h +++ b/pagx/include/pagx/nodes/Path.h @@ -26,13 +26,18 @@ namespace pagx { /** * A path shape. */ -struct Path : public Geometry { +class Path : public Geometry { + public: PathData data = {}; bool reversed = false; NodeType type() const override { return NodeType::Path; } + + ElementType elementType() const override { + return ElementType::Path; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/PathDataResource.h b/pagx/include/pagx/nodes/PathDataResource.h index 10f73a0e6a..dd62c658a0 100644 --- a/pagx/include/pagx/nodes/PathDataResource.h +++ b/pagx/include/pagx/nodes/PathDataResource.h @@ -19,20 +19,18 @@ #pragma once #include -#include "pagx/nodes/Node.h" namespace pagx { /** * PathData resource - stores reusable path data. */ -struct PathDataResource : public Node { +class PathDataResource { + public: + virtual ~PathDataResource() = default; + std::string id = {}; std::string data = {}; // SVG path data string - - NodeType type() const override { - return NodeType::PathData; - } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/PathModifier.h b/pagx/include/pagx/nodes/PathModifier.h index 4b346453e5..e9723fc942 100644 --- a/pagx/include/pagx/nodes/PathModifier.h +++ b/pagx/include/pagx/nodes/PathModifier.h @@ -18,14 +18,14 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Element.h" namespace pagx { /** * Base class for path modifiers (TrimPath, RoundCorner, MergePath). */ -class PathModifier : public Node { +class PathModifier : public Element { protected: PathModifier() = default; }; diff --git a/pagx/include/pagx/nodes/Polystar.h b/pagx/include/pagx/nodes/Polystar.h index 2a746af995..0dbdfd9bbc 100644 --- a/pagx/include/pagx/nodes/Polystar.h +++ b/pagx/include/pagx/nodes/Polystar.h @@ -27,7 +27,8 @@ namespace pagx { /** * A polygon or star shape. */ -struct Polystar : public Geometry { +class Polystar : public Geometry { + public: Point center = {}; PolystarType polystarType = PolystarType::Star; float pointCount = 5; @@ -41,6 +42,10 @@ struct Polystar : public Geometry { NodeType type() const override { return NodeType::Polystar; } + + ElementType elementType() const override { + return ElementType::Polystar; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/RadialGradient.h b/pagx/include/pagx/nodes/RadialGradient.h index 06158c9774..08f1eb45fa 100644 --- a/pagx/include/pagx/nodes/RadialGradient.h +++ b/pagx/include/pagx/nodes/RadialGradient.h @@ -29,7 +29,8 @@ namespace pagx { /** * A radial gradient. */ -struct RadialGradient : public ColorSource { +class RadialGradient : public ColorSource { + public: std::string id = {}; Point center = {}; float radius = 0; @@ -39,6 +40,10 @@ struct RadialGradient : public ColorSource { NodeType type() const override { return NodeType::RadialGradient; } + + ColorSourceType colorSourceType() const override { + return ColorSourceType::RadialGradient; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/RangeSelector.h b/pagx/include/pagx/nodes/RangeSelector.h index b2c88e1c1d..46787659c1 100644 --- a/pagx/include/pagx/nodes/RangeSelector.h +++ b/pagx/include/pagx/nodes/RangeSelector.h @@ -18,7 +18,6 @@ #pragma once -#include "pagx/nodes/Node.h" #include "pagx/types/SelectorMode.h" #include "pagx/types/SelectorShape.h" #include "pagx/types/SelectorUnit.h" @@ -28,7 +27,8 @@ namespace pagx { /** * Range selector for text modifier. */ -struct RangeSelector : public Node { +class RangeSelector { + public: float start = 0; float end = 1; float offset = 0; @@ -40,10 +40,6 @@ struct RangeSelector : public Node { float weight = 1; bool randomizeOrder = false; int randomSeed = 0; - - NodeType type() const override { - return NodeType::RangeSelector; - } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Rectangle.h b/pagx/include/pagx/nodes/Rectangle.h index ae22e702a2..60280df05c 100644 --- a/pagx/include/pagx/nodes/Rectangle.h +++ b/pagx/include/pagx/nodes/Rectangle.h @@ -26,7 +26,8 @@ namespace pagx { /** * A rectangle shape. */ -struct Rectangle : public Geometry { +class Rectangle : public Geometry { + public: Point center = {}; Size size = {100, 100}; float roundness = 0; @@ -35,6 +36,10 @@ struct Rectangle : public Geometry { NodeType type() const override { return NodeType::Rectangle; } + + ElementType elementType() const override { + return ElementType::Rectangle; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Repeater.h b/pagx/include/pagx/nodes/Repeater.h index b51e3cf068..678cdd11a8 100644 --- a/pagx/include/pagx/nodes/Repeater.h +++ b/pagx/include/pagx/nodes/Repeater.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Element.h" #include "pagx/types/RepeaterOrder.h" #include "pagx/types/Types.h" @@ -27,7 +27,8 @@ namespace pagx { /** * Repeater modifier. */ -struct Repeater : public Node { +class Repeater : public Element { + public: float copies = 3; float offset = 0; RepeaterOrder order = RepeaterOrder::BelowOriginal; @@ -41,6 +42,10 @@ struct Repeater : public Node { NodeType type() const override { return NodeType::Repeater; } + + ElementType elementType() const override { + return ElementType::Repeater; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Resource.h b/pagx/include/pagx/nodes/Resource.h new file mode 100644 index 0000000000..ecfcf3598e --- /dev/null +++ b/pagx/include/pagx/nodes/Resource.h @@ -0,0 +1,123 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace pagx { + +class Composition; +class Image; +class PathDataResource; +class ColorSource; + +/** + * Resource types that can be stored in PAGXDocument.resources. + */ +enum class ResourceType { + Image, + PathData, + ColorSource, + Composition +}; + +/** + * Returns the string name of a resource type. + */ +const char* ResourceTypeName(ResourceType type); + +/** + * A resource in the document that can be referenced by ID. + */ +class Resource { + public: + virtual ~Resource() = default; + + /** + * Returns the type of this resource. + */ + virtual ResourceType type() const = 0; + + /** + * Returns the ID of this resource. + */ + virtual const std::string& id() const = 0; + + protected: + Resource() = default; +}; + +/** + * Image resource wrapper. + */ +class ImageResource : public Resource { + public: + std::unique_ptr image = nullptr; + + ResourceType type() const override { + return ResourceType::Image; + } + + const std::string& id() const override; +}; + +/** + * PathData resource wrapper. + */ +class PathDataResourceWrapper : public Resource { + public: + std::unique_ptr pathData = nullptr; + + ResourceType type() const override { + return ResourceType::PathData; + } + + const std::string& id() const override; +}; + +/** + * ColorSource resource wrapper. + */ +class ColorSourceResource : public Resource { + public: + std::unique_ptr colorSource = nullptr; + + ResourceType type() const override { + return ResourceType::ColorSource; + } + + const std::string& id() const override; +}; + +/** + * Composition resource wrapper. + */ +class CompositionResource : public Resource { + public: + std::unique_ptr composition = nullptr; + + ResourceType type() const override { + return ResourceType::Composition; + } + + const std::string& id() const override; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/RoundCorner.h b/pagx/include/pagx/nodes/RoundCorner.h index 41e63064df..03b1193dc6 100644 --- a/pagx/include/pagx/nodes/RoundCorner.h +++ b/pagx/include/pagx/nodes/RoundCorner.h @@ -25,12 +25,17 @@ namespace pagx { /** * Round corner modifier. */ -struct RoundCorner : public PathModifier { +class RoundCorner : public PathModifier { + public: float radius = 10; NodeType type() const override { return NodeType::RoundCorner; } + + ElementType elementType() const override { + return ElementType::RoundCorner; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/SolidColor.h b/pagx/include/pagx/nodes/SolidColor.h index ce1cad497b..1101e2cd7f 100644 --- a/pagx/include/pagx/nodes/SolidColor.h +++ b/pagx/include/pagx/nodes/SolidColor.h @@ -27,13 +27,18 @@ namespace pagx { /** * A solid color. */ -struct SolidColor : public ColorSource { +class SolidColor : public ColorSource { + public: std::string id = {}; Color color = {}; NodeType type() const override { return NodeType::SolidColor; } + + ColorSourceType colorSourceType() const override { + return ColorSourceType::SolidColor; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Stroke.h b/pagx/include/pagx/nodes/Stroke.h index a9b4bede89..13e0422b23 100644 --- a/pagx/include/pagx/nodes/Stroke.h +++ b/pagx/include/pagx/nodes/Stroke.h @@ -34,7 +34,8 @@ namespace pagx { /** * A stroke painter. */ -struct Stroke : public Painter { +class Stroke : public Painter { + public: std::string color = {}; std::unique_ptr colorSource = nullptr; float width = 1; @@ -51,6 +52,10 @@ struct Stroke : public Painter { NodeType type() const override { return NodeType::Stroke; } + + ElementType elementType() const override { + return ElementType::Stroke; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Filter.h b/pagx/include/pagx/nodes/TextAnimator.h similarity index 78% rename from pagx/include/pagx/nodes/Filter.h rename to pagx/include/pagx/nodes/TextAnimator.h index a8a21b30b0..6802ad2867 100644 --- a/pagx/include/pagx/nodes/Filter.h +++ b/pagx/include/pagx/nodes/TextAnimator.h @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -18,16 +18,16 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/Element.h" namespace pagx { /** - * Base class for filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, ColorMatrixFilter). + * Base class for text animators (TextModifier, TextPath, TextLayout). */ -class Filter : public Node { +class TextAnimator : public Element { protected: - Filter() = default; + TextAnimator() = default; }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/TextLayout.h b/pagx/include/pagx/nodes/TextLayout.h index 01471bdc59..38b9e101f4 100644 --- a/pagx/include/pagx/nodes/TextLayout.h +++ b/pagx/include/pagx/nodes/TextLayout.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/Node.h" +#include "pagx/nodes/TextAnimator.h" #include "pagx/types/Overflow.h" #include "pagx/types/TextAlign.h" #include "pagx/types/VerticalAlign.h" @@ -28,7 +28,8 @@ namespace pagx { /** * Text layout modifier. */ -struct TextLayout : public Node { +class TextLayout : public TextAnimator { + public: float width = 0; float height = 0; TextAlign textAlign = TextAlign::Left; @@ -40,6 +41,10 @@ struct TextLayout : public Node { NodeType type() const override { return NodeType::TextLayout; } + + ElementType elementType() const override { + return ElementType::TextLayout; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/TextModifier.h b/pagx/include/pagx/nodes/TextModifier.h index 20c8d466be..3628726ec3 100644 --- a/pagx/include/pagx/nodes/TextModifier.h +++ b/pagx/include/pagx/nodes/TextModifier.h @@ -20,8 +20,8 @@ #include #include -#include "pagx/nodes/Node.h" #include "pagx/nodes/RangeSelector.h" +#include "pagx/nodes/TextAnimator.h" #include "pagx/types/Types.h" namespace pagx { @@ -29,7 +29,8 @@ namespace pagx { /** * Text modifier. */ -struct TextModifier : public Node { +class TextModifier : public TextAnimator { + public: Point anchorPoint = {}; Point position = {}; float rotation = 0; @@ -45,6 +46,10 @@ struct TextModifier : public Node { NodeType type() const override { return NodeType::TextModifier; } + + ElementType elementType() const override { + return ElementType::TextModifier; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/TextPath.h b/pagx/include/pagx/nodes/TextPath.h index c26030eb4a..2d3ff70ec3 100644 --- a/pagx/include/pagx/nodes/TextPath.h +++ b/pagx/include/pagx/nodes/TextPath.h @@ -19,7 +19,7 @@ #pragma once #include -#include "pagx/nodes/Node.h" +#include "pagx/nodes/TextAnimator.h" #include "pagx/types/TextPathAlign.h" namespace pagx { @@ -27,7 +27,8 @@ namespace pagx { /** * Text path modifier. */ -struct TextPath : public Node { +class TextPath : public TextAnimator { + public: std::string path = {}; TextPathAlign pathAlign = TextPathAlign::Start; float firstMargin = 0; @@ -39,6 +40,10 @@ struct TextPath : public Node { NodeType type() const override { return NodeType::TextPath; } + + ElementType elementType() const override { + return ElementType::TextPath; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/TextSpan.h b/pagx/include/pagx/nodes/TextSpan.h index 36bd2bf637..5e081ac9d2 100644 --- a/pagx/include/pagx/nodes/TextSpan.h +++ b/pagx/include/pagx/nodes/TextSpan.h @@ -27,7 +27,8 @@ namespace pagx { /** * A text span. */ -struct TextSpan : public Geometry { +class TextSpan : public Geometry { + public: float x = 0; float y = 0; std::string font = {}; @@ -41,6 +42,10 @@ struct TextSpan : public Geometry { NodeType type() const override { return NodeType::TextSpan; } + + ElementType elementType() const override { + return ElementType::TextSpan; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/TrimPath.h b/pagx/include/pagx/nodes/TrimPath.h index e7369cda55..339b372137 100644 --- a/pagx/include/pagx/nodes/TrimPath.h +++ b/pagx/include/pagx/nodes/TrimPath.h @@ -26,7 +26,8 @@ namespace pagx { /** * Trim path modifier. */ -struct TrimPath : public PathModifier { +class TrimPath : public PathModifier { + public: float start = 0; float end = 1; float offset = 0; @@ -35,6 +36,10 @@ struct TrimPath : public PathModifier { NodeType type() const override { return NodeType::TrimPath; } + + ElementType elementType() const override { + return ElementType::TrimPath; + } }; } // namespace pagx diff --git a/pagx/src/PAGXElement.cpp b/pagx/src/PAGXElement.cpp new file mode 100644 index 0000000000..374f1291f7 --- /dev/null +++ b/pagx/src/PAGXElement.cpp @@ -0,0 +1,60 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/nodes/Element.h" + +namespace pagx { + +const char* ElementTypeName(ElementType type) { + switch (type) { + case ElementType::Rectangle: + return "Rectangle"; + case ElementType::Ellipse: + return "Ellipse"; + case ElementType::Polystar: + return "Polystar"; + case ElementType::Path: + return "Path"; + case ElementType::TextSpan: + return "TextSpan"; + case ElementType::Fill: + return "Fill"; + case ElementType::Stroke: + return "Stroke"; + case ElementType::TrimPath: + return "TrimPath"; + case ElementType::RoundCorner: + return "RoundCorner"; + case ElementType::MergePath: + return "MergePath"; + case ElementType::TextModifier: + return "TextModifier"; + case ElementType::TextPath: + return "TextPath"; + case ElementType::TextLayout: + return "TextLayout"; + case ElementType::Group: + return "Group"; + case ElementType::Repeater: + return "Repeater"; + default: + return "Unknown"; + } +} + +} // namespace pagx diff --git a/pagx/src/PAGXNode.cpp b/pagx/src/PAGXNode.cpp index 6ad34b113f..9d1aabefcf 100644 --- a/pagx/src/PAGXNode.cpp +++ b/pagx/src/PAGXNode.cpp @@ -16,7 +16,11 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "pagx/PAGXNode.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/LayerFilter.h" +#include "pagx/nodes/LayerStyle.h" +#include "pagx/nodes/Node.h" namespace pagx { @@ -97,4 +101,53 @@ const char* NodeTypeName(NodeType type) { } } +const char* ColorSourceTypeName(ColorSourceType type) { + switch (type) { + case ColorSourceType::SolidColor: + return "SolidColor"; + case ColorSourceType::LinearGradient: + return "LinearGradient"; + case ColorSourceType::RadialGradient: + return "RadialGradient"; + case ColorSourceType::ConicGradient: + return "ConicGradient"; + case ColorSourceType::DiamondGradient: + return "DiamondGradient"; + case ColorSourceType::ImagePattern: + return "ImagePattern"; + default: + return "Unknown"; + } +} + +const char* LayerStyleTypeName(LayerStyleType type) { + switch (type) { + case LayerStyleType::DropShadowStyle: + return "DropShadowStyle"; + case LayerStyleType::InnerShadowStyle: + return "InnerShadowStyle"; + case LayerStyleType::BackgroundBlurStyle: + return "BackgroundBlurStyle"; + default: + return "Unknown"; + } +} + +const char* LayerFilterTypeName(LayerFilterType type) { + switch (type) { + case LayerFilterType::BlurFilter: + return "BlurFilter"; + case LayerFilterType::DropShadowFilter: + return "DropShadowFilter"; + case LayerFilterType::InnerShadowFilter: + return "InnerShadowFilter"; + case LayerFilterType::BlendFilter: + return "BlendFilter"; + case LayerFilterType::ColorMatrixFilter: + return "ColorMatrixFilter"; + default: + return "Unknown"; + } +} + } // namespace pagx diff --git a/pagx/src/PAGXResource.cpp b/pagx/src/PAGXResource.cpp new file mode 100644 index 0000000000..9c58f8e6a3 --- /dev/null +++ b/pagx/src/PAGXResource.cpp @@ -0,0 +1,92 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/nodes/Resource.h" +#include "pagx/nodes/ConicGradient.h" +#include "pagx/nodes/Composition.h" +#include "pagx/nodes/DiamondGradient.h" +#include "pagx/nodes/Image.h" +#include "pagx/nodes/ImagePattern.h" +#include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/PathDataResource.h" +#include "pagx/nodes/RadialGradient.h" +#include "pagx/nodes/SolidColor.h" + +namespace pagx { + +const char* ResourceTypeName(ResourceType type) { + switch (type) { + case ResourceType::Image: + return "Image"; + case ResourceType::PathData: + return "PathData"; + case ResourceType::ColorSource: + return "ColorSource"; + case ResourceType::Composition: + return "Composition"; + default: + return "Unknown"; + } +} + +static const std::string emptyString = ""; + +const std::string& ImageResource::id() const { + if (image) { + return image->id; + } + return emptyString; +} + +const std::string& PathDataResourceWrapper::id() const { + if (pathData) { + return pathData->id; + } + return emptyString; +} + +const std::string& ColorSourceResource::id() const { + if (!colorSource) { + return emptyString; + } + switch (colorSource->type()) { + case ColorSourceType::SolidColor: + return static_cast(colorSource.get())->id; + case ColorSourceType::LinearGradient: + return static_cast(colorSource.get())->id; + case ColorSourceType::RadialGradient: + return static_cast(colorSource.get())->id; + case ColorSourceType::ConicGradient: + return static_cast(colorSource.get())->id; + case ColorSourceType::DiamondGradient: + return static_cast(colorSource.get())->id; + case ColorSourceType::ImagePattern: + return static_cast(colorSource.get())->id; + default: + return emptyString; + } +} + +const std::string& CompositionResource::id() const { + if (composition) { + return composition->id; + } + return emptyString; +} + +} // namespace pagx From 0142a68a75a22649bae9e862e3499214c85a86b2 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 16:14:35 +0800 Subject: [PATCH 080/678] Restore Node inheritance for resources and remove Resource wrapper class. --- pagx/include/pagx/PAGXDocument.h | 7 +- pagx/include/pagx/PAGXModel.h | 1 - pagx/include/pagx/nodes/Composition.h | 9 +- pagx/include/pagx/nodes/Fill.h | 2 +- pagx/include/pagx/nodes/Group.h | 2 +- pagx/include/pagx/nodes/Image.h | 9 +- pagx/include/pagx/nodes/Layer.h | 6 +- pagx/include/pagx/nodes/PathDataResource.h | 9 +- pagx/include/pagx/nodes/Resource.h | 123 --------------------- pagx/include/pagx/nodes/Stroke.h | 2 +- pagx/src/PAGXResource.cpp | 92 --------------- 11 files changed, 27 insertions(+), 235 deletions(-) delete mode 100644 pagx/include/pagx/nodes/Resource.h delete mode 100644 pagx/src/PAGXResource.cpp diff --git a/pagx/include/pagx/PAGXDocument.h b/pagx/include/pagx/PAGXDocument.h index c52d32774e..a16eae283c 100644 --- a/pagx/include/pagx/PAGXDocument.h +++ b/pagx/include/pagx/PAGXDocument.h @@ -23,7 +23,6 @@ #include #include #include "pagx/PAGXModel.h" -#include "pagx/nodes/Resource.h" namespace pagx { @@ -55,7 +54,7 @@ class PAGXDocument { * Resources (images, gradients, compositions, etc.). * These can be referenced by "#id" in the document. */ - std::vector> resources = {};; + std::vector> resources = {}; /** * Top-level layers. @@ -99,7 +98,7 @@ class PAGXDocument { * Finds a resource by ID. * Returns nullptr if not found. */ - Resource* findResource(const std::string& id) const; + Node* findResource(const std::string& id) const; /** * Finds a layer by ID (searches recursively). @@ -111,7 +110,7 @@ class PAGXDocument { friend class PAGXXMLParser; PAGXDocument() = default; - mutable std::unordered_map resourceMap = {}; + mutable std::unordered_map resourceMap = {}; mutable bool resourceMapDirty = true; void rebuildResourceMap() const; diff --git a/pagx/include/pagx/PAGXModel.h b/pagx/include/pagx/PAGXModel.h index da73034786..8449f7dac3 100644 --- a/pagx/include/pagx/PAGXModel.h +++ b/pagx/include/pagx/PAGXModel.h @@ -84,7 +84,6 @@ #include "pagx/nodes/Composition.h" #include "pagx/nodes/Image.h" #include "pagx/nodes/PathDataResource.h" -#include "pagx/nodes/Resource.h" // Layer #include "pagx/nodes/Layer.h" diff --git a/pagx/include/pagx/nodes/Composition.h b/pagx/include/pagx/nodes/Composition.h index 72435463e7..2eef5fcffd 100644 --- a/pagx/include/pagx/nodes/Composition.h +++ b/pagx/include/pagx/nodes/Composition.h @@ -21,6 +21,7 @@ #include #include #include +#include "pagx/nodes/Node.h" namespace pagx { @@ -29,14 +30,16 @@ class Layer; /** * Composition resource. */ -class Composition { +class Composition : public Node { public: - virtual ~Composition() = default; - std::string id = {}; float width = 0; float height = 0; std::vector> layers = {}; + + NodeType type() const override { + return NodeType::Composition; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Fill.h b/pagx/include/pagx/nodes/Fill.h index 16bfff891b..b997688b79 100644 --- a/pagx/include/pagx/nodes/Fill.h +++ b/pagx/include/pagx/nodes/Fill.h @@ -36,7 +36,7 @@ namespace pagx { class Fill : public Painter { public: std::string color = {}; - std::unique_ptr colorSource = nullptr; + std::unique_ptr colorSource = nullptr; float alpha = 1; BlendMode blendMode = BlendMode::Normal; FillRule fillRule = FillRule::Winding; diff --git a/pagx/include/pagx/nodes/Group.h b/pagx/include/pagx/nodes/Group.h index d0af6a7771..9d617293fb 100644 --- a/pagx/include/pagx/nodes/Group.h +++ b/pagx/include/pagx/nodes/Group.h @@ -37,7 +37,7 @@ class Group : public Element { float skew = 0; float skewAxis = 0; float alpha = 1; - std::vector> elements = {}; + std::vector> elements = {}; NodeType type() const override { return NodeType::Group; diff --git a/pagx/include/pagx/nodes/Image.h b/pagx/include/pagx/nodes/Image.h index eed29ff01d..cda487dc45 100644 --- a/pagx/include/pagx/nodes/Image.h +++ b/pagx/include/pagx/nodes/Image.h @@ -19,18 +19,21 @@ #pragma once #include +#include "pagx/nodes/Node.h" namespace pagx { /** * Image resource. */ -class Image { +class Image : public Node { public: - virtual ~Image() = default; - std::string id = {}; std::string source = {}; + + NodeType type() const override { + return NodeType::Image; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Layer.h b/pagx/include/pagx/nodes/Layer.h index cae6efb3c9..eca3b0f369 100644 --- a/pagx/include/pagx/nodes/Layer.h +++ b/pagx/include/pagx/nodes/Layer.h @@ -57,9 +57,9 @@ class Layer : public Node { MaskType maskType = MaskType::Alpha; std::string composition = {}; - std::vector> contents = {}; - std::vector> styles = {}; - std::vector> filters = {}; + std::vector> contents = {}; + std::vector> styles = {}; + std::vector> filters = {}; std::vector> children = {}; // Custom data from SVG data-* attributes (key without "data-" prefix) diff --git a/pagx/include/pagx/nodes/PathDataResource.h b/pagx/include/pagx/nodes/PathDataResource.h index dd62c658a0..8c1ecfb14a 100644 --- a/pagx/include/pagx/nodes/PathDataResource.h +++ b/pagx/include/pagx/nodes/PathDataResource.h @@ -19,18 +19,21 @@ #pragma once #include +#include "pagx/nodes/Node.h" namespace pagx { /** * PathData resource - stores reusable path data. */ -class PathDataResource { +class PathDataResource : public Node { public: - virtual ~PathDataResource() = default; - std::string id = {}; std::string data = {}; // SVG path data string + + NodeType type() const override { + return NodeType::PathData; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Resource.h b/pagx/include/pagx/nodes/Resource.h deleted file mode 100644 index ecfcf3598e..0000000000 --- a/pagx/include/pagx/nodes/Resource.h +++ /dev/null @@ -1,123 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include - -namespace pagx { - -class Composition; -class Image; -class PathDataResource; -class ColorSource; - -/** - * Resource types that can be stored in PAGXDocument.resources. - */ -enum class ResourceType { - Image, - PathData, - ColorSource, - Composition -}; - -/** - * Returns the string name of a resource type. - */ -const char* ResourceTypeName(ResourceType type); - -/** - * A resource in the document that can be referenced by ID. - */ -class Resource { - public: - virtual ~Resource() = default; - - /** - * Returns the type of this resource. - */ - virtual ResourceType type() const = 0; - - /** - * Returns the ID of this resource. - */ - virtual const std::string& id() const = 0; - - protected: - Resource() = default; -}; - -/** - * Image resource wrapper. - */ -class ImageResource : public Resource { - public: - std::unique_ptr image = nullptr; - - ResourceType type() const override { - return ResourceType::Image; - } - - const std::string& id() const override; -}; - -/** - * PathData resource wrapper. - */ -class PathDataResourceWrapper : public Resource { - public: - std::unique_ptr pathData = nullptr; - - ResourceType type() const override { - return ResourceType::PathData; - } - - const std::string& id() const override; -}; - -/** - * ColorSource resource wrapper. - */ -class ColorSourceResource : public Resource { - public: - std::unique_ptr colorSource = nullptr; - - ResourceType type() const override { - return ResourceType::ColorSource; - } - - const std::string& id() const override; -}; - -/** - * Composition resource wrapper. - */ -class CompositionResource : public Resource { - public: - std::unique_ptr composition = nullptr; - - ResourceType type() const override { - return ResourceType::Composition; - } - - const std::string& id() const override; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Stroke.h b/pagx/include/pagx/nodes/Stroke.h index 13e0422b23..f0f2e3ef18 100644 --- a/pagx/include/pagx/nodes/Stroke.h +++ b/pagx/include/pagx/nodes/Stroke.h @@ -37,7 +37,7 @@ namespace pagx { class Stroke : public Painter { public: std::string color = {}; - std::unique_ptr colorSource = nullptr; + std::unique_ptr colorSource = nullptr; float width = 1; float alpha = 1; BlendMode blendMode = BlendMode::Normal; diff --git a/pagx/src/PAGXResource.cpp b/pagx/src/PAGXResource.cpp deleted file mode 100644 index 9c58f8e6a3..0000000000 --- a/pagx/src/PAGXResource.cpp +++ /dev/null @@ -1,92 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "pagx/nodes/Resource.h" -#include "pagx/nodes/ConicGradient.h" -#include "pagx/nodes/Composition.h" -#include "pagx/nodes/DiamondGradient.h" -#include "pagx/nodes/Image.h" -#include "pagx/nodes/ImagePattern.h" -#include "pagx/nodes/LinearGradient.h" -#include "pagx/nodes/PathDataResource.h" -#include "pagx/nodes/RadialGradient.h" -#include "pagx/nodes/SolidColor.h" - -namespace pagx { - -const char* ResourceTypeName(ResourceType type) { - switch (type) { - case ResourceType::Image: - return "Image"; - case ResourceType::PathData: - return "PathData"; - case ResourceType::ColorSource: - return "ColorSource"; - case ResourceType::Composition: - return "Composition"; - default: - return "Unknown"; - } -} - -static const std::string emptyString = ""; - -const std::string& ImageResource::id() const { - if (image) { - return image->id; - } - return emptyString; -} - -const std::string& PathDataResourceWrapper::id() const { - if (pathData) { - return pathData->id; - } - return emptyString; -} - -const std::string& ColorSourceResource::id() const { - if (!colorSource) { - return emptyString; - } - switch (colorSource->type()) { - case ColorSourceType::SolidColor: - return static_cast(colorSource.get())->id; - case ColorSourceType::LinearGradient: - return static_cast(colorSource.get())->id; - case ColorSourceType::RadialGradient: - return static_cast(colorSource.get())->id; - case ColorSourceType::ConicGradient: - return static_cast(colorSource.get())->id; - case ColorSourceType::DiamondGradient: - return static_cast(colorSource.get())->id; - case ColorSourceType::ImagePattern: - return static_cast(colorSource.get())->id; - default: - return emptyString; - } -} - -const std::string& CompositionResource::id() const { - if (composition) { - return composition->id; - } - return emptyString; -} - -} // namespace pagx From 21cc0300ab1e45cb2c08b3eca215f2a7c054bcf0 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 16:21:59 +0800 Subject: [PATCH 081/678] Refactor PAGX node hierarchy to use VectorElement as the only intermediate base class. --- pagx/include/pagx/PAGXModel.h | 16 ++-- pagx/include/pagx/nodes/Element.h | 74 --------------- pagx/include/pagx/nodes/Ellipse.h | 21 +++-- pagx/include/pagx/nodes/Fill.h | 41 +++++++-- pagx/include/pagx/nodes/Geometry.h | 33 ------- pagx/include/pagx/nodes/Group.h | 43 +++++++-- pagx/include/pagx/nodes/Layer.h | 100 ++++++++++++++++++++- pagx/include/pagx/nodes/MergePath.h | 14 +-- pagx/include/pagx/nodes/Painter.h | 33 ------- pagx/include/pagx/nodes/Path.h | 18 ++-- pagx/include/pagx/nodes/PathModifier.h | 33 ------- pagx/include/pagx/nodes/Polystar.h | 47 ++++++++-- pagx/include/pagx/nodes/Rectangle.h | 25 ++++-- pagx/include/pagx/nodes/Repeater.h | 48 ++++++++-- pagx/include/pagx/nodes/RoundCorner.h | 14 +-- pagx/include/pagx/nodes/Stroke.h | 62 +++++++++++-- pagx/include/pagx/nodes/TextAnimator.h | 33 ------- pagx/include/pagx/nodes/TextLayout.h | 39 ++++++-- pagx/include/pagx/nodes/TextModifier.h | 56 ++++++++++-- pagx/include/pagx/nodes/TextPath.h | 39 ++++++-- pagx/include/pagx/nodes/TextSpan.h | 46 ++++++++-- pagx/include/pagx/nodes/TrimPath.h | 31 +++++-- pagx/include/pagx/nodes/VectorElement.h | 114 ++++++++++++++++++++++++ pagx/src/PAGXElement.cpp | 34 +++---- pagx/src/PAGXNode.cpp | 2 +- 25 files changed, 702 insertions(+), 314 deletions(-) delete mode 100644 pagx/include/pagx/nodes/Element.h delete mode 100644 pagx/include/pagx/nodes/Geometry.h delete mode 100644 pagx/include/pagx/nodes/Painter.h delete mode 100644 pagx/include/pagx/nodes/PathModifier.h delete mode 100644 pagx/include/pagx/nodes/TextAnimator.h create mode 100644 pagx/include/pagx/nodes/VectorElement.h diff --git a/pagx/include/pagx/PAGXModel.h b/pagx/include/pagx/PAGXModel.h index 8449f7dac3..99b94b756f 100644 --- a/pagx/include/pagx/PAGXModel.h +++ b/pagx/include/pagx/PAGXModel.h @@ -24,14 +24,10 @@ // Base classes #include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/Element.h" -#include "pagx/nodes/Geometry.h" #include "pagx/nodes/LayerFilter.h" #include "pagx/nodes/LayerStyle.h" #include "pagx/nodes/Node.h" -#include "pagx/nodes/Painter.h" -#include "pagx/nodes/PathModifier.h" -#include "pagx/nodes/TextAnimator.h" +#include "pagx/nodes/VectorElement.h" // Color sources #include "pagx/nodes/ColorStop.h" @@ -42,29 +38,29 @@ #include "pagx/nodes/RadialGradient.h" #include "pagx/nodes/SolidColor.h" -// Geometry elements +// Vector elements - shapes #include "pagx/nodes/Ellipse.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/Polystar.h" #include "pagx/nodes/Rectangle.h" #include "pagx/nodes/TextSpan.h" -// Painters +// Vector elements - painters #include "pagx/nodes/Fill.h" #include "pagx/nodes/Stroke.h" -// Path modifiers +// Vector elements - path modifiers #include "pagx/nodes/MergePath.h" #include "pagx/nodes/RoundCorner.h" #include "pagx/nodes/TrimPath.h" -// Text modifiers +// Vector elements - text modifiers #include "pagx/nodes/RangeSelector.h" #include "pagx/nodes/TextLayout.h" #include "pagx/nodes/TextModifier.h" #include "pagx/nodes/TextPath.h" -// Repeater and Group +// Vector elements - containers #include "pagx/nodes/Group.h" #include "pagx/nodes/Repeater.h" diff --git a/pagx/include/pagx/nodes/Element.h b/pagx/include/pagx/nodes/Element.h deleted file mode 100644 index 1e3394eae6..0000000000 --- a/pagx/include/pagx/nodes/Element.h +++ /dev/null @@ -1,74 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/Node.h" - -namespace pagx { - -/** - * Element types that can be placed in Layer.contents or Group.elements. - */ -enum class ElementType { - // Geometry elements - Rectangle, - Ellipse, - Polystar, - Path, - TextSpan, - - // Painters - Fill, - Stroke, - - // Path modifiers - TrimPath, - RoundCorner, - MergePath, - - // Text animators - TextModifier, - TextPath, - TextLayout, - - // Containers - Group, - Repeater -}; - -/** - * Returns the string name of an element type. - */ -const char* ElementTypeName(ElementType type); - -/** - * Base class for elements that can be placed in Layer.contents or Group.elements. - */ -class Element : public Node { - public: - /** - * Returns the element type of this element. - */ - virtual ElementType elementType() const = 0; - - protected: - Element() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Ellipse.h b/pagx/include/pagx/nodes/Ellipse.h index 09aa2e7279..39dbe439f2 100644 --- a/pagx/include/pagx/nodes/Ellipse.h +++ b/pagx/include/pagx/nodes/Ellipse.h @@ -18,26 +18,37 @@ #pragma once -#include "pagx/nodes/Geometry.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/Types.h" namespace pagx { /** - * An ellipse shape. + * Ellipse represents an ellipse shape defined by a center point and size. */ -class Ellipse : public Geometry { +class Ellipse : public VectorElement { public: + /** + * The center point of the ellipse. + */ Point center = {}; + + /** + * The size of the ellipse. The default value is {100, 100}. + */ Size size = {100, 100}; + + /** + * Whether the path direction is reversed. The default value is false. + */ bool reversed = false; NodeType type() const override { return NodeType::Ellipse; } - ElementType elementType() const override { - return ElementType::Ellipse; + VectorElementType vectorElementType() const override { + return VectorElementType::Ellipse; } }; diff --git a/pagx/include/pagx/nodes/Fill.h b/pagx/include/pagx/nodes/Fill.h index b997688b79..7de9bbc2a9 100644 --- a/pagx/include/pagx/nodes/Fill.h +++ b/pagx/include/pagx/nodes/Fill.h @@ -21,7 +21,7 @@ #include #include #include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/Painter.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/BlendMode.h" #include "pagx/types/FillRule.h" #include "pagx/types/Placement.h" @@ -29,25 +29,52 @@ namespace pagx { /** - * A fill painter. - * The color can be a simple color string ("#FF0000"), a reference ("#gradientId"), - * or an inline color source node. + * Fill represents a fill painter that fills shapes with a solid color, gradient, or pattern. The + * color can be specified as a simple color string (e.g., "#FF0000"), a reference to a defined + * color source (e.g., "#gradientId"), or an inline ColorSource node. */ -class Fill : public Painter { +class Fill : public VectorElement { public: + /** + * The fill color as a string. Can be a hex color (e.g., "#FF0000"), a reference to a color + * source (e.g., "#gradientId"), or empty if colorSource is used. + */ std::string color = {}; + + /** + * An inline color source node (SolidColor, LinearGradient, etc.) for complex fills. If provided, + * this takes precedence over the color string. + */ std::unique_ptr colorSource = nullptr; + + /** + * The opacity of the fill, ranging from 0 (transparent) to 1 (opaque). The default value is 1. + */ float alpha = 1; + + /** + * The blend mode used when compositing the fill. The default value is Normal. + */ BlendMode blendMode = BlendMode::Normal; + + /** + * The fill rule that determines the interior of self-intersecting paths. The default value is + * Winding. + */ FillRule fillRule = FillRule::Winding; + + /** + * The placement of the fill relative to strokes (Background or Foreground). The default value is + * Background. + */ Placement placement = Placement::Background; NodeType type() const override { return NodeType::Fill; } - ElementType elementType() const override { - return ElementType::Fill; + VectorElementType vectorElementType() const override { + return VectorElementType::Fill; } }; diff --git a/pagx/include/pagx/nodes/Geometry.h b/pagx/include/pagx/nodes/Geometry.h deleted file mode 100644 index 7f4f3d2506..0000000000 --- a/pagx/include/pagx/nodes/Geometry.h +++ /dev/null @@ -1,33 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/Element.h" - -namespace pagx { - -/** - * Base class for geometry elements (Rectangle, Ellipse, Polystar, Path, TextSpan). - */ -class Geometry : public Element { - protected: - Geometry() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Group.h b/pagx/include/pagx/nodes/Group.h index 9d617293fb..70a3695288 100644 --- a/pagx/include/pagx/nodes/Group.h +++ b/pagx/include/pagx/nodes/Group.h @@ -20,31 +20,64 @@ #include #include -#include "pagx/nodes/Element.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/Types.h" namespace pagx { /** - * Group container. + * Group is a container that groups multiple vector elements with its own transform. It can contain + * shapes, painters, modifiers, and nested groups, allowing for hierarchical organization of + * content. */ -class Group : public Element { +class Group : public VectorElement { public: + /** + * The anchor point for transformations. + */ Point anchorPoint = {}; + + /** + * The position offset of the group. + */ Point position = {}; + + /** + * The rotation angle in degrees. The default value is 0. + */ float rotation = 0; + + /** + * The scale factor as (scaleX, scaleY). The default value is {1, 1}. + */ Point scale = {1, 1}; + + /** + * The skew angle in degrees. The default value is 0. + */ float skew = 0; + + /** + * The axis angle in degrees for the skew transformation. The default value is 0. + */ float skewAxis = 0; + + /** + * The opacity of the group, ranging from 0 to 1. The default value is 1. + */ float alpha = 1; + + /** + * The child elements contained in this group. + */ std::vector> elements = {}; NodeType type() const override { return NodeType::Group; } - ElementType elementType() const override { - return ElementType::Group; + VectorElementType vectorElementType() const override { + return VectorElementType::Group; } }; diff --git a/pagx/include/pagx/nodes/Layer.h b/pagx/include/pagx/nodes/Layer.h index eca3b0f369..dbb26d201c 100644 --- a/pagx/include/pagx/nodes/Layer.h +++ b/pagx/include/pagx/nodes/Layer.h @@ -22,10 +22,10 @@ #include #include #include -#include "pagx/nodes/Element.h" #include "pagx/nodes/LayerFilter.h" #include "pagx/nodes/LayerStyle.h" #include "pagx/nodes/Node.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/BlendMode.h" #include "pagx/types/MaskType.h" #include "pagx/types/Types.h" @@ -33,36 +33,130 @@ namespace pagx { /** - * Layer node. + * Layer represents a layer node that can contain vector elements, layer styles, filters, and child + * layers. It is the main building block for composing visual content in a PAGX document. */ class Layer : public Node { public: + /** + * The unique identifier of the layer. + */ std::string id = {}; + + /** + * The display name of the layer. + */ std::string name = {}; + + /** + * Whether the layer is visible. The default value is true. + */ bool visible = true; + + /** + * The opacity of the layer, ranging from 0 to 1. The default value is 1. + */ float alpha = 1; + + /** + * The blend mode used when compositing the layer. The default value is Normal. + */ BlendMode blendMode = BlendMode::Normal; + + /** + * The x-coordinate of the layer position. The default value is 0. + */ float x = 0; + + /** + * The y-coordinate of the layer position. The default value is 0. + */ float y = 0; + + /** + * The 2D transformation matrix of the layer. + */ Matrix matrix = {}; + + /** + * The 3D transformation matrix as a 16-element array in column-major order. + */ std::vector matrix3D = {}; + + /** + * Whether to preserve 3D transformations for child layers. The default value is false. + */ bool preserve3D = false; + + /** + * Whether to apply anti-aliasing to the layer edges. The default value is true. + */ bool antiAlias = true; + + /** + * Whether to use group opacity mode for the layer and its children. The default value is false. + */ bool groupOpacity = false; + + /** + * Whether layer effects pass through to the background. The default value is true. + */ bool passThroughBackground = true; + + /** + * Whether to exclude child effects when applying layer styles. The default value is false. + */ bool excludeChildEffectsInLayerStyle = false; + + /** + * The scroll rectangle for clipping the layer content. + */ Rect scrollRect = {}; + + /** + * Whether a scroll rectangle is applied to the layer. The default value is false. + */ bool hasScrollRect = false; + + /** + * A reference to a mask layer (e.g., "#maskId"). + */ std::string mask = {}; + + /** + * The type of masking to apply (Alpha, Luminosity, InvertedAlpha, or InvertedLuminosity). The + * default value is Alpha. + */ MaskType maskType = MaskType::Alpha; + + /** + * A reference to a composition (e.g., "#compositionId") used as the layer content. + */ std::string composition = {}; + /** + * The vector elements contained in this layer (shapes, painters, modifiers, etc.). + */ std::vector> contents = {}; + + /** + * The layer styles applied to this layer (drop shadow, inner shadow, etc.). + */ std::vector> styles = {}; + + /** + * The filters applied to this layer (blur, color matrix, etc.). + */ std::vector> filters = {}; + + /** + * The child layers contained in this layer. + */ std::vector> children = {}; - // Custom data from SVG data-* attributes (key without "data-" prefix) + /** + * Custom data from SVG data-* attributes. The keys are stored without the "data-" prefix. + */ std::unordered_map customData = {}; NodeType type() const override { diff --git a/pagx/include/pagx/nodes/MergePath.h b/pagx/include/pagx/nodes/MergePath.h index 7a73725a97..b73ffd60d9 100644 --- a/pagx/include/pagx/nodes/MergePath.h +++ b/pagx/include/pagx/nodes/MergePath.h @@ -18,24 +18,28 @@ #pragma once -#include "pagx/nodes/PathModifier.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/MergePathMode.h" namespace pagx { /** - * Merge path modifier. + * MergePath is a path modifier that merges multiple paths using boolean operations. It can append, + * add, subtract, intersect, or exclude paths from each other. */ -class MergePath : public PathModifier { +class MergePath : public VectorElement { public: + /** + * The merge mode that determines how paths are combined. The default value is Append. + */ MergePathMode mode = MergePathMode::Append; NodeType type() const override { return NodeType::MergePath; } - ElementType elementType() const override { - return ElementType::MergePath; + VectorElementType vectorElementType() const override { + return VectorElementType::MergePath; } }; diff --git a/pagx/include/pagx/nodes/Painter.h b/pagx/include/pagx/nodes/Painter.h deleted file mode 100644 index adaddab76b..0000000000 --- a/pagx/include/pagx/nodes/Painter.h +++ /dev/null @@ -1,33 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/Element.h" - -namespace pagx { - -/** - * Base class for painters (Fill, Stroke). - */ -class Painter : public Element { - protected: - Painter() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Path.h b/pagx/include/pagx/nodes/Path.h index 001ce799ea..4d589ff88f 100644 --- a/pagx/include/pagx/nodes/Path.h +++ b/pagx/include/pagx/nodes/Path.h @@ -19,24 +19,32 @@ #pragma once #include "pagx/PathData.h" -#include "pagx/nodes/Geometry.h" +#include "pagx/nodes/VectorElement.h" namespace pagx { /** - * A path shape. + * Path represents a freeform shape defined by a PathData containing vertices, in-tangents, and + * out-tangents. */ -class Path : public Geometry { +class Path : public VectorElement { public: + /** + * The path data containing vertices and control points. + */ PathData data = {}; + + /** + * Whether the path direction is reversed. The default value is false. + */ bool reversed = false; NodeType type() const override { return NodeType::Path; } - ElementType elementType() const override { - return ElementType::Path; + VectorElementType vectorElementType() const override { + return VectorElementType::Path; } }; diff --git a/pagx/include/pagx/nodes/PathModifier.h b/pagx/include/pagx/nodes/PathModifier.h deleted file mode 100644 index e9723fc942..0000000000 --- a/pagx/include/pagx/nodes/PathModifier.h +++ /dev/null @@ -1,33 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/Element.h" - -namespace pagx { - -/** - * Base class for path modifiers (TrimPath, RoundCorner, MergePath). - */ -class PathModifier : public Element { - protected: - PathModifier() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Polystar.h b/pagx/include/pagx/nodes/Polystar.h index 0dbdfd9bbc..a751cc758e 100644 --- a/pagx/include/pagx/nodes/Polystar.h +++ b/pagx/include/pagx/nodes/Polystar.h @@ -18,33 +18,70 @@ #pragma once -#include "pagx/nodes/Geometry.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/PolystarType.h" #include "pagx/types/Types.h" namespace pagx { /** - * A polygon or star shape. + * Polystar represents a polygon or star shape with configurable points, radii, and roundness. */ -class Polystar : public Geometry { +class Polystar : public VectorElement { public: + /** + * The center point of the polystar. + */ Point center = {}; + + /** + * The type of polystar shape, either Star or Polygon. The default value is Star. + */ PolystarType polystarType = PolystarType::Star; + + /** + * The number of points in the polystar. The default value is 5. + */ float pointCount = 5; + + /** + * The outer radius of the polystar. The default value is 100. + */ float outerRadius = 100; + + /** + * The inner radius of the polystar. Only applies when polystarType is Star. The default value + * is 50. + */ float innerRadius = 50; + + /** + * The rotation angle in degrees. The default value is 0. + */ float rotation = 0; + + /** + * The roundness of the outer points, ranging from 0 to 100. The default value is 0. + */ float outerRoundness = 0; + + /** + * The roundness of the inner points, ranging from 0 to 100. Only applies when polystarType is + * Star. The default value is 0. + */ float innerRoundness = 0; + + /** + * Whether the path direction is reversed. The default value is false. + */ bool reversed = false; NodeType type() const override { return NodeType::Polystar; } - ElementType elementType() const override { - return ElementType::Polystar; + VectorElementType vectorElementType() const override { + return VectorElementType::Polystar; } }; diff --git a/pagx/include/pagx/nodes/Rectangle.h b/pagx/include/pagx/nodes/Rectangle.h index 60280df05c..33f5f1c1b3 100644 --- a/pagx/include/pagx/nodes/Rectangle.h +++ b/pagx/include/pagx/nodes/Rectangle.h @@ -18,27 +18,42 @@ #pragma once -#include "pagx/nodes/Geometry.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/Types.h" namespace pagx { /** - * A rectangle shape. + * Rectangle represents a rectangle shape with optional rounded corners. */ -class Rectangle : public Geometry { +class Rectangle : public VectorElement { public: + /** + * The center point of the rectangle. + */ Point center = {}; + + /** + * The size of the rectangle. The default value is {100, 100}. + */ Size size = {100, 100}; + + /** + * The corner roundness of the rectangle, ranging from 0 to 100. The default value is 0. + */ float roundness = 0; + + /** + * Whether the path direction is reversed. The default value is false. + */ bool reversed = false; NodeType type() const override { return NodeType::Rectangle; } - ElementType elementType() const override { - return ElementType::Rectangle; + VectorElementType vectorElementType() const override { + return VectorElementType::Rectangle; } }; diff --git a/pagx/include/pagx/nodes/Repeater.h b/pagx/include/pagx/nodes/Repeater.h index 678cdd11a8..f0f11f17c9 100644 --- a/pagx/include/pagx/nodes/Repeater.h +++ b/pagx/include/pagx/nodes/Repeater.h @@ -18,33 +18,71 @@ #pragma once -#include "pagx/nodes/Element.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/RepeaterOrder.h" #include "pagx/types/Types.h" namespace pagx { /** - * Repeater modifier. + * Repeater is a modifier that creates multiple copies of preceding elements with progressive + * transformations. Each copy can have an incremental offset in position, rotation, scale, and + * opacity. */ -class Repeater : public Element { +class Repeater : public VectorElement { public: + /** + * The number of copies to create. The default value is 3. + */ float copies = 3; + + /** + * The offset applied to the copy index, allowing fractional copies. The default value is 0. + */ float offset = 0; + + /** + * The stacking order of copies (BelowOriginal or AboveOriginal). The default value is + * BelowOriginal. + */ RepeaterOrder order = RepeaterOrder::BelowOriginal; + + /** + * The anchor point for transformations. + */ Point anchorPoint = {}; + + /** + * The position offset applied between each copy. The default value is {100, 100}. + */ Point position = {100, 100}; + + /** + * The rotation angle in degrees applied between each copy. The default value is 0. + */ float rotation = 0; + + /** + * The scale factor applied between each copy. The default value is {1, 1}. + */ Point scale = {1, 1}; + + /** + * The starting opacity for the first copy, ranging from 0 to 1. The default value is 1. + */ float startAlpha = 1; + + /** + * The ending opacity for the last copy, ranging from 0 to 1. The default value is 1. + */ float endAlpha = 1; NodeType type() const override { return NodeType::Repeater; } - ElementType elementType() const override { - return ElementType::Repeater; + VectorElementType vectorElementType() const override { + return VectorElementType::Repeater; } }; diff --git a/pagx/include/pagx/nodes/RoundCorner.h b/pagx/include/pagx/nodes/RoundCorner.h index 03b1193dc6..a86000bc4e 100644 --- a/pagx/include/pagx/nodes/RoundCorner.h +++ b/pagx/include/pagx/nodes/RoundCorner.h @@ -18,23 +18,27 @@ #pragma once -#include "pagx/nodes/PathModifier.h" +#include "pagx/nodes/VectorElement.h" namespace pagx { /** - * Round corner modifier. + * RoundCorner is a path modifier that rounds the corners of shapes by adding smooth curves at + * sharp vertices. */ -class RoundCorner : public PathModifier { +class RoundCorner : public VectorElement { public: + /** + * The radius of the rounded corners in pixels. The default value is 10. + */ float radius = 10; NodeType type() const override { return NodeType::RoundCorner; } - ElementType elementType() const override { - return ElementType::RoundCorner; + VectorElementType vectorElementType() const override { + return VectorElementType::RoundCorner; } }; diff --git a/pagx/include/pagx/nodes/Stroke.h b/pagx/include/pagx/nodes/Stroke.h index f0f2e3ef18..3128359c4d 100644 --- a/pagx/include/pagx/nodes/Stroke.h +++ b/pagx/include/pagx/nodes/Stroke.h @@ -22,7 +22,7 @@ #include #include #include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/Painter.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/BlendMode.h" #include "pagx/types/LineCap.h" #include "pagx/types/LineJoin.h" @@ -32,29 +32,81 @@ namespace pagx { /** - * A stroke painter. + * Stroke represents a stroke painter that outlines shapes with a solid color, gradient, or + * pattern. It supports various line cap styles, join styles, dash patterns, and stroke alignment. */ -class Stroke : public Painter { +class Stroke : public VectorElement { public: + /** + * The stroke color as a string. Can be a hex color (e.g., "#FF0000"), a reference to a color + * source (e.g., "#gradientId"), or empty if colorSource is used. + */ std::string color = {}; + + /** + * An inline color source node (SolidColor, LinearGradient, etc.) for complex strokes. If + * provided, this takes precedence over the color string. + */ std::unique_ptr colorSource = nullptr; + + /** + * The stroke width in pixels. The default value is 1. + */ float width = 1; + + /** + * The opacity of the stroke, ranging from 0 (transparent) to 1 (opaque). The default value is 1. + */ float alpha = 1; + + /** + * The blend mode used when compositing the stroke. The default value is Normal. + */ BlendMode blendMode = BlendMode::Normal; + + /** + * The line cap style at the endpoints of open paths. The default value is Butt. + */ LineCap cap = LineCap::Butt; + + /** + * The line join style at path corners. The default value is Miter. + */ LineJoin join = LineJoin::Miter; + + /** + * The limit for miter joins before they are beveled. The default value is 4. + */ float miterLimit = 4; + + /** + * The dash pattern as an array of dash and gap lengths. An empty array means a solid line. + */ std::vector dashes = {}; + + /** + * The offset into the dash pattern. The default value is 0. + */ float dashOffset = 0; + + /** + * The alignment of the stroke relative to the path (Center, Inner, or Outer). The default value + * is Center. + */ StrokeAlign align = StrokeAlign::Center; + + /** + * The placement of the stroke relative to fills (Background or Foreground). The default value is + * Background. + */ Placement placement = Placement::Background; NodeType type() const override { return NodeType::Stroke; } - ElementType elementType() const override { - return ElementType::Stroke; + VectorElementType vectorElementType() const override { + return VectorElementType::Stroke; } }; diff --git a/pagx/include/pagx/nodes/TextAnimator.h b/pagx/include/pagx/nodes/TextAnimator.h deleted file mode 100644 index 6802ad2867..0000000000 --- a/pagx/include/pagx/nodes/TextAnimator.h +++ /dev/null @@ -1,33 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/Element.h" - -namespace pagx { - -/** - * Base class for text animators (TextModifier, TextPath, TextLayout). - */ -class TextAnimator : public Element { - protected: - TextAnimator() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/TextLayout.h b/pagx/include/pagx/nodes/TextLayout.h index 38b9e101f4..aa71723088 100644 --- a/pagx/include/pagx/nodes/TextLayout.h +++ b/pagx/include/pagx/nodes/TextLayout.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/nodes/TextAnimator.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/Overflow.h" #include "pagx/types/TextAlign.h" #include "pagx/types/VerticalAlign.h" @@ -26,24 +26,53 @@ namespace pagx { /** - * Text layout modifier. + * TextLayout is a text animator that controls text layout within a bounding box. It provides + * options for text alignment, line height, indentation, and overflow handling. */ -class TextLayout : public TextAnimator { +class TextLayout : public VectorElement { public: + /** + * The width of the text box in pixels. A value of 0 means auto-width. The default value is 0. + */ float width = 0; + + /** + * The height of the text box in pixels. A value of 0 means auto-height. The default value is 0. + */ float height = 0; + + /** + * The horizontal text alignment (Left, Center, Right, or Justify). The default value is Left. + */ TextAlign textAlign = TextAlign::Left; + + /** + * The vertical text alignment (Top, Middle, or Bottom). The default value is Top. + */ VerticalAlign verticalAlign = VerticalAlign::Top; + + /** + * The line height multiplier. The default value is 1.2. + */ float lineHeight = 1.2f; + + /** + * The first-line indent in pixels. The default value is 0. + */ float indent = 0; + + /** + * The overflow behavior when text exceeds the bounding box (Clip, Visible, or Scroll). The + * default value is Clip. + */ Overflow overflow = Overflow::Clip; NodeType type() const override { return NodeType::TextLayout; } - ElementType elementType() const override { - return ElementType::TextLayout; + VectorElementType vectorElementType() const override { + return VectorElementType::TextLayout; } }; diff --git a/pagx/include/pagx/nodes/TextModifier.h b/pagx/include/pagx/nodes/TextModifier.h index 3628726ec3..96cfab1228 100644 --- a/pagx/include/pagx/nodes/TextModifier.h +++ b/pagx/include/pagx/nodes/TextModifier.h @@ -21,34 +21,80 @@ #include #include #include "pagx/nodes/RangeSelector.h" -#include "pagx/nodes/TextAnimator.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/Types.h" namespace pagx { /** - * Text modifier. + * TextModifier is a text animator that modifies text appearance with range-based transformations. + * It applies transformations like position, rotation, scale, and color changes to selected ranges + * of characters. */ -class TextModifier : public TextAnimator { +class TextModifier : public VectorElement { public: + /** + * The anchor point for transformations. + */ Point anchorPoint = {}; + + /** + * The position offset applied to selected characters. + */ Point position = {}; + + /** + * The rotation angle in degrees applied to selected characters. The default value is 0. + */ float rotation = 0; + + /** + * The scale factor applied to selected characters. The default value is {1, 1}. + */ Point scale = {1, 1}; + + /** + * The skew angle in degrees applied to selected characters. The default value is 0. + */ float skew = 0; + + /** + * The axis angle in degrees for the skew transformation. The default value is 0. + */ float skewAxis = 0; + + /** + * The opacity applied to selected characters, ranging from 0 to 1. The default value is 1. + */ float alpha = 1; + + /** + * The fill color override for selected characters as a hex color string. + */ std::string fillColor = {}; + + /** + * The stroke color override for selected characters as a hex color string. + */ std::string strokeColor = {}; + + /** + * The stroke width override for selected characters. A value of -1 means no override. The + * default value is -1. + */ float strokeWidth = -1; + + /** + * The range selectors that determine which characters are affected by this modifier. + */ std::vector rangeSelectors = {}; NodeType type() const override { return NodeType::TextModifier; } - ElementType elementType() const override { - return ElementType::TextModifier; + VectorElementType vectorElementType() const override { + return VectorElementType::TextModifier; } }; diff --git a/pagx/include/pagx/nodes/TextPath.h b/pagx/include/pagx/nodes/TextPath.h index 2d3ff70ec3..811d4fe1b7 100644 --- a/pagx/include/pagx/nodes/TextPath.h +++ b/pagx/include/pagx/nodes/TextPath.h @@ -19,30 +19,59 @@ #pragma once #include -#include "pagx/nodes/TextAnimator.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/TextPathAlign.h" namespace pagx { /** - * Text path modifier. + * TextPath is a text animator that places text along a path. It allows text to follow the contour + * of a referenced path shape. */ -class TextPath : public TextAnimator { +class TextPath : public VectorElement { public: + /** + * A reference to the path shape (e.g., "#pathId") that the text follows. + */ std::string path = {}; + + /** + * The alignment of text along the path (Start, Center, or Justify). The default value is Start. + */ TextPathAlign pathAlign = TextPathAlign::Start; + + /** + * The margin from the start of the path in pixels. The default value is 0. + */ float firstMargin = 0; + + /** + * The margin from the end of the path in pixels. The default value is 0. + */ float lastMargin = 0; + + /** + * Whether characters are rotated to be perpendicular to the path. The default value is true. + */ bool perpendicularToPath = true; + + /** + * Whether to reverse the direction of the path. The default value is false. + */ bool reversed = false; + + /** + * Whether to force text alignment to the path even when it exceeds the path length. The default + * value is false. + */ bool forceAlignment = false; NodeType type() const override { return NodeType::TextPath; } - ElementType elementType() const override { - return ElementType::TextPath; + VectorElementType vectorElementType() const override { + return VectorElementType::TextPath; } }; diff --git a/pagx/include/pagx/nodes/TextSpan.h b/pagx/include/pagx/nodes/TextSpan.h index 5e081ac9d2..5ab74dca89 100644 --- a/pagx/include/pagx/nodes/TextSpan.h +++ b/pagx/include/pagx/nodes/TextSpan.h @@ -19,32 +19,68 @@ #pragma once #include -#include "pagx/nodes/Geometry.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/FontStyle.h" namespace pagx { /** - * A text span. + * TextSpan represents a text span that generates glyph paths for rendering. It defines the text + * content, font properties, and positioning within a shape layer. */ -class TextSpan : public Geometry { +class TextSpan : public VectorElement { public: + /** + * The x-coordinate of the text baseline starting point. The default value is 0. + */ float x = 0; + + /** + * The y-coordinate of the text baseline starting point. The default value is 0. + */ float y = 0; + + /** + * The font family name. + */ std::string font = {}; + + /** + * The font size in pixels. The default value is 12. + */ float fontSize = 12; + + /** + * The font weight, ranging from 100 to 900. The default value is 400 (normal). + */ int fontWeight = 400; + + /** + * The font style (Normal, Italic, or Oblique). The default value is Normal. + */ FontStyle fontStyle = FontStyle::Normal; + + /** + * The tracking value that adjusts spacing between characters. The default value is 0. + */ float tracking = 0; + + /** + * The baseline shift in pixels, positive values shift the text up. The default value is 0. + */ float baselineShift = 0; + + /** + * The text content to render. + */ std::string text = {}; NodeType type() const override { return NodeType::TextSpan; } - ElementType elementType() const override { - return ElementType::TextSpan; + VectorElementType vectorElementType() const override { + return VectorElementType::TextSpan; } }; diff --git a/pagx/include/pagx/nodes/TrimPath.h b/pagx/include/pagx/nodes/TrimPath.h index 339b372137..a6b347ef09 100644 --- a/pagx/include/pagx/nodes/TrimPath.h +++ b/pagx/include/pagx/nodes/TrimPath.h @@ -18,27 +18,48 @@ #pragma once -#include "pagx/nodes/PathModifier.h" +#include "pagx/nodes/VectorElement.h" #include "pagx/types/TrimType.h" namespace pagx { /** - * Trim path modifier. + * TrimPath is a path modifier that trims paths to a specified range. It can be used to animate + * path drawing or reveal effects by adjusting the start and end values. */ -class TrimPath : public PathModifier { +class TrimPath : public VectorElement { public: + /** + * The starting point of the trim as a percentage of the path length, ranging from 0 to 1. The + * default value is 0. + */ float start = 0; + + /** + * The ending point of the trim as a percentage of the path length, ranging from 0 to 1. The + * default value is 1. + */ float end = 1; + + /** + * The offset to shift the trim range along the path, where 1 represents a full path length. The + * default value is 0. + */ float offset = 0; + + /** + * The trim type that determines how multiple paths are trimmed. Separate trims each path + * individually, while Simultaneous trims all paths as one continuous path. The default value is + * Separate. + */ TrimType trimType = TrimType::Separate; NodeType type() const override { return NodeType::TrimPath; } - ElementType elementType() const override { - return ElementType::TrimPath; + VectorElementType vectorElementType() const override { + return VectorElementType::TrimPath; } }; diff --git a/pagx/include/pagx/nodes/VectorElement.h b/pagx/include/pagx/nodes/VectorElement.h new file mode 100644 index 0000000000..693b93e73f --- /dev/null +++ b/pagx/include/pagx/nodes/VectorElement.h @@ -0,0 +1,114 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * VectorElementType enumerates all types of vector elements that can be placed in Layer.contents + * or Group.elements. + */ +enum class VectorElementType { + /** + * A rectangle shape with optional rounded corners. + */ + Rectangle, + /** + * An ellipse shape defined by center point and size. + */ + Ellipse, + /** + * A polygon or star shape with configurable points and roundness. + */ + Polystar, + /** + * A freeform path shape defined by a PathData. + */ + Path, + /** + * A text span that generates glyph paths for rendering. + */ + TextSpan, + /** + * A fill painter that fills shapes with a color or gradient. + */ + Fill, + /** + * A stroke painter that outlines shapes with a color or gradient. + */ + Stroke, + /** + * A path modifier that trims paths to a specified range. + */ + TrimPath, + /** + * A path modifier that rounds the corners of shapes. + */ + RoundCorner, + /** + * A path modifier that merges multiple paths using boolean operations. + */ + MergePath, + /** + * A text animator that modifies text appearance with range-based transformations. + */ + TextModifier, + /** + * A text animator that places text along a path. + */ + TextPath, + /** + * A text animator that controls text layout within a bounding box. + */ + TextLayout, + /** + * A container that groups multiple elements with its own transform. + */ + Group, + /** + * A modifier that creates multiple copies of preceding elements. + */ + Repeater +}; + +/** + * Returns the string name of a vector element type. + */ +const char* VectorElementTypeName(VectorElementType type); + +/** + * VectorElement is the base class for all vector elements in a shape layer. It includes shapes + * (Rectangle, Ellipse, Polystar, Path, TextSpan), painters (Fill, Stroke), modifiers (TrimPath, + * RoundCorner, MergePath), text elements (TextModifier, TextPath, TextLayout), and containers + * (Group, Repeater). + */ +class VectorElement : public Node { + public: + /** + * Returns the vector element type of this element. + */ + virtual VectorElementType vectorElementType() const = 0; + + protected: + VectorElement() = default; +}; + +} // namespace pagx diff --git a/pagx/src/PAGXElement.cpp b/pagx/src/PAGXElement.cpp index 374f1291f7..15b8afec40 100644 --- a/pagx/src/PAGXElement.cpp +++ b/pagx/src/PAGXElement.cpp @@ -16,41 +16,41 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "pagx/nodes/Element.h" +#include "pagx/nodes/VectorElement.h" namespace pagx { -const char* ElementTypeName(ElementType type) { +const char* VectorElementTypeName(VectorElementType type) { switch (type) { - case ElementType::Rectangle: + case VectorElementType::Rectangle: return "Rectangle"; - case ElementType::Ellipse: + case VectorElementType::Ellipse: return "Ellipse"; - case ElementType::Polystar: + case VectorElementType::Polystar: return "Polystar"; - case ElementType::Path: + case VectorElementType::Path: return "Path"; - case ElementType::TextSpan: + case VectorElementType::TextSpan: return "TextSpan"; - case ElementType::Fill: + case VectorElementType::Fill: return "Fill"; - case ElementType::Stroke: + case VectorElementType::Stroke: return "Stroke"; - case ElementType::TrimPath: + case VectorElementType::TrimPath: return "TrimPath"; - case ElementType::RoundCorner: + case VectorElementType::RoundCorner: return "RoundCorner"; - case ElementType::MergePath: + case VectorElementType::MergePath: return "MergePath"; - case ElementType::TextModifier: + case VectorElementType::TextModifier: return "TextModifier"; - case ElementType::TextPath: + case VectorElementType::TextPath: return "TextPath"; - case ElementType::TextLayout: + case VectorElementType::TextLayout: return "TextLayout"; - case ElementType::Group: + case VectorElementType::Group: return "Group"; - case ElementType::Repeater: + case VectorElementType::Repeater: return "Repeater"; default: return "Unknown"; diff --git a/pagx/src/PAGXNode.cpp b/pagx/src/PAGXNode.cpp index 9d1aabefcf..b25eb7918f 100644 --- a/pagx/src/PAGXNode.cpp +++ b/pagx/src/PAGXNode.cpp @@ -17,10 +17,10 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/Element.h" #include "pagx/nodes/LayerFilter.h" #include "pagx/nodes/LayerStyle.h" #include "pagx/nodes/Node.h" +#include "pagx/nodes/VectorElement.h" namespace pagx { From 5d5063a8e27766e3a282841ffad9ee6438fde97d Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 16:43:00 +0800 Subject: [PATCH 082/678] Improve PAGX spec document formatting and descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change file structure description from "调试时" to more accurate scenarios - Update appendix B description to "常见用法示例" - Standardize all section titles to Chinese-first format with English in parentheses - Rename "图片图案" to "图片填充" (ImagePattern) - Consolidate color source coordinate system description to apply to all non-solid color sources --- pagx/docs/pagx_spec.md | 153 ++++++++++++++++++++--------------------- 1 file changed, 75 insertions(+), 78 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index fdde4ccd5b..8f098f532f 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1,6 +1,6 @@ # PAGX 格式规范 v1.0 -## 1. Introduction(介绍) +## 1. 介绍(Introduction) **PAGX**(Portable Animated Graphics XML)是一种基于 XML 的矢量动画标记语言。它提供了统一且强大的矢量图形与动画描述能力,旨在成为跨所有主要工具与运行时的矢量动画交换标准。 @@ -20,7 +20,7 @@ ### 1.2 文件结构 -PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视频、音频、字体等),也支持通过数据 URI 内嵌资源。PAGX 与二进制 PAG 格式可双向互转:发布时转换为 PAG 以优化加载性能,调试时转换回 PAGX 以便阅读和编辑。 +PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视频、音频、字体等),也支持通过数据 URI 内嵌资源。PAGX 与二进制 PAG 格式可双向互转:发布时转换为 PAG 以优化加载性能;开发、审查或编辑时可使用 PAGX 格式以便阅读和修改。 ### 1.3 文档组织 @@ -34,12 +34,12 @@ PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视 **附录**(方便速查): - **附录 A**:节点层级与包含关系 -- **附录 B**:示例 +- **附录 B**:常见用法示例 - **附录 C**:节点与属性速查 --- -## 2. Basic Data Types(基础数据类型) +## 2. 基础数据类型(Basic Data Types) 本节定义 PAGX 文档中使用的基础数据类型和命名规范。 @@ -200,7 +200,7 @@ PAGX 支持多种颜色格式: --- -## 3. Document Structure(文档结构) +## 3. 文档结构(Document Structure) 本节定义 PAGX 文档的整体结构。 @@ -235,7 +235,7 @@ PAGX 使用标准的 2D 笛卡尔坐标系: **图层渲染顺序**:图层按文档顺序依次渲染,文档中靠前的图层先渲染(位于下方),靠后的图层后渲染(位于上方)。 -### 3.3 Resources(资源区) +### 3.3 资源区(Resources) `` 定义可复用的资源,包括图片、路径数据、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `#id` 形式引用。 @@ -255,7 +255,7 @@ PAGX 使用标准的 2D 笛卡尔坐标系: ``` -#### 3.3.1 Image(图片) +#### 3.3.1 图片(Image) 图片资源定义可在文档中引用的位图数据。 @@ -270,7 +270,7 @@ PAGX 使用标准的 2D 笛卡尔坐标系: **支持格式**:PNG、JPEG、WebP、GIF -#### 3.3.2 PathData(路径数据) +#### 3.3.2 路径数据(PathData) PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器引用。 @@ -289,7 +289,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 1. **共享定义**:在 `` 中预定义,通过 `#id` 引用。适用于**被多处引用**的颜色源。 2. **内联定义**:直接嵌套在 `` 或 `` 元素内部。适用于**仅使用一次**的颜色源,更简洁。 -##### SolidColor(纯色) +##### 纯色(SolidColor) ```xml @@ -299,7 +299,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 |------|------|--------|------| | `color` | color | (必填) | 颜色值 | -##### LinearGradient(线性渐变) +##### 线性渐变(LinearGradient) 线性渐变沿起点到终点的方向插值。 @@ -318,7 +318,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 **计算**:对于点 P,其颜色由 P 在起点-终点连线上的投影位置决定。 -##### RadialGradient(径向渐变) +##### 径向渐变(RadialGradient) 径向渐变从中心向外辐射。 @@ -337,7 +337,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 **计算**:对于点 P,其颜色由 `distance(P, center) / radius` 决定。 -##### ConicGradient(锥形渐变) +##### 锥形渐变(ConicGradient) 锥形渐变(也称扫描渐变)沿圆周方向插值。 @@ -357,7 +357,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 **计算**:对于点 P,其颜色由 `atan2(P.y - center.y, P.x - center.x)` 在 `[startAngle, endAngle]` 范围内的比例决定。 -##### DiamondGradient(菱形渐变) +##### 菱形渐变(DiamondGradient) 菱形渐变从中心向四角辐射。 @@ -376,7 +376,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 **计算**:对于点 P,其颜色由曼哈顿距离 `(|P.x - center.x| + |P.y - center.y|) / halfDiagonal` 决定。 -##### ColorStop(渐变色标) +##### 渐变色标(ColorStop) ```xml @@ -395,11 +395,30 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 - `offset > 1` 的色标被视为 `offset = 1` - 如果没有 `offset = 0` 的色标,使用第一个色标的颜色填充 - 如果没有 `offset = 1` 的色标,使用最后一个色标的颜色填充 -- **渐变变换**:`matrix` 属性对渐变坐标系应用变换 + +##### 图片填充(ImagePattern) + +图片图案使用图片作为颜色源。 + +```xml + +``` + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `image` | idref | (必填) | 图片引用 "#id" | +| `tileModeX` | TileMode | clamp | X 方向平铺模式 | +| `tileModeY` | TileMode | clamp | Y 方向平铺模式 | +| `sampling` | SamplingMode | linear | 采样模式 | +| `matrix` | string | 单位矩阵 | 变换矩阵 | + +**TileMode(平铺模式)**:`clamp`(钳制)、`repeat`(重复)、`mirror`(镜像)、`decal`(贴花) + +**SamplingMode(采样模式)**:`nearest`(最近邻)、`linear`(双线性)、`mipmap`(多级渐远) ##### 颜色源坐标系统 -所有颜色源(渐变、图案)的坐标系是**相对于几何元素的局部坐标系原点**。 +除纯色外,所有颜色源(渐变、图片填充)都有坐标系的概念,其坐标系**相对于几何元素的局部坐标系原点**。可通过 `matrix` 属性对颜色源坐标系应用变换。 **变换行为**: @@ -428,29 +447,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 - 对该图层应用 `scale(2, 2)` 变换:矩形变为 200×200,渐变也随之放大,视觉效果保持一致 - 直接将 Rectangle 的 size 改为 200,200:矩形变为 200×200,但渐变坐标不变,只覆盖矩形的左半部分 -##### ImagePattern(图片图案) - -图片图案使用图片作为颜色源。 - -```xml - -``` - -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `image` | idref | (必填) | 图片引用 "#id" | -| `tileModeX` | TileMode | clamp | X 方向平铺模式 | -| `tileModeY` | TileMode | clamp | Y 方向平铺模式 | -| `sampling` | SamplingMode | linear | 采样模式 | -| `matrix` | string | 单位矩阵 | 变换矩阵 | - -**TileMode(平铺模式)**:`clamp`(钳制)、`repeat`(重复)、`mirror`(镜像)、`decal`(贴花) - -**SamplingMode(采样模式)**:`nearest`(最近邻)、`linear`(双线性)、`mipmap`(多级渐远) - -**图案变换**:`matrix` 属性对图案应用变换,`matrix="2,0,0,2,0,0"` 使图案视觉上放大 2 倍。 - -#### 3.3.4 Composition(合成) +#### 3.3.4 合成(Composition) 合成用于内容复用(类似 After Effects 的 Pre-comp)。 @@ -504,11 +501,11 @@ PAGX 文档采用层级结构组织内容: --- -## 4. Layer System(图层系统) +## 4. 图层系统(Layer System) 图层(Layer)是 PAGX 内容组织的基本单元,提供了丰富的视觉效果控制能力。 -### 4.1 Layer(图层) +### 4.1 图层(Layer) `` 是内容和子图层的基本容器。 @@ -639,7 +636,7 @@ PAGX 文档采用层级结构组织内容: 上例中,矩形填充完全透明不可见,但投影阴影仍然会基于矩形的轮廓生成。 -### 4.2 Layer Styles(图层样式) +### 4.2 图层样式(Layer Styles) 图层样式在图层内容渲染完成后应用。 @@ -657,7 +654,7 @@ PAGX 文档采用层级结构组织内容: |------|------|--------|------| | `blendMode` | BlendMode | normal | 混合模式(见 4.1) | -#### 4.2.1 DropShadowStyle(投影阴影) +#### 4.2.1 投影阴影(DropShadowStyle) 在图层外部绘制投影阴影。 @@ -679,7 +676,7 @@ PAGX 文档采用层级结构组织内容: - `true`:阴影完整显示,包括被图层内容遮挡的部分 - `false`:阴影被图层内容遮挡的部分会被挖空(仅显示图层轮廓外的阴影) -#### 4.2.2 InnerShadowStyle(内阴影) +#### 4.2.2 内阴影(InnerShadowStyle) 在图层内部绘制内阴影。 @@ -696,7 +693,7 @@ PAGX 文档采用层级结构组织内容: 2. 偏移并模糊 3. 与图层内容求交集 -#### 4.2.3 BackgroundBlurStyle(背景模糊) +#### 4.2.3 背景模糊(BackgroundBlurStyle) 对图层下方的背景应用模糊效果。 @@ -711,7 +708,7 @@ PAGX 文档采用层级结构组织内容: 2. 应用高斯模糊 `(blurrinessX, blurrinessY)` 3. 使用图层轮廓作为遮罩裁剪模糊结果 -### 4.3 Filter Effects(滤镜效果) +### 4.3 滤镜效果(Filter Effects) 滤镜按文档顺序链式应用,每个滤镜的输出作为下一个滤镜的输入。 @@ -723,7 +720,7 @@ PAGX 文档采用层级结构组织内容:
``` -#### 4.3.1 BlurFilter(模糊滤镜) +#### 4.3.1 模糊滤镜(BlurFilter) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| @@ -731,7 +728,7 @@ PAGX 文档采用层级结构组织内容: | `blurrinessY` | float | (必填) | Y 模糊半径 | | `tileMode` | TileMode | decal | 平铺模式 | -#### 4.3.2 DropShadowFilter(投影阴影滤镜) +#### 4.3.2 投影阴影滤镜(DropShadowFilter) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| @@ -742,7 +739,7 @@ PAGX 文档采用层级结构组织内容: | `color` | color | #000000 | 阴影颜色 | | `shadowOnly` | bool | false | 仅显示阴影 | -#### 4.3.3 InnerShadowFilter(内阴影滤镜) +#### 4.3.3 内阴影滤镜(InnerShadowFilter) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| @@ -753,7 +750,7 @@ PAGX 文档采用层级结构组织内容: | `color` | color | #000000 | 阴影颜色 | | `shadowOnly` | bool | false | 仅显示阴影 | -#### 4.3.4 BlendFilter(混合滤镜) +#### 4.3.4 混合滤镜(BlendFilter) 将指定颜色以指定混合模式叠加到图层上。 @@ -762,7 +759,7 @@ PAGX 文档采用层级结构组织内容: | `color` | color | (必填) | 混合颜色 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1) | -#### 4.3.5 ColorMatrixFilter(颜色矩阵滤镜) +#### 4.3.5 颜色矩阵滤镜(ColorMatrixFilter) 使用 4×5 颜色矩阵变换颜色。 @@ -779,7 +776,7 @@ PAGX 文档采用层级结构组织内容: | 1 | ``` -### 4.4 Clipping and Masking(裁剪与遮罩) +### 4.4 裁剪与遮罩(Clipping and Masking) #### 4.4.1 scrollRect(滚动裁剪) @@ -819,11 +816,11 @@ PAGX 文档采用层级结构组织内容: --- -## 5. VectorElement System(矢量元素系统) +## 5. 矢量元素系统(VectorElement System) 矢量元素系统定义了图层 `` 内的矢量内容如何被处理和渲染。 -### 5.1 Processing Model(处理模型) +### 5.1 处理模型(Processing Model) VectorElement 系统采用**累积-渲染**的处理模型:几何元素在渲染上下文中累积,修改器对累积的几何进行变换,绘制器触发最终渲染。 @@ -886,11 +883,11 @@ VectorElement 按**文档顺序**依次处理,文档中靠前的元素先处 | 文本修改器(TextModifier、TextPath、TextLayout) | 仅字形列表 | 对 Path 无效 | | 复制器(Repeater) | Path + 字形列表 | 同时作用于所有几何 | -### 5.2 Geometry Elements(几何元素) +### 5.2 几何元素(Geometry Elements) 几何元素提供可渲染的形状。 -#### 5.2.1 Rectangle(矩形) +#### 5.2.1 矩形(Rectangle) 矩形从中心点定义,支持统一圆角。 @@ -919,7 +916,7 @@ rect.bottom = center.y + size.height / 2 **路径起点**:矩形路径从**右上角**开始,顺时针方向绘制(`reversed="false"` 时)。 -#### 5.2.2 Ellipse(椭圆) +#### 5.2.2 椭圆(Ellipse) 椭圆从中心点定义。 @@ -943,7 +940,7 @@ boundingRect.bottom = center.y + size.height / 2 **路径起点**:椭圆路径从**右侧中点**(3 点钟方向)开始。 -#### 5.2.3 Polystar(多边形/星形) +#### 5.2.3 多边形/星形(Polystar) 支持正多边形和星形两种模式。 @@ -996,7 +993,7 @@ y = center.y + outerRadius * sin(angle) - 0 表示尖角,1 表示完全圆滑 - 圆度通过在顶点处添加贝塞尔控制点实现 -#### 5.2.4 Path(路径) +#### 5.2.4 路径(Path) 使用 SVG 路径语法定义任意形状,支持内联数据或引用 Resources 中定义的 PathData。 @@ -1013,7 +1010,7 @@ y = center.y + outerRadius * sin(angle) | `data` | string/idref | (必填) | SVG 路径数据或 PathData 资源引用 "#id" | | `reversed` | bool | false | 反转路径方向 | -#### 5.2.5 TextSpan(文本片段) +#### 5.2.5 文本片段(TextSpan) 文本片段提供文本内容的几何形状。一个 TextSpan 经过塑形后会产生**字形列表**(多个字形),而非单一 Path。 @@ -1042,11 +1039,11 @@ y = center.y + outerRadius * sin(angle) **字体回退**:当指定字体不可用时,按平台默认字体回退链选择替代字体。 -### 5.3 Painters(绘制器) +### 5.3 绘制器(Painters) 绘制器(Fill、Stroke)对**当前时刻**累积的所有几何(Path 和字形列表)进行渲染。 -#### 5.3.1 Fill(填充) +#### 5.3.1 填充(Fill) 填充使用指定的颜色源绘制几何的内部区域。 @@ -1091,7 +1088,7 @@ y = center.y + outerRadius * sin(angle) - 支持通过 TextModifier 对单个字形应用颜色覆盖 - 颜色覆盖采用 alpha 混合:`finalColor = lerp(originalColor, overrideColor, overrideAlpha)` -#### 5.3.2 Stroke(描边) +#### 5.3.2 描边(Stroke) 描边沿几何边界绘制线条。 @@ -1157,7 +1154,7 @@ y = center.y + outerRadius * sin(angle) - `dashes`:定义虚线段长度序列,如 `"5,3"` 表示 5px 实线 + 3px 空白 - `dashOffset`:虚线起始偏移量 -#### 5.3.3 LayerPlacement(绘制位置) +#### 5.3.3 绘制位置(LayerPlacement) Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: @@ -1166,11 +1163,11 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: | `background` | 在子图层**下方**绘制(默认) | | `foreground` | 在子图层**上方**绘制 | -### 5.4 Shape Modifiers(形状修改器) +### 5.4 形状修改器(Shape Modifiers) 形状修改器对累积的 Path 进行**原地变换**,对字形列表则触发强制转换为 Path。 -#### 5.4.1 TrimPath(路径裁剪) +#### 5.4.1 路径裁剪(TrimPath) 裁剪路径到指定的起止范围。 @@ -1204,7 +1201,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: ``` -#### 5.4.2 RoundCorner(圆角) +#### 5.4.2 圆角(RoundCorner) 将路径的尖角转换为圆角。 @@ -1221,7 +1218,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: - 圆角半径自动限制为不超过相邻边长度的一半 - `radius <= 0` 时不执行任何操作 -#### 5.4.3 MergePath(路径合并) +#### 5.4.3 路径合并(MergePath) 将所有形状合并为单个形状。 @@ -1257,7 +1254,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: ``` -### 5.5 Text Modifiers(文本修改器) +### 5.5 文本修改器(Text Modifiers) 文本修改器对文本中的独立字形进行变换。 @@ -1319,7 +1316,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: ``` -#### 5.5.3 TextModifier(文本变换器) +#### 5.5.3 文本变换器(TextModifier) 对选定范围内的字形应用变换和样式覆盖。 @@ -1376,7 +1373,7 @@ blendFactor = overrideColor.alpha × |factor| finalColor = blend(originalColor, overrideColor, blendFactor) ``` -#### 5.5.4 RangeSelector(范围选择器) +#### 5.5.4 范围选择器(RangeSelector) 范围选择器定义 TextModifier 影响的字形范围和影响程度。 @@ -1427,7 +1424,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | `max` | 最大:取最大值 | | `difference` | 差值:取绝对差值 | -#### 5.5.5 TextPath(文本路径) +#### 5.5.5 文本路径(TextPath) 将文本沿指定路径排列。 @@ -1466,7 +1463,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) **强制对齐**:`forceAlignment="true"` 时,自动调整字间距以填满可用路径长度(减去边距)。 -#### 5.5.6 TextLayout(文本排版) +#### 5.5.6 文本排版(TextLayout) 文本排版修改器对累积的文本元素应用段落排版,是 PAGX 格式特有的元素。与 TextPath 类似,TextLayout 作用于累积的字形列表,为其应用自动换行和对齐。 @@ -1534,7 +1531,7 @@ finalColor = blend(originalColor, overrideColor, blendFactor) ``` -### 5.6 Repeater(复制器) +### 5.6 复制器(Repeater) 复制累积的内容和已渲染的样式,对每个副本应用渐进变换。Repeater 对 Path 和字形列表同时生效,且不会触发文本转形状。 @@ -1608,7 +1605,7 @@ alpha = lerp(startAlpha, endAlpha, t) ``` -### 5.7 Group(容器) +### 5.7 容器(Group) Group 是带变换属性的矢量元素容器。 @@ -1739,7 +1736,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: --- -## Appendix A. Node Hierarchy(节点层级与包含关系) +## 附录 A. 节点层级与包含关系(Node Hierarchy) 本附录描述节点的分类和嵌套规则。 @@ -1816,7 +1813,7 @@ Layer.contents / Group --- -## Appendix B. Examples(示例) +## 附录 B. 常见用法示例(Examples) ### B.1 完整示例 @@ -1989,7 +1986,7 @@ Layer.contents / Group --- -## Appendix C. Node and Attribute Reference(节点与属性速查) +## 附录 C. 节点与属性速查(Node and Attribute Reference) 本附录列出所有节点的属性定义,省略详细说明。 From 829b0d170713514ebe57a09fa4337f35374a7ad5 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:06:18 +0800 Subject: [PATCH 083/678] Simplify Layer structure by removing wrapper elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove , , and wrapper elements from Layer. Child elements are now directly nested under Layer and automatically categorized by type: - VectorElement (Rectangle, Fill, etc.) → contents list - LayerStyle (DropShadowStyle, etc.) → styles list - LayerFilter (BlurFilter, etc.) → filters list - Layer → children list This results in a flatter, more concise XML structure while maintaining full semantic clarity through element type names. --- pagx/docs/pagx_spec.md | 184 +++++++++++++++++------------------------ 1 file changed, 77 insertions(+), 107 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 8f098f532f..601d87178d 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -49,7 +49,6 @@ PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视 |------|------|------| | 元素名 | PascalCase,不缩写 | `Group`、`Rectangle`、`Fill` | | 属性名 | camelCase,尽量简短 | `antiAlias`、`blendMode`、`fontSize` | -| 属性节点 | camelCase | ``、``、`` | | 默认单位 | 像素(无需标注) | `width="100"` | | 角度单位 | 度 | `rotation="45"` | @@ -82,12 +81,10 @@ PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视 **示例**: ```xml - - - - - - + + + + ``` ### 2.4 基本数值类型 @@ -437,10 +434,8 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 - - - - + + ``` @@ -454,10 +449,8 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 ```xml - - - - + + ``` @@ -474,18 +467,12 @@ PAGX 文档采用层级结构组织内容: ``` ← 根元素(定义画布尺寸) ├── ← 图层(可多个) -│ ├── ← 矢量内容(VectorElement 系统) -│ │ ├── 几何元素 ← Rectangle、Ellipse、Path、TextSpan 等 -│ │ ├── 修改器 ← TrimPath、RoundCorner、TextModifier 等 -│ │ ├── 绘制器 ← Fill、Stroke -│ │ └── ← 矢量元素容器(可嵌套) -│ │ -│ ├── ← 图层样式 -│ │ └── ← 投影、内阴影等 -│ │ -│ ├── ← 滤镜 -│ │ └── ← 模糊、颜色矩阵等 -│ │ +│ ├── 几何元素 ← Rectangle、Ellipse、Path、TextSpan 等 +│ ├── 修改器 ← TrimPath、RoundCorner、TextModifier 等 +│ ├── 绘制器 ← Fill、Stroke +│ ├── ← 矢量元素容器(可嵌套) +│ ├── LayerStyle ← DropShadowStyle、InnerShadowStyle 等 +│ ├── LayerFilter ← BlurFilter、ColorMatrixFilter 等 │ └── ← 子图层(递归结构) │ └── ... │ @@ -511,29 +498,30 @@ PAGX 文档采用层级结构组织内容: ```xml - - - - - - - - - - + + + + - - - - + + ``` -#### contents 子节点 +#### 子元素 + +Layer 的子元素按类型自动归类为四个集合: + +| 子元素类型 | 归类 | 说明 | +|-----------|------|------| +| VectorElement | contents | 几何元素、修改器、绘制器(参与累积处理) | +| LayerStyle | styles | DropShadowStyle、InnerShadowStyle、BackgroundBlurStyle | +| LayerFilter | filters | BlurFilter、DropShadowFilter 等滤镜 | +| Layer | children | 嵌套子图层 | -`` 是图层的矢量内容容器,本身相当于一个不带变换属性的 Group,可直接包含几何元素、修改器、绘制器等 VectorElement。 +**建议顺序**:虽然子元素顺序不影响解析结果,但建议按 VectorElement → LayerStyle → LayerFilter → 子Layer 的顺序书写,以提高可读性。 #### 图层属性 @@ -624,13 +612,9 @@ PAGX 文档采用层级结构组织内容: ```xml - - - - - - - + + + ``` @@ -641,11 +625,13 @@ PAGX 文档采用层级结构组织内容: 图层样式在图层内容渲染完成后应用。 ```xml - + + + - + ``` **所有 LayerStyle 共有属性**: @@ -713,11 +699,13 @@ PAGX 文档采用层级结构组织内容: 滤镜按文档顺序链式应用,每个滤镜的输出作为下一个滤镜的输入。 ```xml - + + + - + ``` #### 4.3.1 模糊滤镜(BlurFilter) @@ -784,10 +772,8 @@ PAGX 文档采用层级结构组织内容: ```xml - - - - + + ``` @@ -797,16 +783,14 @@ PAGX 文档采用层级结构组织内容: ```xml - - - - + + - - - - + + + +``` ``` @@ -818,7 +802,7 @@ PAGX 文档采用层级结构组织内容: ## 5. 矢量元素系统(VectorElement System) -矢量元素系统定义了图层 `` 内的矢量内容如何被处理和渲染。 +矢量元素系统定义了 Layer 内的矢量内容如何被处理和渲染。 ### 5.1 处理模型(Processing Model) @@ -1674,17 +1658,15 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: **示例 2 - 子 Group 几何向上累积**: ```xml - - - - - - - - - - - + + + + + + + + + ``` **示例 3 - 多个绘制器复用几何**: @@ -1823,26 +1805,20 @@ Layer.contents / Group - - - - + + - - - - - - - - - - - - + + + + + + + + @@ -1851,17 +1827,13 @@ Layer.contents / Group - - - - + + - - - - + + @@ -1872,11 +1844,9 @@ Layer.contents / Group - - - - + + From d2e6486bec0a1f8f670635da2ac59151785d5fdf Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:08:05 +0800 Subject: [PATCH 084/678] Add note clarifying Layer vs Group geometry accumulation behavior --- pagx/docs/pagx_spec.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 601d87178d..f970c18c23 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -523,6 +523,8 @@ Layer 的子元素按类型自动归类为四个集合: **建议顺序**:虽然子元素顺序不影响解析结果,但建议按 VectorElement → LayerStyle → LayerFilter → 子Layer 的顺序书写,以提高可读性。 +**与 Group 的区别**:Layer 是几何累积的终止点,其内部的几何不会向上传递;而 Group 处理完成后,几何会累积到父作用域供后续绘制器使用。 + #### 图层属性 | 属性 | 类型 | 默认值 | 说明 | From 105810217e348d0d0dc998f43b0808b02f1856d0 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:11:20 +0800 Subject: [PATCH 085/678] Move Layer/Group accumulation distinction to Group section --- pagx/docs/pagx_spec.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index f970c18c23..f803346638 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -523,8 +523,6 @@ Layer 的子元素按类型自动归类为四个集合: **建议顺序**:虽然子元素顺序不影响解析结果,但建议按 VectorElement → LayerStyle → LayerFilter → 子Layer 的顺序书写,以提高可读性。 -**与 Group 的区别**:Layer 是几何累积的终止点,其内部的几何不会向上传递;而 Group 处理完成后,几何会累积到父作用域供后续绘制器使用。 - #### 图层属性 | 属性 | 类型 | 默认值 | 说明 | @@ -1647,6 +1645,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - **子 Group 几何向上累积**:子 Group 处理完成后,其几何会累积到父作用域,父级末尾的绘制器可以渲染所有子 Group 的几何 - **同级 Group 互不影响**:每个 Group 创建独立的累积起点,不会看到后续兄弟 Group 的几何 - **隔离渲染范围**:Group 内的绘制器只能渲染到当前位置已累积的几何,包括本组和已完成的子 Group +- **Layer 是累积终止点**:几何向上累积直到遇到 Layer 边界,不会跨 Layer 传递 **示例 1 - 基本隔离**: ```xml From 3a84041301ff9b581054dd8e6c655c3494f76d31 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:14:16 +0800 Subject: [PATCH 086/678] Remove Node base class from PAGX while keeping NodeType enum for type checking. --- pagx/include/pagx/LayerBuilder.h | 4 +- pagx/include/pagx/PAGXModel.h | 66 +---- pagx/include/pagx/PAGXNode.h | 2 +- pagx/include/pagx/PAGXSVGParser.h | 8 +- pagx/include/pagx/PAGXTypes.h | 4 +- pagx/include/pagx/model/BackgroundBlurStyle.h | 46 ++++ pagx/include/pagx/model/BlendFilter.h | 44 ++++ pagx/include/pagx/model/BlurFilter.h | 44 ++++ pagx/include/pagx/model/ColorMatrixFilter.h | 42 ++++ pagx/include/pagx/model/ColorSource.h | 57 +++++ pagx/include/pagx/model/ColorStop.h | 34 +++ pagx/include/pagx/model/Composition.h | 53 ++++ pagx/include/pagx/model/ConicGradient.h | 58 +++++ pagx/include/pagx/model/DiamondGradient.h | 57 +++++ pagx/include/pagx/model/Document.h | 121 +++++++++ pagx/include/pagx/model/DropShadowFilter.h | 47 ++++ pagx/include/pagx/model/DropShadowStyle.h | 49 ++++ pagx/include/pagx/model/Element.h | 120 +++++++++ pagx/include/pagx/model/Ellipse.h | 55 +++++ pagx/include/pagx/model/Fill.h | 81 +++++++ pagx/include/pagx/model/Group.h | 84 +++++++ pagx/include/pagx/model/Image.h | 47 ++++ pagx/include/pagx/model/ImagePattern.h | 58 +++++ pagx/include/pagx/model/InnerShadowFilter.h | 47 ++++ pagx/include/pagx/model/InnerShadowStyle.h | 48 ++++ pagx/include/pagx/model/Layer.h | 162 +++++++++++++ pagx/include/pagx/model/LayerFilter.h | 62 +++++ pagx/include/pagx/model/LayerStyle.h | 60 +++++ pagx/include/pagx/model/LinearGradient.h | 57 +++++ pagx/include/pagx/model/MergePath.h | 46 ++++ pagx/include/pagx/model/Model.h | 88 +++++++ pagx/include/pagx/model/NodeType.h | 91 +++++++ pagx/include/pagx/model/Path.h | 51 ++++ pagx/include/pagx/model/PathData.h | 176 ++++++++++++++ pagx/include/pagx/model/PathDataResource.h | 47 ++++ pagx/include/pagx/model/Polystar.h | 88 +++++++ pagx/include/pagx/model/RadialGradient.h | 57 +++++ pagx/include/pagx/model/RangeSelector.h | 45 ++++ pagx/include/pagx/model/Rectangle.h | 60 +++++ pagx/include/pagx/model/Repeater.h | 89 +++++++ pagx/include/pagx/model/Resource.h | 100 ++++++++ pagx/include/pagx/model/RoundCorner.h | 45 ++++ pagx/include/pagx/model/SolidColor.h | 52 ++++ pagx/include/pagx/model/Stroke.h | 113 +++++++++ pagx/include/pagx/model/TextLayout.h | 79 ++++++ pagx/include/pagx/model/TextModifier.h | 101 ++++++++ pagx/include/pagx/model/TextPath.h | 78 ++++++ pagx/include/pagx/model/TextSpan.h | 87 +++++++ pagx/include/pagx/model/TrimPath.h | 66 +++++ pagx/include/pagx/model/types/BlendMode.h | 52 ++++ pagx/include/pagx/model/types/Color.h | 170 +++++++++++++ pagx/include/pagx/model/types/Enums.h | 54 +++++ pagx/include/pagx/model/types/FillRule.h | 36 +++ pagx/include/pagx/model/types/FontStyle.h | 36 +++ pagx/include/pagx/model/types/LineCap.h | 37 +++ pagx/include/pagx/model/types/LineJoin.h | 37 +++ pagx/include/pagx/model/types/MaskType.h | 37 +++ pagx/include/pagx/model/types/Matrix.h | 160 ++++++++++++ pagx/include/pagx/model/types/MergePathMode.h | 39 +++ pagx/include/pagx/model/types/Overflow.h | 37 +++ pagx/include/pagx/model/types/Placement.h | 36 +++ pagx/include/pagx/model/types/Point.h | 39 +++ pagx/include/pagx/model/types/PolystarType.h | 36 +++ pagx/include/pagx/model/types/Rect.h | 79 ++++++ pagx/include/pagx/model/types/RepeaterOrder.h | 36 +++ pagx/include/pagx/model/types/SamplingMode.h | 37 +++ pagx/include/pagx/model/types/SelectorMode.h | 40 +++ pagx/include/pagx/model/types/SelectorShape.h | 40 +++ pagx/include/pagx/model/types/SelectorUnit.h | 36 +++ pagx/include/pagx/model/types/Size.h | 39 +++ pagx/include/pagx/model/types/StrokeAlign.h | 37 +++ pagx/include/pagx/model/types/TextAlign.h | 38 +++ pagx/include/pagx/model/types/TextPathAlign.h | 37 +++ pagx/include/pagx/model/types/TileMode.h | 38 +++ pagx/include/pagx/model/types/TrimType.h | 36 +++ pagx/include/pagx/model/types/Types.h | 229 ++++++++++++++++++ pagx/include/pagx/model/types/VerticalAlign.h | 37 +++ pagx/src/PAGXDocument.cpp | 52 +--- pagx/src/PAGXElement.cpp | 112 +++++++-- pagx/src/PAGXTypes.cpp | 80 +++++- pagx/src/PAGXXMLParser.cpp | 52 ++-- pagx/src/PAGXXMLParser.h | 18 +- pagx/src/PAGXXMLWriter.cpp | 43 ++-- pagx/src/PAGXXMLWriter.h | 4 +- pagx/src/PathData.cpp | 2 +- pagx/src/svg/PAGXSVGParser.cpp | 53 ++-- pagx/src/svg/SVGParserInternal.h | 34 +-- pagx/src/tgfx/LayerBuilder.cpp | 19 +- test/src/PAGXTest.cpp | 10 +- 89 files changed, 4964 insertions(+), 256 deletions(-) create mode 100644 pagx/include/pagx/model/BackgroundBlurStyle.h create mode 100644 pagx/include/pagx/model/BlendFilter.h create mode 100644 pagx/include/pagx/model/BlurFilter.h create mode 100644 pagx/include/pagx/model/ColorMatrixFilter.h create mode 100644 pagx/include/pagx/model/ColorSource.h create mode 100644 pagx/include/pagx/model/ColorStop.h create mode 100644 pagx/include/pagx/model/Composition.h create mode 100644 pagx/include/pagx/model/ConicGradient.h create mode 100644 pagx/include/pagx/model/DiamondGradient.h create mode 100644 pagx/include/pagx/model/Document.h create mode 100644 pagx/include/pagx/model/DropShadowFilter.h create mode 100644 pagx/include/pagx/model/DropShadowStyle.h create mode 100644 pagx/include/pagx/model/Element.h create mode 100644 pagx/include/pagx/model/Ellipse.h create mode 100644 pagx/include/pagx/model/Fill.h create mode 100644 pagx/include/pagx/model/Group.h create mode 100644 pagx/include/pagx/model/Image.h create mode 100644 pagx/include/pagx/model/ImagePattern.h create mode 100644 pagx/include/pagx/model/InnerShadowFilter.h create mode 100644 pagx/include/pagx/model/InnerShadowStyle.h create mode 100644 pagx/include/pagx/model/Layer.h create mode 100644 pagx/include/pagx/model/LayerFilter.h create mode 100644 pagx/include/pagx/model/LayerStyle.h create mode 100644 pagx/include/pagx/model/LinearGradient.h create mode 100644 pagx/include/pagx/model/MergePath.h create mode 100644 pagx/include/pagx/model/Model.h create mode 100644 pagx/include/pagx/model/NodeType.h create mode 100644 pagx/include/pagx/model/Path.h create mode 100644 pagx/include/pagx/model/PathData.h create mode 100644 pagx/include/pagx/model/PathDataResource.h create mode 100644 pagx/include/pagx/model/Polystar.h create mode 100644 pagx/include/pagx/model/RadialGradient.h create mode 100644 pagx/include/pagx/model/RangeSelector.h create mode 100644 pagx/include/pagx/model/Rectangle.h create mode 100644 pagx/include/pagx/model/Repeater.h create mode 100644 pagx/include/pagx/model/Resource.h create mode 100644 pagx/include/pagx/model/RoundCorner.h create mode 100644 pagx/include/pagx/model/SolidColor.h create mode 100644 pagx/include/pagx/model/Stroke.h create mode 100644 pagx/include/pagx/model/TextLayout.h create mode 100644 pagx/include/pagx/model/TextModifier.h create mode 100644 pagx/include/pagx/model/TextPath.h create mode 100644 pagx/include/pagx/model/TextSpan.h create mode 100644 pagx/include/pagx/model/TrimPath.h create mode 100644 pagx/include/pagx/model/types/BlendMode.h create mode 100644 pagx/include/pagx/model/types/Color.h create mode 100644 pagx/include/pagx/model/types/Enums.h create mode 100644 pagx/include/pagx/model/types/FillRule.h create mode 100644 pagx/include/pagx/model/types/FontStyle.h create mode 100644 pagx/include/pagx/model/types/LineCap.h create mode 100644 pagx/include/pagx/model/types/LineJoin.h create mode 100644 pagx/include/pagx/model/types/MaskType.h create mode 100644 pagx/include/pagx/model/types/Matrix.h create mode 100644 pagx/include/pagx/model/types/MergePathMode.h create mode 100644 pagx/include/pagx/model/types/Overflow.h create mode 100644 pagx/include/pagx/model/types/Placement.h create mode 100644 pagx/include/pagx/model/types/Point.h create mode 100644 pagx/include/pagx/model/types/PolystarType.h create mode 100644 pagx/include/pagx/model/types/Rect.h create mode 100644 pagx/include/pagx/model/types/RepeaterOrder.h create mode 100644 pagx/include/pagx/model/types/SamplingMode.h create mode 100644 pagx/include/pagx/model/types/SelectorMode.h create mode 100644 pagx/include/pagx/model/types/SelectorShape.h create mode 100644 pagx/include/pagx/model/types/SelectorUnit.h create mode 100644 pagx/include/pagx/model/types/Size.h create mode 100644 pagx/include/pagx/model/types/StrokeAlign.h create mode 100644 pagx/include/pagx/model/types/TextAlign.h create mode 100644 pagx/include/pagx/model/types/TextPathAlign.h create mode 100644 pagx/include/pagx/model/types/TileMode.h create mode 100644 pagx/include/pagx/model/types/TrimType.h create mode 100644 pagx/include/pagx/model/types/Types.h create mode 100644 pagx/include/pagx/model/types/VerticalAlign.h diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index c84f09ecf2..3b605aa743 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -21,7 +21,7 @@ #include #include #include -#include "pagx/PAGXDocument.h" +#include "pagx/model/Document.h" #include "tgfx/core/Typeface.h" #include "tgfx/layers/Layer.h" @@ -81,7 +81,7 @@ class LayerBuilder { /** * Builds a layer tree from a PAGXDocument. */ - static PAGXContent Build(const PAGXDocument& document, const Options& options = Options()); + static PAGXContent Build(const Document& document, const Options& options = Options()); /** * Builds a layer tree from a PAGX file. diff --git a/pagx/include/pagx/PAGXModel.h b/pagx/include/pagx/PAGXModel.h index 99b94b756f..4ecddd7909 100644 --- a/pagx/include/pagx/PAGXModel.h +++ b/pagx/include/pagx/PAGXModel.h @@ -18,68 +18,4 @@ #pragma once -// Basic types and enums -#include "pagx/types/Enums.h" -#include "pagx/types/Types.h" - -// Base classes -#include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/LayerFilter.h" -#include "pagx/nodes/LayerStyle.h" -#include "pagx/nodes/Node.h" -#include "pagx/nodes/VectorElement.h" - -// Color sources -#include "pagx/nodes/ColorStop.h" -#include "pagx/nodes/ConicGradient.h" -#include "pagx/nodes/DiamondGradient.h" -#include "pagx/nodes/ImagePattern.h" -#include "pagx/nodes/LinearGradient.h" -#include "pagx/nodes/RadialGradient.h" -#include "pagx/nodes/SolidColor.h" - -// Vector elements - shapes -#include "pagx/nodes/Ellipse.h" -#include "pagx/nodes/Path.h" -#include "pagx/nodes/Polystar.h" -#include "pagx/nodes/Rectangle.h" -#include "pagx/nodes/TextSpan.h" - -// Vector elements - painters -#include "pagx/nodes/Fill.h" -#include "pagx/nodes/Stroke.h" - -// Vector elements - path modifiers -#include "pagx/nodes/MergePath.h" -#include "pagx/nodes/RoundCorner.h" -#include "pagx/nodes/TrimPath.h" - -// Vector elements - text modifiers -#include "pagx/nodes/RangeSelector.h" -#include "pagx/nodes/TextLayout.h" -#include "pagx/nodes/TextModifier.h" -#include "pagx/nodes/TextPath.h" - -// Vector elements - containers -#include "pagx/nodes/Group.h" -#include "pagx/nodes/Repeater.h" - -// Layer styles -#include "pagx/nodes/BackgroundBlurStyle.h" -#include "pagx/nodes/DropShadowStyle.h" -#include "pagx/nodes/InnerShadowStyle.h" - -// Layer filters -#include "pagx/nodes/BlendFilter.h" -#include "pagx/nodes/BlurFilter.h" -#include "pagx/nodes/ColorMatrixFilter.h" -#include "pagx/nodes/DropShadowFilter.h" -#include "pagx/nodes/InnerShadowFilter.h" - -// Resources -#include "pagx/nodes/Composition.h" -#include "pagx/nodes/Image.h" -#include "pagx/nodes/PathDataResource.h" - -// Layer -#include "pagx/nodes/Layer.h" +#include "pagx/model/Model.h" diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h index bcc16fe4f3..4ecddd7909 100644 --- a/pagx/include/pagx/PAGXNode.h +++ b/pagx/include/pagx/PAGXNode.h @@ -18,4 +18,4 @@ #pragma once -#include "pagx/PAGXModel.h" +#include "pagx/model/Model.h" diff --git a/pagx/include/pagx/PAGXSVGParser.h b/pagx/include/pagx/PAGXSVGParser.h index 380ba99beb..072ef87a17 100644 --- a/pagx/include/pagx/PAGXSVGParser.h +++ b/pagx/include/pagx/PAGXSVGParser.h @@ -20,7 +20,7 @@ #include #include -#include "pagx/PAGXDocument.h" +#include "pagx/model/Document.h" namespace pagx { @@ -53,19 +53,19 @@ class PAGXSVGParser { /** * Parses an SVG file and creates a PAGXDocument. */ - static std::shared_ptr Parse(const std::string& filePath, + static std::shared_ptr Parse(const std::string& filePath, const Options& options = Options()); /** * Parses SVG data and creates a PAGXDocument. */ - static std::shared_ptr Parse(const uint8_t* data, size_t length, + static std::shared_ptr Parse(const uint8_t* data, size_t length, const Options& options = Options()); /** * Parses an SVG string and creates a PAGXDocument. */ - static std::shared_ptr ParseString(const std::string& svgContent, + static std::shared_ptr ParseString(const std::string& svgContent, const Options& options = Options()); }; diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h index e6966ec81f..54c4bb9de8 100644 --- a/pagx/include/pagx/PAGXTypes.h +++ b/pagx/include/pagx/PAGXTypes.h @@ -18,5 +18,5 @@ #pragma once -#include "pagx/types/Enums.h" -#include "pagx/types/Types.h" +#include "pagx/model/types/Enums.h" +#include "pagx/model/types/Types.h" diff --git a/pagx/include/pagx/model/BackgroundBlurStyle.h b/pagx/include/pagx/model/BackgroundBlurStyle.h new file mode 100644 index 0000000000..1e134c1a35 --- /dev/null +++ b/pagx/include/pagx/model/BackgroundBlurStyle.h @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/LayerStyle.h" +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/TileMode.h" + +namespace pagx { + +/** + * Background blur style. + */ +class BackgroundBlurStyle : public LayerStyle { + public: + float blurrinessX = 0; + float blurrinessY = 0; + TileMode tileMode = TileMode::Mirror; + BlendMode blendMode = BlendMode::Normal; + + LayerStyleType layerStyleType() const override { + return LayerStyleType::BackgroundBlurStyle; + } + + NodeType type() const override { + return NodeType::BackgroundBlurStyle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/BlendFilter.h b/pagx/include/pagx/model/BlendFilter.h new file mode 100644 index 0000000000..154513ad5d --- /dev/null +++ b/pagx/include/pagx/model/BlendFilter.h @@ -0,0 +1,44 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/LayerFilter.h" +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Blend filter. + */ +class BlendFilter : public LayerFilter { + public: + Color color = {}; + BlendMode blendMode = BlendMode::Normal; + + NodeType type() const override { + return NodeType::BlendFilter; + } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::BlendFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/BlurFilter.h b/pagx/include/pagx/model/BlurFilter.h new file mode 100644 index 0000000000..1fd373c1cd --- /dev/null +++ b/pagx/include/pagx/model/BlurFilter.h @@ -0,0 +1,44 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/LayerFilter.h" +#include "pagx/model/types/TileMode.h" + +namespace pagx { + +/** + * Blur filter. + */ +class BlurFilter : public LayerFilter { + public: + float blurrinessX = 0; + float blurrinessY = 0; + TileMode tileMode = TileMode::Decal; + + NodeType type() const override { + return NodeType::BlurFilter; + } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::BlurFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/ColorMatrixFilter.h b/pagx/include/pagx/model/ColorMatrixFilter.h new file mode 100644 index 0000000000..247cc3f16f --- /dev/null +++ b/pagx/include/pagx/model/ColorMatrixFilter.h @@ -0,0 +1,42 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/LayerFilter.h" + +namespace pagx { + +/** + * Color matrix filter. + */ +class ColorMatrixFilter : public LayerFilter { + public: + std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; + + NodeType type() const override { + return NodeType::ColorMatrixFilter; + } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::ColorMatrixFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/ColorSource.h b/pagx/include/pagx/model/ColorSource.h new file mode 100644 index 0000000000..3567500729 --- /dev/null +++ b/pagx/include/pagx/model/ColorSource.h @@ -0,0 +1,57 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Resource.h" + +namespace pagx { + +/** + * Color source types. + */ +enum class ColorSourceType { + SolidColor, + LinearGradient, + RadialGradient, + ConicGradient, + DiamondGradient, + ImagePattern +}; + +/** + * Returns the string name of a color source type. + */ +const char* ColorSourceTypeName(ColorSourceType type); + +/** + * Base class for color sources (SolidColor, gradients, ImagePattern). + * ColorSource can be used both inline in painters and as standalone resources. + */ +class ColorSource : public Resource { + public: + /** + * Returns the color source type of this color source. + */ + virtual ColorSourceType colorSourceType() const = 0; + + protected: + ColorSource() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/ColorStop.h b/pagx/include/pagx/model/ColorStop.h new file mode 100644 index 0000000000..e7b51fcf56 --- /dev/null +++ b/pagx/include/pagx/model/ColorStop.h @@ -0,0 +1,34 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * A color stop in a gradient. + */ +class ColorStop { + public: + float offset = 0; + Color color = {}; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Composition.h b/pagx/include/pagx/model/Composition.h new file mode 100644 index 0000000000..41910fb4e3 --- /dev/null +++ b/pagx/include/pagx/model/Composition.h @@ -0,0 +1,53 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/model/Resource.h" + +namespace pagx { + +class Layer; + +/** + * Composition resource. + */ +class Composition : public Resource { + public: + std::string id = {}; + float width = 0; + float height = 0; + std::vector> layers = {}; + + NodeType type() const override { + return NodeType::Composition; + } + + ResourceType resourceType() const override { + return ResourceType::Composition; + } + + const std::string& resourceId() const override { + return id; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/ConicGradient.h b/pagx/include/pagx/model/ConicGradient.h new file mode 100644 index 0000000000..b35c5e81f8 --- /dev/null +++ b/pagx/include/pagx/model/ConicGradient.h @@ -0,0 +1,58 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/ColorStop.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * A conic (sweep) gradient. + */ +class ConicGradient : public ColorSource { + public: + std::string id = {}; + Point center = {}; + float startAngle = 0; + float endAngle = 360; + Matrix matrix = {}; + std::vector colorStops = {}; + + ColorSourceType colorSourceType() const override { + return ColorSourceType::ConicGradient; + } + + ResourceType resourceType() const override { + return ResourceType::ConicGradient; + } + + const std::string& resourceId() const override { + return id; + } + + NodeType type() const override { + return NodeType::ConicGradient; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/DiamondGradient.h b/pagx/include/pagx/model/DiamondGradient.h new file mode 100644 index 0000000000..c5a3df1270 --- /dev/null +++ b/pagx/include/pagx/model/DiamondGradient.h @@ -0,0 +1,57 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/ColorStop.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * A diamond gradient. + */ +class DiamondGradient : public ColorSource { + public: + std::string id = {}; + Point center = {}; + float halfDiagonal = 0; + Matrix matrix = {}; + std::vector colorStops = {}; + + ColorSourceType colorSourceType() const override { + return ColorSourceType::DiamondGradient; + } + + ResourceType resourceType() const override { + return ResourceType::DiamondGradient; + } + + const std::string& resourceId() const override { + return id; + } + + NodeType type() const override { + return NodeType::DiamondGradient; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Document.h b/pagx/include/pagx/model/Document.h new file mode 100644 index 0000000000..b303d731c9 --- /dev/null +++ b/pagx/include/pagx/model/Document.h @@ -0,0 +1,121 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include "pagx/model/Model.h" + +namespace pagx { + +class PAGXXMLParser; + +/** + * Document is the root container for a PAGX document. + * It contains resources and layers, and provides methods for loading, saving, and manipulating + * the document. + */ +class Document { + public: + /** + * Format version. + */ + std::string version = "1.0"; + + /** + * Canvas width. + */ + float width = 0; + + /** + * Canvas height. + */ + float height = 0; + + /** + * Resources (images, gradients, compositions, etc.). + * These can be referenced by "#id" in the document. + */ + std::vector> resources = {}; + + /** + * Top-level layers. + */ + std::vector> layers = {}; + + /** + * Base path for resolving relative resource paths. + */ + std::string basePath = {}; + + /** + * Creates an empty document with the specified size. + */ + static std::shared_ptr Make(float width, float height); + + /** + * Loads a document from a file. + * Returns nullptr if the file cannot be loaded. + */ + static std::shared_ptr FromFile(const std::string& filePath); + + /** + * Parses a document from XML content. + * Returns nullptr if parsing fails. + */ + static std::shared_ptr FromXML(const std::string& xmlContent); + + /** + * Parses a document from XML data. + * Returns nullptr if parsing fails. + */ + static std::shared_ptr FromXML(const uint8_t* data, size_t length); + + /** + * Exports the document to XML format. + */ + std::string toXML() const; + + /** + * Finds a resource by ID. + * Returns nullptr if not found. + */ + Resource* findResource(const std::string& id) const; + + /** + * Finds a layer by ID (searches recursively). + * Returns nullptr if not found. + */ + Layer* findLayer(const std::string& id) const; + + private: + friend class PAGXXMLParser; + Document() = default; + + mutable std::unordered_map resourceMap = {}; + mutable bool resourceMapDirty = true; + + void rebuildResourceMap() const; + static Layer* findLayerRecursive(const std::vector>& layers, + const std::string& id); +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/DropShadowFilter.h b/pagx/include/pagx/model/DropShadowFilter.h new file mode 100644 index 0000000000..b81e77f20a --- /dev/null +++ b/pagx/include/pagx/model/DropShadowFilter.h @@ -0,0 +1,47 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/LayerFilter.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Drop shadow filter. + */ +class DropShadowFilter : public LayerFilter { + public: + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool shadowOnly = false; + + NodeType type() const override { + return NodeType::DropShadowFilter; + } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::DropShadowFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/DropShadowStyle.h b/pagx/include/pagx/model/DropShadowStyle.h new file mode 100644 index 0000000000..68e3f9486f --- /dev/null +++ b/pagx/include/pagx/model/DropShadowStyle.h @@ -0,0 +1,49 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/LayerStyle.h" +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Drop shadow style. + */ +class DropShadowStyle : public LayerStyle { + public: + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool showBehindLayer = true; + BlendMode blendMode = BlendMode::Normal; + + LayerStyleType layerStyleType() const override { + return LayerStyleType::DropShadowStyle; + } + + NodeType type() const override { + return NodeType::DropShadowStyle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Element.h b/pagx/include/pagx/model/Element.h new file mode 100644 index 0000000000..fd7f531e42 --- /dev/null +++ b/pagx/include/pagx/model/Element.h @@ -0,0 +1,120 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/NodeType.h" + +namespace pagx { + +/** + * ElementType enumerates all types of elements that can be placed in Layer.contents or + * Group.elements. + */ +enum class ElementType { + /** + * A rectangle shape with optional rounded corners. + */ + Rectangle, + /** + * An ellipse shape defined by center point and size. + */ + Ellipse, + /** + * A polygon or star shape with configurable points and roundness. + */ + Polystar, + /** + * A freeform path shape defined by a PathData. + */ + Path, + /** + * A text span that generates glyph paths for rendering. + */ + TextSpan, + /** + * A fill painter that fills shapes with a color or gradient. + */ + Fill, + /** + * A stroke painter that outlines shapes with a color or gradient. + */ + Stroke, + /** + * A path modifier that trims paths to a specified range. + */ + TrimPath, + /** + * A path modifier that rounds the corners of shapes. + */ + RoundCorner, + /** + * A path modifier that merges multiple paths using boolean operations. + */ + MergePath, + /** + * A text animator that modifies text appearance with range-based transformations. + */ + TextModifier, + /** + * A text animator that places text along a path. + */ + TextPath, + /** + * A text animator that controls text layout within a bounding box. + */ + TextLayout, + /** + * A container that groups multiple elements with its own transform. + */ + Group, + /** + * A modifier that creates multiple copies of preceding elements. + */ + Repeater +}; + +/** + * Returns the string name of an element type. + */ +const char* ElementTypeName(ElementType type); + +/** + * Element is the base class for all elements in a shape layer. It includes shapes (Rectangle, + * Ellipse, Polystar, Path, TextSpan), painters (Fill, Stroke), modifiers (TrimPath, RoundCorner, + * MergePath), text elements (TextModifier, TextPath, TextLayout), and containers (Group, Repeater). + */ +class Element { + public: + virtual ~Element() = default; + + /** + * Returns the element type of this element. + */ + virtual ElementType elementType() const = 0; + + /** + * Returns the unified node type of this element. + */ + virtual NodeType type() const = 0; + + protected: + Element() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Ellipse.h b/pagx/include/pagx/model/Ellipse.h new file mode 100644 index 0000000000..7abd8025c5 --- /dev/null +++ b/pagx/include/pagx/model/Ellipse.h @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Element.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Ellipse represents an ellipse shape defined by a center point and size. + */ +class Ellipse : public Element { + public: + /** + * The center point of the ellipse. + */ + Point center = {}; + + /** + * The size of the ellipse. The default value is {100, 100}. + */ + Size size = {100, 100}; + + /** + * Whether the path direction is reversed. The default value is false. + */ + bool reversed = false; + + ElementType elementType() const override { + return ElementType::Ellipse; + } + + NodeType type() const override { + return NodeType::Ellipse; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Fill.h b/pagx/include/pagx/model/Fill.h new file mode 100644 index 0000000000..b47f67569d --- /dev/null +++ b/pagx/include/pagx/model/Fill.h @@ -0,0 +1,81 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/Element.h" +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/FillRule.h" +#include "pagx/model/types/Placement.h" + +namespace pagx { + +/** + * Fill represents a fill painter that fills shapes with a solid color, gradient, or pattern. The + * color can be specified as a simple color string (e.g., "#FF0000"), a reference to a defined + * color source (e.g., "#gradientId"), or an inline ColorSource node. + */ +class Fill : public Element { + public: + /** + * The fill color as a string. Can be a hex color (e.g., "#FF0000"), a reference to a color + * source (e.g., "#gradientId"), or empty if colorSource is used. + */ + std::string color = {}; + + /** + * An inline color source node (SolidColor, LinearGradient, etc.) for complex fills. If provided, + * this takes precedence over the color string. + */ + std::unique_ptr colorSource = nullptr; + + /** + * The opacity of the fill, ranging from 0 (transparent) to 1 (opaque). The default value is 1. + */ + float alpha = 1; + + /** + * The blend mode used when compositing the fill. The default value is Normal. + */ + BlendMode blendMode = BlendMode::Normal; + + /** + * The fill rule that determines the interior of self-intersecting paths. The default value is + * Winding. + */ + FillRule fillRule = FillRule::Winding; + + /** + * The placement of the fill relative to strokes (Background or Foreground). The default value is + * Background. + */ + Placement placement = Placement::Background; + + ElementType elementType() const override { + return ElementType::Fill; + } + + NodeType type() const override { + return NodeType::Fill; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Group.h b/pagx/include/pagx/model/Group.h new file mode 100644 index 0000000000..776654504d --- /dev/null +++ b/pagx/include/pagx/model/Group.h @@ -0,0 +1,84 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/Element.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Group is a container that groups multiple vector elements with its own transform. It can contain + * shapes, painters, modifiers, and nested groups, allowing for hierarchical organization of + * content. + */ +class Group : public Element { + public: + /** + * The anchor point for transformations. + */ + Point anchorPoint = {}; + + /** + * The position offset of the group. + */ + Point position = {}; + + /** + * The rotation angle in degrees. The default value is 0. + */ + float rotation = 0; + + /** + * The scale factor as (scaleX, scaleY). The default value is {1, 1}. + */ + Point scale = {1, 1}; + + /** + * The skew angle in degrees. The default value is 0. + */ + float skew = 0; + + /** + * The axis angle in degrees for the skew transformation. The default value is 0. + */ + float skewAxis = 0; + + /** + * The opacity of the group, ranging from 0 to 1. The default value is 1. + */ + float alpha = 1; + + /** + * The child elements contained in this group. + */ + std::vector> elements = {}; + + ElementType elementType() const override { + return ElementType::Group; + } + + NodeType type() const override { + return NodeType::Group; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Image.h b/pagx/include/pagx/model/Image.h new file mode 100644 index 0000000000..07ab02f842 --- /dev/null +++ b/pagx/include/pagx/model/Image.h @@ -0,0 +1,47 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/Resource.h" + +namespace pagx { + +/** + * Image resource. + */ +class Image : public Resource { + public: + std::string id = {}; + std::string source = {}; + + NodeType type() const override { + return NodeType::Image; + } + + ResourceType resourceType() const override { + return ResourceType::Image; + } + + const std::string& resourceId() const override { + return id; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/ImagePattern.h b/pagx/include/pagx/model/ImagePattern.h new file mode 100644 index 0000000000..b90bc754a1 --- /dev/null +++ b/pagx/include/pagx/model/ImagePattern.h @@ -0,0 +1,58 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/types/SamplingMode.h" +#include "pagx/model/types/TileMode.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * An image pattern. + */ +class ImagePattern : public ColorSource { + public: + std::string id = {}; + std::string image = {}; + TileMode tileModeX = TileMode::Clamp; + TileMode tileModeY = TileMode::Clamp; + SamplingMode sampling = SamplingMode::Linear; + Matrix matrix = {}; + + ColorSourceType colorSourceType() const override { + return ColorSourceType::ImagePattern; + } + + ResourceType resourceType() const override { + return ResourceType::ImagePattern; + } + + const std::string& resourceId() const override { + return id; + } + + NodeType type() const override { + return NodeType::ImagePattern; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/InnerShadowFilter.h b/pagx/include/pagx/model/InnerShadowFilter.h new file mode 100644 index 0000000000..210d61886a --- /dev/null +++ b/pagx/include/pagx/model/InnerShadowFilter.h @@ -0,0 +1,47 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/LayerFilter.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Inner shadow filter. + */ +class InnerShadowFilter : public LayerFilter { + public: + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + bool shadowOnly = false; + + NodeType type() const override { + return NodeType::InnerShadowFilter; + } + + LayerFilterType layerFilterType() const override { + return LayerFilterType::InnerShadowFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/InnerShadowStyle.h b/pagx/include/pagx/model/InnerShadowStyle.h new file mode 100644 index 0000000000..da233812dc --- /dev/null +++ b/pagx/include/pagx/model/InnerShadowStyle.h @@ -0,0 +1,48 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/LayerStyle.h" +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Inner shadow style. + */ +class InnerShadowStyle : public LayerStyle { + public: + float offsetX = 0; + float offsetY = 0; + float blurrinessX = 0; + float blurrinessY = 0; + Color color = {}; + BlendMode blendMode = BlendMode::Normal; + + LayerStyleType layerStyleType() const override { + return LayerStyleType::InnerShadowStyle; + } + + NodeType type() const override { + return NodeType::InnerShadowStyle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Layer.h b/pagx/include/pagx/model/Layer.h new file mode 100644 index 0000000000..e5d49cdbe2 --- /dev/null +++ b/pagx/include/pagx/model/Layer.h @@ -0,0 +1,162 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include "pagx/model/Element.h" +#include "pagx/model/LayerFilter.h" +#include "pagx/model/LayerStyle.h" +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/MaskType.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Layer represents a layer node that can contain vector elements, layer styles, filters, and child + * layers. It is the main building block for composing visual content in a PAGX document. + */ +class Layer { + public: + /** + * The unique identifier of the layer. + */ + std::string id = {}; + + /** + * The display name of the layer. + */ + std::string name = {}; + + /** + * Whether the layer is visible. The default value is true. + */ + bool visible = true; + + /** + * The opacity of the layer, ranging from 0 to 1. The default value is 1. + */ + float alpha = 1; + + /** + * The blend mode used when compositing the layer. The default value is Normal. + */ + BlendMode blendMode = BlendMode::Normal; + + /** + * The x-coordinate of the layer position. The default value is 0. + */ + float x = 0; + + /** + * The y-coordinate of the layer position. The default value is 0. + */ + float y = 0; + + /** + * The 2D transformation matrix of the layer. + */ + Matrix matrix = {}; + + /** + * The 3D transformation matrix as a 16-element array in column-major order. + */ + std::vector matrix3D = {}; + + /** + * Whether to preserve 3D transformations for child layers. The default value is false. + */ + bool preserve3D = false; + + /** + * Whether to apply anti-aliasing to the layer edges. The default value is true. + */ + bool antiAlias = true; + + /** + * Whether to use group opacity mode for the layer and its children. The default value is false. + */ + bool groupOpacity = false; + + /** + * Whether layer effects pass through to the background. The default value is true. + */ + bool passThroughBackground = true; + + /** + * Whether to exclude child effects when applying layer styles. The default value is false. + */ + bool excludeChildEffectsInLayerStyle = false; + + /** + * The scroll rectangle for clipping the layer content. + */ + Rect scrollRect = {}; + + /** + * Whether a scroll rectangle is applied to the layer. The default value is false. + */ + bool hasScrollRect = false; + + /** + * A reference to a mask layer (e.g., "#maskId"). + */ + std::string mask = {}; + + /** + * The type of masking to apply (Alpha, Luminosity, InvertedAlpha, or InvertedLuminosity). The + * default value is Alpha. + */ + MaskType maskType = MaskType::Alpha; + + /** + * A reference to a composition (e.g., "#compositionId") used as the layer content. + */ + std::string composition = {}; + + /** + * The vector elements contained in this layer (shapes, painters, modifiers, etc.). + */ + std::vector> contents = {}; + + /** + * The layer styles applied to this layer (drop shadow, inner shadow, etc.). + */ + std::vector> styles = {}; + + /** + * The filters applied to this layer (blur, color matrix, etc.). + */ + std::vector> filters = {}; + + /** + * The child layers contained in this layer. + */ + std::vector> children = {}; + + /** + * Custom data from SVG data-* attributes. The keys are stored without the "data-" prefix. + */ + std::unordered_map customData = {}; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/LayerFilter.h b/pagx/include/pagx/model/LayerFilter.h new file mode 100644 index 0000000000..5156108544 --- /dev/null +++ b/pagx/include/pagx/model/LayerFilter.h @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/NodeType.h" + +namespace pagx { + +/** + * Layer filter types. + */ +enum class LayerFilterType { + BlurFilter, + DropShadowFilter, + InnerShadowFilter, + BlendFilter, + ColorMatrixFilter +}; + +/** + * Returns the string name of a layer filter type. + */ +const char* LayerFilterTypeName(LayerFilterType type); + +/** + * Base class for layer filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, ColorMatrixFilter). + */ +class LayerFilter { + public: + virtual ~LayerFilter() = default; + + /** + * Returns the layer filter type of this layer filter. + */ + virtual LayerFilterType layerFilterType() const = 0; + + /** + * Returns the unified node type of this layer filter. + */ + virtual NodeType type() const = 0; + + protected: + LayerFilter() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/LayerStyle.h b/pagx/include/pagx/model/LayerStyle.h new file mode 100644 index 0000000000..3c7fa41999 --- /dev/null +++ b/pagx/include/pagx/model/LayerStyle.h @@ -0,0 +1,60 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/NodeType.h" + +namespace pagx { + +/** + * Layer style types. + */ +enum class LayerStyleType { + DropShadowStyle, + InnerShadowStyle, + BackgroundBlurStyle +}; + +/** + * Returns the string name of a layer style type. + */ +const char* LayerStyleTypeName(LayerStyleType type); + +/** + * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle). + */ +class LayerStyle { + public: + virtual ~LayerStyle() = default; + + /** + * Returns the layer style type of this layer style. + */ + virtual LayerStyleType layerStyleType() const = 0; + + /** + * Returns the unified node type of this layer style. + */ + virtual NodeType type() const = 0; + + protected: + LayerStyle() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/LinearGradient.h b/pagx/include/pagx/model/LinearGradient.h new file mode 100644 index 0000000000..02707f03b2 --- /dev/null +++ b/pagx/include/pagx/model/LinearGradient.h @@ -0,0 +1,57 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/ColorStop.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * A linear gradient. + */ +class LinearGradient : public ColorSource { + public: + std::string id = {}; + Point startPoint = {}; + Point endPoint = {}; + Matrix matrix = {}; + std::vector colorStops = {}; + + ColorSourceType colorSourceType() const override { + return ColorSourceType::LinearGradient; + } + + ResourceType resourceType() const override { + return ResourceType::LinearGradient; + } + + const std::string& resourceId() const override { + return id; + } + + NodeType type() const override { + return NodeType::LinearGradient; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/MergePath.h b/pagx/include/pagx/model/MergePath.h new file mode 100644 index 0000000000..2a879b85ed --- /dev/null +++ b/pagx/include/pagx/model/MergePath.h @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Element.h" +#include "pagx/model/types/MergePathMode.h" + +namespace pagx { + +/** + * MergePath is a path modifier that merges multiple paths using boolean operations. It can append, + * add, subtract, intersect, or exclude paths from each other. + */ +class MergePath : public Element { + public: + /** + * The merge mode that determines how paths are combined. The default value is Append. + */ + MergePathMode mode = MergePathMode::Append; + + ElementType elementType() const override { + return ElementType::MergePath; + } + + NodeType type() const override { + return NodeType::MergePath; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Model.h b/pagx/include/pagx/model/Model.h new file mode 100644 index 0000000000..345e27043b --- /dev/null +++ b/pagx/include/pagx/model/Model.h @@ -0,0 +1,88 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// Basic types and enums +#include "pagx/model/types/Enums.h" +#include "pagx/model/types/Types.h" + +// Unified node type +#include "pagx/model/NodeType.h" + +// Base classes +#include "pagx/model/ColorSource.h" +#include "pagx/model/Element.h" +#include "pagx/model/LayerFilter.h" +#include "pagx/model/LayerStyle.h" +#include "pagx/model/Resource.h" + +// Color sources +#include "pagx/model/ColorStop.h" +#include "pagx/model/ConicGradient.h" +#include "pagx/model/DiamondGradient.h" +#include "pagx/model/ImagePattern.h" +#include "pagx/model/LinearGradient.h" +#include "pagx/model/RadialGradient.h" +#include "pagx/model/SolidColor.h" + +// Vector elements - shapes +#include "pagx/model/Ellipse.h" +#include "pagx/model/Path.h" +#include "pagx/model/Polystar.h" +#include "pagx/model/Rectangle.h" +#include "pagx/model/TextSpan.h" + +// Vector elements - painters +#include "pagx/model/Fill.h" +#include "pagx/model/Stroke.h" + +// Vector elements - path modifiers +#include "pagx/model/MergePath.h" +#include "pagx/model/RoundCorner.h" +#include "pagx/model/TrimPath.h" + +// Vector elements - text modifiers +#include "pagx/model/RangeSelector.h" +#include "pagx/model/TextLayout.h" +#include "pagx/model/TextModifier.h" +#include "pagx/model/TextPath.h" + +// Vector elements - containers +#include "pagx/model/Group.h" +#include "pagx/model/Repeater.h" + +// Layer styles +#include "pagx/model/BackgroundBlurStyle.h" +#include "pagx/model/DropShadowStyle.h" +#include "pagx/model/InnerShadowStyle.h" + +// Layer filters +#include "pagx/model/BlendFilter.h" +#include "pagx/model/BlurFilter.h" +#include "pagx/model/ColorMatrixFilter.h" +#include "pagx/model/DropShadowFilter.h" +#include "pagx/model/InnerShadowFilter.h" + +// Resources +#include "pagx/model/Composition.h" +#include "pagx/model/Image.h" +#include "pagx/model/PathDataResource.h" + +// Layer +#include "pagx/model/Layer.h" diff --git a/pagx/include/pagx/model/NodeType.h b/pagx/include/pagx/model/NodeType.h new file mode 100644 index 0000000000..25b1024dc6 --- /dev/null +++ b/pagx/include/pagx/model/NodeType.h @@ -0,0 +1,91 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * NodeType enumerates all types of nodes in a PAGX document. + * This unified type system is used for type checking across different categories of nodes. + */ +enum class NodeType { + // Color sources + SolidColor, + LinearGradient, + RadialGradient, + ConicGradient, + DiamondGradient, + ImagePattern, + ColorStop, + + // Geometry elements + Rectangle, + Ellipse, + Polystar, + Path, + TextSpan, + + // Painters + Fill, + Stroke, + + // Shape modifiers + TrimPath, + RoundCorner, + MergePath, + + // Text modifiers + TextModifier, + TextPath, + TextLayout, + RangeSelector, + + // Repeater + Repeater, + + // Container + Group, + + // Layer styles + DropShadowStyle, + InnerShadowStyle, + BackgroundBlurStyle, + + // Layer filters + BlurFilter, + DropShadowFilter, + InnerShadowFilter, + BlendFilter, + ColorMatrixFilter, + + // Resources + Image, + PathData, + Composition, + + // Layer + Layer +}; + +/** + * Returns the string name of a node type. + */ +const char* NodeTypeName(NodeType type); + +} // namespace pagx diff --git a/pagx/include/pagx/model/Path.h b/pagx/include/pagx/model/Path.h new file mode 100644 index 0000000000..7d68f7ab53 --- /dev/null +++ b/pagx/include/pagx/model/Path.h @@ -0,0 +1,51 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/PathData.h" +#include "pagx/model/Element.h" + +namespace pagx { + +/** + * Path represents a freeform shape defined by a PathData containing vertices, in-tangents, and + * out-tangents. + */ +class Path : public Element { + public: + /** + * The path data containing vertices and control points. + */ + PathData data = {}; + + /** + * Whether the path direction is reversed. The default value is false. + */ + bool reversed = false; + + ElementType elementType() const override { + return ElementType::Path; + } + + NodeType type() const override { + return NodeType::Path; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/PathData.h b/pagx/include/pagx/model/PathData.h new file mode 100644 index 0000000000..d2d8395666 --- /dev/null +++ b/pagx/include/pagx/model/PathData.h @@ -0,0 +1,176 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Path command types. + */ +enum class PathVerb : uint8_t { + Move, // 1 point: destination + Line, // 1 point: end point + Quad, // 2 points: control point, end point + Cubic, // 3 points: control point 1, control point 2, end point + Close // 0 points +}; + +/** + * PathData stores path commands in a format optimized for fast iteration + * and serialization. Unlike tgfx::Path, it exposes raw data arrays directly. + */ +class PathData { + public: + PathData() = default; + + /** + * Creates a PathData from an SVG path data string (d attribute). + */ + static PathData FromSVGString(const std::string& d); + + /** + * Starts a new contour at the specified point. + */ + void moveTo(float x, float y); + + /** + * Adds a line from the current point to the specified point. + */ + void lineTo(float x, float y); + + /** + * Adds a quadratic Bezier curve from the current point. + * @param cx control point x + * @param cy control point y + * @param x end point x + * @param y end point y + */ + void quadTo(float cx, float cy, float x, float y); + + /** + * Adds a cubic Bezier curve from the current point. + * @param c1x first control point x + * @param c1y first control point y + * @param c2x second control point x + * @param c2y second control point y + * @param x end point x + * @param y end point y + */ + void cubicTo(float c1x, float c1y, float c2x, float c2y, float x, float y); + + /** + * Closes the current contour by connecting to the starting point. + */ + void close(); + + /** + * Adds a rectangle to the path. + */ + void addRect(const Rect& rect); + + /** + * Adds an oval inscribed in the specified rectangle. + */ + void addOval(const Rect& rect); + + /** + * Adds a rounded rectangle to the path. + */ + void addRoundRect(const Rect& rect, float radiusX, float radiusY); + + /** + * Returns the array of path commands. + */ + const std::vector& verbs() const { + return _verbs; + } + + /** + * Returns the array of point coordinates. + * Points are stored as [x0, y0, x1, y1, ...]. + */ + const std::vector& points() const { + return _points; + } + + /** + * Returns the number of point coordinates. + */ + size_t countPoints() const { + return _points.size() / 2; + } + + /** + * Iterates over all path commands with a visitor function. + * The visitor receives the verb and a pointer to the point data. + */ + template + void forEach(Visitor&& visitor) const { + size_t pointIndex = 0; + for (auto verb : _verbs) { + const float* pts = _points.data() + pointIndex; + visitor(verb, pts); + pointIndex += PointsPerVerb(verb) * 2; + } + } + + /** + * Converts the path to an SVG path data string. + */ + std::string toSVGString() const; + + /** + * Returns the bounding rectangle of the path. + */ + Rect getBounds() const; + + /** + * Returns true if the path contains no commands. + */ + bool isEmpty() const { + return _verbs.empty(); + } + + /** + * Clears all path data. + */ + void clear(); + + /** + * Transforms all points in the path by the given matrix. + */ + void transform(const Matrix& matrix); + + /** + * Returns the number of points used by the given verb. + */ + static int PointsPerVerb(PathVerb verb); + + private: + std::vector _verbs = {}; + std::vector _points = {}; + mutable Rect _cachedBounds = {}; + mutable bool _boundsDirty = true; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/PathDataResource.h b/pagx/include/pagx/model/PathDataResource.h new file mode 100644 index 0000000000..4b675bdf39 --- /dev/null +++ b/pagx/include/pagx/model/PathDataResource.h @@ -0,0 +1,47 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/Resource.h" + +namespace pagx { + +/** + * PathData resource - stores reusable path data. + */ +class PathDataResource : public Resource { + public: + std::string id = {}; + std::string data = {}; // SVG path data string + + NodeType type() const override { + return NodeType::PathData; + } + + ResourceType resourceType() const override { + return ResourceType::PathData; + } + + const std::string& resourceId() const override { + return id; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Polystar.h b/pagx/include/pagx/model/Polystar.h new file mode 100644 index 0000000000..100ef0a1e8 --- /dev/null +++ b/pagx/include/pagx/model/Polystar.h @@ -0,0 +1,88 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Element.h" +#include "pagx/model/types/PolystarType.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Polystar represents a polygon or star shape with configurable points, radii, and roundness. + */ +class Polystar : public Element { + public: + /** + * The center point of the polystar. + */ + Point center = {}; + + /** + * The type of polystar shape, either Star or Polygon. The default value is Star. + */ + PolystarType polystarType = PolystarType::Star; + + /** + * The number of points in the polystar. The default value is 5. + */ + float pointCount = 5; + + /** + * The outer radius of the polystar. The default value is 100. + */ + float outerRadius = 100; + + /** + * The inner radius of the polystar. Only applies when polystarType is Star. The default value + * is 50. + */ + float innerRadius = 50; + + /** + * The rotation angle in degrees. The default value is 0. + */ + float rotation = 0; + + /** + * The roundness of the outer points, ranging from 0 to 100. The default value is 0. + */ + float outerRoundness = 0; + + /** + * The roundness of the inner points, ranging from 0 to 100. Only applies when polystarType is + * Star. The default value is 0. + */ + float innerRoundness = 0; + + /** + * Whether the path direction is reversed. The default value is false. + */ + bool reversed = false; + + ElementType elementType() const override { + return ElementType::Polystar; + } + + NodeType type() const override { + return NodeType::Polystar; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/RadialGradient.h b/pagx/include/pagx/model/RadialGradient.h new file mode 100644 index 0000000000..6e71881136 --- /dev/null +++ b/pagx/include/pagx/model/RadialGradient.h @@ -0,0 +1,57 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/ColorStop.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * A radial gradient. + */ +class RadialGradient : public ColorSource { + public: + std::string id = {}; + Point center = {}; + float radius = 0; + Matrix matrix = {}; + std::vector colorStops = {}; + + ColorSourceType colorSourceType() const override { + return ColorSourceType::RadialGradient; + } + + ResourceType resourceType() const override { + return ResourceType::RadialGradient; + } + + const std::string& resourceId() const override { + return id; + } + + NodeType type() const override { + return NodeType::RadialGradient; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/RangeSelector.h b/pagx/include/pagx/model/RangeSelector.h new file mode 100644 index 0000000000..ce60587625 --- /dev/null +++ b/pagx/include/pagx/model/RangeSelector.h @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/types/SelectorMode.h" +#include "pagx/model/types/SelectorShape.h" +#include "pagx/model/types/SelectorUnit.h" + +namespace pagx { + +/** + * Range selector for text modifier. + */ +class RangeSelector { + public: + float start = 0; + float end = 1; + float offset = 0; + SelectorUnit unit = SelectorUnit::Percentage; + SelectorShape shape = SelectorShape::Square; + float easeIn = 0; + float easeOut = 0; + SelectorMode mode = SelectorMode::Add; + float weight = 1; + bool randomizeOrder = false; + int randomSeed = 0; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Rectangle.h b/pagx/include/pagx/model/Rectangle.h new file mode 100644 index 0000000000..463e25ac6a --- /dev/null +++ b/pagx/include/pagx/model/Rectangle.h @@ -0,0 +1,60 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Element.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Rectangle represents a rectangle shape with optional rounded corners. + */ +class Rectangle : public Element { + public: + /** + * The center point of the rectangle. + */ + Point center = {}; + + /** + * The size of the rectangle. The default value is {100, 100}. + */ + Size size = {100, 100}; + + /** + * The corner roundness of the rectangle, ranging from 0 to 100. The default value is 0. + */ + float roundness = 0; + + /** + * Whether the path direction is reversed. The default value is false. + */ + bool reversed = false; + + ElementType elementType() const override { + return ElementType::Rectangle; + } + + NodeType type() const override { + return NodeType::Rectangle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Repeater.h b/pagx/include/pagx/model/Repeater.h new file mode 100644 index 0000000000..a484ca73cb --- /dev/null +++ b/pagx/include/pagx/model/Repeater.h @@ -0,0 +1,89 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Element.h" +#include "pagx/model/types/RepeaterOrder.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * Repeater is a modifier that creates multiple copies of preceding elements with progressive + * transformations. Each copy can have an incremental offset in position, rotation, scale, and + * opacity. + */ +class Repeater : public Element { + public: + /** + * The number of copies to create. The default value is 3. + */ + float copies = 3; + + /** + * The offset applied to the copy index, allowing fractional copies. The default value is 0. + */ + float offset = 0; + + /** + * The stacking order of copies (BelowOriginal or AboveOriginal). The default value is + * BelowOriginal. + */ + RepeaterOrder order = RepeaterOrder::BelowOriginal; + + /** + * The anchor point for transformations. + */ + Point anchorPoint = {}; + + /** + * The position offset applied between each copy. The default value is {100, 100}. + */ + Point position = {100, 100}; + + /** + * The rotation angle in degrees applied between each copy. The default value is 0. + */ + float rotation = 0; + + /** + * The scale factor applied between each copy. The default value is {1, 1}. + */ + Point scale = {1, 1}; + + /** + * The starting opacity for the first copy, ranging from 0 to 1. The default value is 1. + */ + float startAlpha = 1; + + /** + * The ending opacity for the last copy, ranging from 0 to 1. The default value is 1. + */ + float endAlpha = 1; + + ElementType elementType() const override { + return ElementType::Repeater; + } + + NodeType type() const override { + return NodeType::Repeater; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Resource.h b/pagx/include/pagx/model/Resource.h new file mode 100644 index 0000000000..f4223feb1f --- /dev/null +++ b/pagx/include/pagx/model/Resource.h @@ -0,0 +1,100 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/NodeType.h" + +namespace pagx { + +/** + * ResourceType enumerates all types of resources that can be stored in a PAGX document. + */ +enum class ResourceType { + /** + * An image resource. + */ + Image, + /** + * A reusable path data resource. + */ + PathData, + /** + * A composition resource containing layers. + */ + Composition, + /** + * A solid color resource. + */ + SolidColor, + /** + * A linear gradient resource. + */ + LinearGradient, + /** + * A radial gradient resource. + */ + RadialGradient, + /** + * A conic gradient resource. + */ + ConicGradient, + /** + * A diamond gradient resource. + */ + DiamondGradient, + /** + * An image pattern resource. + */ + ImagePattern +}; + +/** + * Returns the string name of a resource type. + */ +const char* ResourceTypeName(ResourceType type); + +/** + * Resource is the base class for all resources in a PAGX document. Resources are reusable items + * that can be referenced by ID (e.g., "#imageId", "#gradientId"). + */ +class Resource { + public: + virtual ~Resource() = default; + + /** + * Returns the resource type of this resource. + */ + virtual ResourceType resourceType() const = 0; + + /** + * Returns the unified node type of this resource. + */ + virtual NodeType type() const = 0; + + /** + * Returns the unique identifier of this resource. + */ + virtual const std::string& resourceId() const = 0; + + protected: + Resource() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/RoundCorner.h b/pagx/include/pagx/model/RoundCorner.h new file mode 100644 index 0000000000..494bf28743 --- /dev/null +++ b/pagx/include/pagx/model/RoundCorner.h @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Element.h" + +namespace pagx { + +/** + * RoundCorner is a path modifier that rounds the corners of shapes by adding smooth curves at + * sharp vertices. + */ +class RoundCorner : public Element { + public: + /** + * The radius of the rounded corners in pixels. The default value is 10. + */ + float radius = 10; + + ElementType elementType() const override { + return ElementType::RoundCorner; + } + + NodeType type() const override { + return NodeType::RoundCorner; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/SolidColor.h b/pagx/include/pagx/model/SolidColor.h new file mode 100644 index 0000000000..d010784c6d --- /dev/null +++ b/pagx/include/pagx/model/SolidColor.h @@ -0,0 +1,52 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * A solid color. + */ +class SolidColor : public ColorSource { + public: + std::string id = {}; + Color color = {}; + + ColorSourceType colorSourceType() const override { + return ColorSourceType::SolidColor; + } + + ResourceType resourceType() const override { + return ResourceType::SolidColor; + } + + const std::string& resourceId() const override { + return id; + } + + NodeType type() const override { + return NodeType::SolidColor; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Stroke.h b/pagx/include/pagx/model/Stroke.h new file mode 100644 index 0000000000..5750909d65 --- /dev/null +++ b/pagx/include/pagx/model/Stroke.h @@ -0,0 +1,113 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/Element.h" +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/LineCap.h" +#include "pagx/model/types/LineJoin.h" +#include "pagx/model/types/Placement.h" +#include "pagx/model/types/StrokeAlign.h" + +namespace pagx { + +/** + * Stroke represents a stroke painter that outlines shapes with a solid color, gradient, or + * pattern. It supports various line cap styles, join styles, dash patterns, and stroke alignment. + */ +class Stroke : public Element { + public: + /** + * The stroke color as a string. Can be a hex color (e.g., "#FF0000"), a reference to a color + * source (e.g., "#gradientId"), or empty if colorSource is used. + */ + std::string color = {}; + + /** + * An inline color source node (SolidColor, LinearGradient, etc.) for complex strokes. If + * provided, this takes precedence over the color string. + */ + std::unique_ptr colorSource = nullptr; + + /** + * The stroke width in pixels. The default value is 1. + */ + float width = 1; + + /** + * The opacity of the stroke, ranging from 0 (transparent) to 1 (opaque). The default value is 1. + */ + float alpha = 1; + + /** + * The blend mode used when compositing the stroke. The default value is Normal. + */ + BlendMode blendMode = BlendMode::Normal; + + /** + * The line cap style at the endpoints of open paths. The default value is Butt. + */ + LineCap cap = LineCap::Butt; + + /** + * The line join style at path corners. The default value is Miter. + */ + LineJoin join = LineJoin::Miter; + + /** + * The limit for miter joins before they are beveled. The default value is 4. + */ + float miterLimit = 4; + + /** + * The dash pattern as an array of dash and gap lengths. An empty array means a solid line. + */ + std::vector dashes = {}; + + /** + * The offset into the dash pattern. The default value is 0. + */ + float dashOffset = 0; + + /** + * The alignment of the stroke relative to the path (Center, Inner, or Outer). The default value + * is Center. + */ + StrokeAlign align = StrokeAlign::Center; + + /** + * The placement of the stroke relative to fills (Background or Foreground). The default value is + * Background. + */ + Placement placement = Placement::Background; + + ElementType elementType() const override { + return ElementType::Stroke; + } + + NodeType type() const override { + return NodeType::Stroke; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/TextLayout.h b/pagx/include/pagx/model/TextLayout.h new file mode 100644 index 0000000000..2dfdfb3578 --- /dev/null +++ b/pagx/include/pagx/model/TextLayout.h @@ -0,0 +1,79 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Element.h" +#include "pagx/model/types/Overflow.h" +#include "pagx/model/types/TextAlign.h" +#include "pagx/model/types/VerticalAlign.h" + +namespace pagx { + +/** + * TextLayout is a text animator that controls text layout within a bounding box. It provides + * options for text alignment, line height, indentation, and overflow handling. + */ +class TextLayout : public Element { + public: + /** + * The width of the text box in pixels. A value of 0 means auto-width. The default value is 0. + */ + float width = 0; + + /** + * The height of the text box in pixels. A value of 0 means auto-height. The default value is 0. + */ + float height = 0; + + /** + * The horizontal text alignment (Left, Center, Right, or Justify). The default value is Left. + */ + TextAlign textAlign = TextAlign::Left; + + /** + * The vertical text alignment (Top, Middle, or Bottom). The default value is Top. + */ + VerticalAlign verticalAlign = VerticalAlign::Top; + + /** + * The line height multiplier. The default value is 1.2. + */ + float lineHeight = 1.2f; + + /** + * The first-line indent in pixels. The default value is 0. + */ + float indent = 0; + + /** + * The overflow behavior when text exceeds the bounding box (Clip, Visible, or Scroll). The + * default value is Clip. + */ + Overflow overflow = Overflow::Clip; + + ElementType elementType() const override { + return ElementType::TextLayout; + } + + NodeType type() const override { + return NodeType::TextLayout; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/TextModifier.h b/pagx/include/pagx/model/TextModifier.h new file mode 100644 index 0000000000..2e18161f14 --- /dev/null +++ b/pagx/include/pagx/model/TextModifier.h @@ -0,0 +1,101 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/Element.h" +#include "pagx/model/RangeSelector.h" +#include "pagx/model/types/Types.h" + +namespace pagx { + +/** + * TextModifier is a text animator that modifies text appearance with range-based transformations. + * It applies transformations like position, rotation, scale, and color changes to selected ranges + * of characters. + */ +class TextModifier : public Element { + public: + /** + * The anchor point for transformations. + */ + Point anchorPoint = {}; + + /** + * The position offset applied to selected characters. + */ + Point position = {}; + + /** + * The rotation angle in degrees applied to selected characters. The default value is 0. + */ + float rotation = 0; + + /** + * The scale factor applied to selected characters. The default value is {1, 1}. + */ + Point scale = {1, 1}; + + /** + * The skew angle in degrees applied to selected characters. The default value is 0. + */ + float skew = 0; + + /** + * The axis angle in degrees for the skew transformation. The default value is 0. + */ + float skewAxis = 0; + + /** + * The opacity applied to selected characters, ranging from 0 to 1. The default value is 1. + */ + float alpha = 1; + + /** + * The fill color override for selected characters as a hex color string. + */ + std::string fillColor = {}; + + /** + * The stroke color override for selected characters as a hex color string. + */ + std::string strokeColor = {}; + + /** + * The stroke width override for selected characters. A value of -1 means no override. The + * default value is -1. + */ + float strokeWidth = -1; + + /** + * The range selectors that determine which characters are affected by this modifier. + */ + std::vector rangeSelectors = {}; + + ElementType elementType() const override { + return ElementType::TextModifier; + } + + NodeType type() const override { + return NodeType::TextModifier; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/TextPath.h b/pagx/include/pagx/model/TextPath.h new file mode 100644 index 0000000000..4bea12846c --- /dev/null +++ b/pagx/include/pagx/model/TextPath.h @@ -0,0 +1,78 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/Element.h" +#include "pagx/model/types/TextPathAlign.h" + +namespace pagx { + +/** + * TextPath is a text animator that places text along a path. It allows text to follow the contour + * of a referenced path shape. + */ +class TextPath : public Element { + public: + /** + * A reference to the path shape (e.g., "#pathId") that the text follows. + */ + std::string path = {}; + + /** + * The alignment of text along the path (Start, Center, or Justify). The default value is Start. + */ + TextPathAlign pathAlign = TextPathAlign::Start; + + /** + * The margin from the start of the path in pixels. The default value is 0. + */ + float firstMargin = 0; + + /** + * The margin from the end of the path in pixels. The default value is 0. + */ + float lastMargin = 0; + + /** + * Whether characters are rotated to be perpendicular to the path. The default value is true. + */ + bool perpendicularToPath = true; + + /** + * Whether to reverse the direction of the path. The default value is false. + */ + bool reversed = false; + + /** + * Whether to force text alignment to the path even when it exceeds the path length. The default + * value is false. + */ + bool forceAlignment = false; + + ElementType elementType() const override { + return ElementType::TextPath; + } + + NodeType type() const override { + return NodeType::TextPath; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/TextSpan.h b/pagx/include/pagx/model/TextSpan.h new file mode 100644 index 0000000000..c1e290fc5a --- /dev/null +++ b/pagx/include/pagx/model/TextSpan.h @@ -0,0 +1,87 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/Element.h" +#include "pagx/model/types/FontStyle.h" + +namespace pagx { + +/** + * TextSpan represents a text span that generates glyph paths for rendering. It defines the text + * content, font properties, and positioning within a shape layer. + */ +class TextSpan : public Element { + public: + /** + * The x-coordinate of the text baseline starting point. The default value is 0. + */ + float x = 0; + + /** + * The y-coordinate of the text baseline starting point. The default value is 0. + */ + float y = 0; + + /** + * The font family name. + */ + std::string font = {}; + + /** + * The font size in pixels. The default value is 12. + */ + float fontSize = 12; + + /** + * The font weight, ranging from 100 to 900. The default value is 400 (normal). + */ + int fontWeight = 400; + + /** + * The font style (Normal, Italic, or Oblique). The default value is Normal. + */ + FontStyle fontStyle = FontStyle::Normal; + + /** + * The tracking value that adjusts spacing between characters. The default value is 0. + */ + float tracking = 0; + + /** + * The baseline shift in pixels, positive values shift the text up. The default value is 0. + */ + float baselineShift = 0; + + /** + * The text content to render. + */ + std::string text = {}; + + ElementType elementType() const override { + return ElementType::TextSpan; + } + + NodeType type() const override { + return NodeType::TextSpan; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/TrimPath.h b/pagx/include/pagx/model/TrimPath.h new file mode 100644 index 0000000000..9f30bf4a66 --- /dev/null +++ b/pagx/include/pagx/model/TrimPath.h @@ -0,0 +1,66 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/model/Element.h" +#include "pagx/model/types/TrimType.h" + +namespace pagx { + +/** + * TrimPath is a path modifier that trims paths to a specified range. It can be used to animate + * path drawing or reveal effects by adjusting the start and end values. + */ +class TrimPath : public Element { + public: + /** + * The starting point of the trim as a percentage of the path length, ranging from 0 to 1. The + * default value is 0. + */ + float start = 0; + + /** + * The ending point of the trim as a percentage of the path length, ranging from 0 to 1. The + * default value is 1. + */ + float end = 1; + + /** + * The offset to shift the trim range along the path, where 1 represents a full path length. The + * default value is 0. + */ + float offset = 0; + + /** + * The trim type that determines how multiple paths are trimmed. Separate trims each path + * individually, while Simultaneous trims all paths as one continuous path. The default value is + * Separate. + */ + TrimType trimType = TrimType::Separate; + + ElementType elementType() const override { + return ElementType::TrimPath; + } + + NodeType type() const override { + return NodeType::TrimPath; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/BlendMode.h b/pagx/include/pagx/model/types/BlendMode.h new file mode 100644 index 0000000000..10b90cbdcb --- /dev/null +++ b/pagx/include/pagx/model/types/BlendMode.h @@ -0,0 +1,52 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Blend modes for compositing. + */ +enum class BlendMode { + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, + PlusLighter, + PlusDarker +}; + +std::string BlendModeToString(BlendMode mode); +BlendMode BlendModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/Color.h b/pagx/include/pagx/model/types/Color.h new file mode 100644 index 0000000000..04eaf24c27 --- /dev/null +++ b/pagx/include/pagx/model/types/Color.h @@ -0,0 +1,170 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace pagx { + +namespace detail { +inline int ParseHexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return 0; +} +} // namespace detail + +/** + * An RGBA color with floating-point components in [0, 1]. + */ +struct Color { + float red = 0; + float green = 0; + float blue = 0; + float alpha = 1; + + /** + * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). + */ + static Color FromHex(uint32_t hex, bool hasAlpha = false) { + Color color = {}; + if (hasAlpha) { + color.red = static_cast((hex >> 24) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.blue = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.alpha = static_cast(hex & 0xFF) / 255.0f; + } else { + color.red = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.blue = static_cast(hex & 0xFF) / 255.0f; + color.alpha = 1.0f; + } + return color; + } + + /** + * Returns a Color from RGBA components in [0, 1]. + */ + static Color FromRGBA(float r, float g, float b, float a = 1) { + return {r, g, b, a}; + } + + /** + * Parses a color string. Supports: + * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" + * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" + * Returns black if parsing fails. + */ + static Color Parse(const std::string& str) { + if (str.empty()) { + return {}; + } + if (str[0] == '#') { + auto hex = str.substr(1); + if (hex.size() == 3) { + int r = detail::ParseHexDigit(hex[0]); + int g = detail::ParseHexDigit(hex[1]); + int b = detail::ParseHexDigit(hex[2]); + return Color::FromRGBA(static_cast(r * 17) / 255.0f, + static_cast(g * 17) / 255.0f, + static_cast(b * 17) / 255.0f, 1.0f); + } + if (hex.size() == 6) { + int r = detail::ParseHexDigit(hex[0]) * 16 + detail::ParseHexDigit(hex[1]); + int g = detail::ParseHexDigit(hex[2]) * 16 + detail::ParseHexDigit(hex[3]); + int b = detail::ParseHexDigit(hex[4]) * 16 + detail::ParseHexDigit(hex[5]); + return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, + static_cast(b) / 255.0f, 1.0f); + } + if (hex.size() == 8) { + int r = detail::ParseHexDigit(hex[0]) * 16 + detail::ParseHexDigit(hex[1]); + int g = detail::ParseHexDigit(hex[2]) * 16 + detail::ParseHexDigit(hex[3]); + int b = detail::ParseHexDigit(hex[4]) * 16 + detail::ParseHexDigit(hex[5]); + int a = detail::ParseHexDigit(hex[6]) * 16 + detail::ParseHexDigit(hex[7]); + return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, + static_cast(b) / 255.0f, static_cast(a) / 255.0f); + } + } + if (str.substr(0, 4) == "rgb(" || str.substr(0, 5) == "rgba(") { + auto start = str.find('('); + auto end = str.find(')'); + if (start != std::string::npos && end != std::string::npos) { + auto values = str.substr(start + 1, end - start - 1); + std::istringstream iss(values); + std::string token = {}; + std::vector components = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + components.push_back(std::stof(trimmed)); + } + if (components.size() >= 3) { + float r = components[0] / 255.0f; + float g = components[1] / 255.0f; + float b = components[2] / 255.0f; + float a = components.size() >= 4 ? components[3] : 1.0f; + return Color::FromRGBA(r, g, b, a); + } + } + } + return {}; + } + + /** + * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". + */ + std::string toHexString(bool includeAlpha = false) const { + auto toHex = [](float v) { + int i = static_cast(std::round(v * 255.0f)); + i = std::max(0, std::min(255, i)); + char buf[3] = {}; + snprintf(buf, sizeof(buf), "%02X", i); + return std::string(buf); + }; + std::string result = "#" + toHex(red) + toHex(green) + toHex(blue); + if (includeAlpha && alpha < 1.0f) { + result += toHex(alpha); + } + return result; + } + + bool operator==(const Color& other) const { + return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; + } + + bool operator!=(const Color& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/Enums.h b/pagx/include/pagx/model/types/Enums.h new file mode 100644 index 0000000000..acc3ffed7e --- /dev/null +++ b/pagx/include/pagx/model/types/Enums.h @@ -0,0 +1,54 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// Layer related +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/MaskType.h" + +// Painter related +#include "pagx/model/types/FillRule.h" +#include "pagx/model/types/LineCap.h" +#include "pagx/model/types/LineJoin.h" +#include "pagx/model/types/Placement.h" +#include "pagx/model/types/StrokeAlign.h" + +// Color source related +#include "pagx/model/types/SamplingMode.h" +#include "pagx/model/types/TileMode.h" + +// Geometry related +#include "pagx/model/types/PolystarType.h" + +// Path modifier related +#include "pagx/model/types/MergePathMode.h" +#include "pagx/model/types/TrimType.h" + +// Text modifier related +#include "pagx/model/types/FontStyle.h" +#include "pagx/model/types/Overflow.h" +#include "pagx/model/types/SelectorMode.h" +#include "pagx/model/types/SelectorShape.h" +#include "pagx/model/types/SelectorUnit.h" +#include "pagx/model/types/TextAlign.h" +#include "pagx/model/types/TextPathAlign.h" +#include "pagx/model/types/VerticalAlign.h" + +// Repeater related +#include "pagx/model/types/RepeaterOrder.h" diff --git a/pagx/include/pagx/model/types/FillRule.h b/pagx/include/pagx/model/types/FillRule.h new file mode 100644 index 0000000000..5ae1093101 --- /dev/null +++ b/pagx/include/pagx/model/types/FillRule.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Fill rules for paths. + */ +enum class FillRule { + Winding, + EvenOdd +}; + +std::string FillRuleToString(FillRule rule); +FillRule FillRuleFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/FontStyle.h b/pagx/include/pagx/model/types/FontStyle.h new file mode 100644 index 0000000000..59568b80ff --- /dev/null +++ b/pagx/include/pagx/model/types/FontStyle.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Font style. + */ +enum class FontStyle { + Normal, + Italic +}; + +std::string FontStyleToString(FontStyle style); +FontStyle FontStyleFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/LineCap.h b/pagx/include/pagx/model/types/LineCap.h new file mode 100644 index 0000000000..dfb779acca --- /dev/null +++ b/pagx/include/pagx/model/types/LineCap.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Line cap styles for strokes. + */ +enum class LineCap { + Butt, + Round, + Square +}; + +std::string LineCapToString(LineCap cap); +LineCap LineCapFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/LineJoin.h b/pagx/include/pagx/model/types/LineJoin.h new file mode 100644 index 0000000000..e6b12c6f57 --- /dev/null +++ b/pagx/include/pagx/model/types/LineJoin.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Line join styles for strokes. + */ +enum class LineJoin { + Miter, + Round, + Bevel +}; + +std::string LineJoinToString(LineJoin join); +LineJoin LineJoinFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/MaskType.h b/pagx/include/pagx/model/types/MaskType.h new file mode 100644 index 0000000000..f274bd9a7c --- /dev/null +++ b/pagx/include/pagx/model/types/MaskType.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Mask types for layer masking. + */ +enum class MaskType { + Alpha, + Luminance, + Contour +}; + +std::string MaskTypeToString(MaskType type); +MaskType MaskTypeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/Matrix.h b/pagx/include/pagx/model/types/Matrix.h new file mode 100644 index 0000000000..0f3fd49c39 --- /dev/null +++ b/pagx/include/pagx/model/types/Matrix.h @@ -0,0 +1,160 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include "pagx/model/types/Point.h" + +namespace pagx { + +/** + * A 2D affine transformation matrix. + * Matrix form: + * | a c tx | + * | b d ty | + * | 0 0 1 | + */ +struct Matrix { + float a = 1; // scaleX + float b = 0; // skewY + float c = 0; // skewX + float d = 1; // scaleY + float tx = 0; // transX + float ty = 0; // transY + + /** + * Returns the identity matrix. + */ + static Matrix Identity() { + return {}; + } + + /** + * Returns a translation matrix. + */ + static Matrix Translate(float x, float y) { + Matrix m = {}; + m.tx = x; + m.ty = y; + return m; + } + + /** + * Returns a scale matrix. + */ + static Matrix Scale(float sx, float sy) { + Matrix m = {}; + m.a = sx; + m.d = sy; + return m; + } + + /** + * Returns a rotation matrix (angle in degrees). + */ + static Matrix Rotate(float degrees) { + Matrix m = {}; + float radians = degrees * 3.14159265358979323846f / 180.0f; + float cosVal = std::cos(radians); + float sinVal = std::sin(radians); + m.a = cosVal; + m.b = sinVal; + m.c = -sinVal; + m.d = cosVal; + return m; + } + + /** + * Parses a matrix string "a,b,c,d,tx,ty". + */ + static Matrix Parse(const std::string& str) { + Matrix m = {}; + std::istringstream iss(str); + std::string token = {}; + std::vector values = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + if (!trimmed.empty()) { + values.push_back(std::stof(trimmed)); + } + } + if (values.size() >= 6) { + m.a = values[0]; + m.b = values[1]; + m.c = values[2]; + m.d = values[3]; + m.tx = values[4]; + m.ty = values[5]; + } + return m; + } + + /** + * Returns the matrix as a string "a,b,c,d,tx,ty". + */ + std::string toString() const { + std::ostringstream oss = {}; + oss << a << "," << b << "," << c << "," << d << "," << tx << "," << ty; + return oss.str(); + } + + /** + * Returns true if this is the identity matrix. + */ + bool isIdentity() const { + return a == 1 && b == 0 && c == 0 && d == 1 && tx == 0 && ty == 0; + } + + /** + * Concatenates this matrix with another. + */ + Matrix operator*(const Matrix& other) const { + Matrix result = {}; + result.a = a * other.a + c * other.b; + result.b = b * other.a + d * other.b; + result.c = a * other.c + c * other.d; + result.d = b * other.c + d * other.d; + result.tx = a * other.tx + c * other.ty + tx; + result.ty = b * other.tx + d * other.ty + ty; + return result; + } + + /** + * Transforms a point by this matrix. + */ + Point mapPoint(const Point& point) const { + return {a * point.x + c * point.y + tx, b * point.x + d * point.y + ty}; + } + + bool operator==(const Matrix& other) const { + return a == other.a && b == other.b && c == other.c && d == other.d && tx == other.tx && + ty == other.ty; + } + + bool operator!=(const Matrix& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/MergePathMode.h b/pagx/include/pagx/model/types/MergePathMode.h new file mode 100644 index 0000000000..94a17879ec --- /dev/null +++ b/pagx/include/pagx/model/types/MergePathMode.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Path merge modes (boolean operations). + */ +enum class MergePathMode { + Append, + Union, + Intersect, + Xor, + Difference +}; + +std::string MergePathModeToString(MergePathMode mode); +MergePathMode MergePathModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/Overflow.h b/pagx/include/pagx/model/types/Overflow.h new file mode 100644 index 0000000000..4a6868d063 --- /dev/null +++ b/pagx/include/pagx/model/types/Overflow.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Text overflow handling. + */ +enum class Overflow { + Clip, + Visible, + Ellipsis +}; + +std::string OverflowToString(Overflow overflow); +Overflow OverflowFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/Placement.h b/pagx/include/pagx/model/types/Placement.h new file mode 100644 index 0000000000..2cb3aaf32b --- /dev/null +++ b/pagx/include/pagx/model/types/Placement.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Placement of fill/stroke relative to child layers. + */ +enum class Placement { + Background, + Foreground +}; + +std::string PlacementToString(Placement placement); +Placement PlacementFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/Point.h b/pagx/include/pagx/model/types/Point.h new file mode 100644 index 0000000000..e4996dcf21 --- /dev/null +++ b/pagx/include/pagx/model/types/Point.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * A point with x and y coordinates. + */ +struct Point { + float x = 0; + float y = 0; + + bool operator==(const Point& other) const { + return x == other.x && y == other.y; + } + + bool operator!=(const Point& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/PolystarType.h b/pagx/include/pagx/model/types/PolystarType.h new file mode 100644 index 0000000000..5834dc05c2 --- /dev/null +++ b/pagx/include/pagx/model/types/PolystarType.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Polystar types. + */ +enum class PolystarType { + Polygon, + Star +}; + +std::string PolystarTypeToString(PolystarType type); +PolystarType PolystarTypeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/Rect.h b/pagx/include/pagx/model/types/Rect.h new file mode 100644 index 0000000000..5adea6206d --- /dev/null +++ b/pagx/include/pagx/model/types/Rect.h @@ -0,0 +1,79 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * A rectangle defined by position and size. + */ +struct Rect { + float x = 0; + float y = 0; + float width = 0; + float height = 0; + + /** + * Returns a Rect from left, top, right, bottom coordinates. + */ + static Rect MakeLTRB(float l, float t, float r, float b) { + return {l, t, r - l, b - t}; + } + + /** + * Returns a Rect from position and size. + */ + static Rect MakeXYWH(float x, float y, float w, float h) { + return {x, y, w, h}; + } + + float left() const { + return x; + } + + float top() const { + return y; + } + + float right() const { + return x + width; + } + + float bottom() const { + return y + height; + } + + bool isEmpty() const { + return width <= 0 || height <= 0; + } + + void setEmpty() { + x = y = width = height = 0; + } + + bool operator==(const Rect& other) const { + return x == other.x && y == other.y && width == other.width && height == other.height; + } + + bool operator!=(const Rect& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/RepeaterOrder.h b/pagx/include/pagx/model/types/RepeaterOrder.h new file mode 100644 index 0000000000..e98b5a3439 --- /dev/null +++ b/pagx/include/pagx/model/types/RepeaterOrder.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Repeater stacking order. + */ +enum class RepeaterOrder { + BelowOriginal, + AboveOriginal +}; + +std::string RepeaterOrderToString(RepeaterOrder order); +RepeaterOrder RepeaterOrderFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/SamplingMode.h b/pagx/include/pagx/model/types/SamplingMode.h new file mode 100644 index 0000000000..270a5160e5 --- /dev/null +++ b/pagx/include/pagx/model/types/SamplingMode.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Sampling modes for images. + */ +enum class SamplingMode { + Nearest, + Linear, + Mipmap +}; + +std::string SamplingModeToString(SamplingMode mode); +SamplingMode SamplingModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/SelectorMode.h b/pagx/include/pagx/model/types/SelectorMode.h new file mode 100644 index 0000000000..c6794115ee --- /dev/null +++ b/pagx/include/pagx/model/types/SelectorMode.h @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Range selector combination mode. + */ +enum class SelectorMode { + Add, + Subtract, + Intersect, + Min, + Max, + Difference +}; + +std::string SelectorModeToString(SelectorMode mode); +SelectorMode SelectorModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/SelectorShape.h b/pagx/include/pagx/model/types/SelectorShape.h new file mode 100644 index 0000000000..e61239f4e8 --- /dev/null +++ b/pagx/include/pagx/model/types/SelectorShape.h @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Range selector shape. + */ +enum class SelectorShape { + Square, + RampUp, + RampDown, + Triangle, + Round, + Smooth +}; + +std::string SelectorShapeToString(SelectorShape shape); +SelectorShape SelectorShapeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/SelectorUnit.h b/pagx/include/pagx/model/types/SelectorUnit.h new file mode 100644 index 0000000000..759da0682d --- /dev/null +++ b/pagx/include/pagx/model/types/SelectorUnit.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Range selector unit. + */ +enum class SelectorUnit { + Index, + Percentage +}; + +std::string SelectorUnitToString(SelectorUnit unit); +SelectorUnit SelectorUnitFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/Size.h b/pagx/include/pagx/model/types/Size.h new file mode 100644 index 0000000000..bb0ea3e110 --- /dev/null +++ b/pagx/include/pagx/model/types/Size.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * A size with width and height. + */ +struct Size { + float width = 0; + float height = 0; + + bool operator==(const Size& other) const { + return width == other.width && height == other.height; + } + + bool operator!=(const Size& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/StrokeAlign.h b/pagx/include/pagx/model/types/StrokeAlign.h new file mode 100644 index 0000000000..adccb2ed9f --- /dev/null +++ b/pagx/include/pagx/model/types/StrokeAlign.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Stroke alignment relative to path. + */ +enum class StrokeAlign { + Center, + Inside, + Outside +}; + +std::string StrokeAlignToString(StrokeAlign align); +StrokeAlign StrokeAlignFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/TextAlign.h b/pagx/include/pagx/model/types/TextAlign.h new file mode 100644 index 0000000000..88ba8c64b2 --- /dev/null +++ b/pagx/include/pagx/model/types/TextAlign.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Text horizontal alignment. + */ +enum class TextAlign { + Left, + Center, + Right, + Justify +}; + +std::string TextAlignToString(TextAlign align); +TextAlign TextAlignFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/TextPathAlign.h b/pagx/include/pagx/model/types/TextPathAlign.h new file mode 100644 index 0000000000..7957c8679b --- /dev/null +++ b/pagx/include/pagx/model/types/TextPathAlign.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Text path alignment. + */ +enum class TextPathAlign { + Start, + Center, + End +}; + +std::string TextPathAlignToString(TextPathAlign align); +TextPathAlign TextPathAlignFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/TileMode.h b/pagx/include/pagx/model/types/TileMode.h new file mode 100644 index 0000000000..758865336c --- /dev/null +++ b/pagx/include/pagx/model/types/TileMode.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Tile modes for patterns and gradients. + */ +enum class TileMode { + Clamp, + Repeat, + Mirror, + Decal +}; + +std::string TileModeToString(TileMode mode); +TileMode TileModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/TrimType.h b/pagx/include/pagx/model/types/TrimType.h new file mode 100644 index 0000000000..31b3aab19f --- /dev/null +++ b/pagx/include/pagx/model/types/TrimType.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Trim path types. + */ +enum class TrimType { + Separate, + Continuous +}; + +std::string TrimTypeToString(TrimType type); +TrimType TrimTypeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/Types.h b/pagx/include/pagx/model/types/Types.h new file mode 100644 index 0000000000..3c17baaadf --- /dev/null +++ b/pagx/include/pagx/model/types/Types.h @@ -0,0 +1,229 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace pagx { + +/** + * A point with x and y coordinates. + */ +struct Point { + float x = 0; + float y = 0; + + bool operator==(const Point& other) const { + return x == other.x && y == other.y; + } + + bool operator!=(const Point& other) const { + return !(*this == other); + } +}; + +/** + * A size with width and height. + */ +struct Size { + float width = 0; + float height = 0; + + bool operator==(const Size& other) const { + return width == other.width && height == other.height; + } + + bool operator!=(const Size& other) const { + return !(*this == other); + } +}; + +/** + * A rectangle defined by position and size. + */ +struct Rect { + float x = 0; + float y = 0; + float width = 0; + float height = 0; + + /** + * Returns a Rect from left, top, right, bottom coordinates. + */ + static Rect MakeLTRB(float l, float t, float r, float b) { + return {l, t, r - l, b - t}; + } + + /** + * Returns a Rect from position and size. + */ + static Rect MakeXYWH(float x, float y, float w, float h) { + return {x, y, w, h}; + } + + float left() const { + return x; + } + + float top() const { + return y; + } + + float right() const { + return x + width; + } + + float bottom() const { + return y + height; + } + + bool isEmpty() const { + return width <= 0 || height <= 0; + } + + void setEmpty() { + x = y = width = height = 0; + } + + bool operator==(const Rect& other) const { + return x == other.x && y == other.y && width == other.width && height == other.height; + } + + bool operator!=(const Rect& other) const { + return !(*this == other); + } +}; + +/** + * An RGBA color with floating-point components in [0, 1]. + */ +struct Color { + float red = 0; + float green = 0; + float blue = 0; + float alpha = 1; + + /** + * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). + */ + static Color FromHex(uint32_t hex, bool hasAlpha = false); + + /** + * Returns a Color from RGBA components in [0, 1]. + */ + static Color FromRGBA(float r, float g, float b, float a = 1); + + /** + * Parses a color string. Supports: + * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" + * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" + * Returns black if parsing fails. + */ + static Color Parse(const std::string& str); + + /** + * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". + */ + std::string toHexString(bool includeAlpha = false) const; + + bool operator==(const Color& other) const { + return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; + } + + bool operator!=(const Color& other) const { + return !(*this == other); + } +}; + +/** + * A 2D affine transformation matrix. + * Matrix form: + * | a c tx | + * | b d ty | + * | 0 0 1 | + */ +struct Matrix { + float a = 1; // scaleX + float b = 0; // skewY + float c = 0; // skewX + float d = 1; // scaleY + float tx = 0; // transX + float ty = 0; // transY + + /** + * Returns the identity matrix. + */ + static Matrix Identity() { + return {}; + } + + /** + * Returns a translation matrix. + */ + static Matrix Translate(float x, float y); + + /** + * Returns a scale matrix. + */ + static Matrix Scale(float sx, float sy); + + /** + * Returns a rotation matrix (angle in degrees). + */ + static Matrix Rotate(float degrees); + + /** + * Parses a matrix string "a,b,c,d,tx,ty". + */ + static Matrix Parse(const std::string& str); + + /** + * Returns the matrix as a string "a,b,c,d,tx,ty". + */ + std::string toString() const; + + /** + * Returns true if this is the identity matrix. + */ + bool isIdentity() const { + return a == 1 && b == 0 && c == 0 && d == 1 && tx == 0 && ty == 0; + } + + /** + * Concatenates this matrix with another. + */ + Matrix operator*(const Matrix& other) const; + + /** + * Transforms a point by this matrix. + */ + Point mapPoint(const Point& point) const; + + bool operator==(const Matrix& other) const { + return a == other.a && b == other.b && c == other.c && d == other.d && tx == other.tx && + ty == other.ty; + } + + bool operator!=(const Matrix& other) const { + return !(*this == other); + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/types/VerticalAlign.h b/pagx/include/pagx/model/types/VerticalAlign.h new file mode 100644 index 0000000000..e1db9e2487 --- /dev/null +++ b/pagx/include/pagx/model/types/VerticalAlign.h @@ -0,0 +1,37 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * Text vertical alignment. + */ +enum class VerticalAlign { + Top, + Center, + Bottom +}; + +std::string VerticalAlignToString(VerticalAlign align); +VerticalAlign VerticalAlignFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp index 7f309a82fa..bc57127416 100644 --- a/pagx/src/PAGXDocument.cpp +++ b/pagx/src/PAGXDocument.cpp @@ -16,7 +16,7 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "pagx/PAGXDocument.h" +#include "pagx/model/Document.h" #include #include #include "PAGXXMLParser.h" @@ -24,40 +24,14 @@ namespace pagx { -// Helper function to get id from a resource node -static std::string getResourceId(const Node* node) { - switch (node->type()) { - case NodeType::Image: - return static_cast(node)->id; - case NodeType::PathData: - return static_cast(node)->id; - case NodeType::SolidColor: - return static_cast(node)->id; - case NodeType::LinearGradient: - return static_cast(node)->id; - case NodeType::RadialGradient: - return static_cast(node)->id; - case NodeType::ConicGradient: - return static_cast(node)->id; - case NodeType::DiamondGradient: - return static_cast(node)->id; - case NodeType::ImagePattern: - return static_cast(node)->id; - case NodeType::Composition: - return static_cast(node)->id; - default: - return {}; - } -} - -std::shared_ptr PAGXDocument::Make(float docWidth, float docHeight) { - auto doc = std::shared_ptr(new PAGXDocument()); +std::shared_ptr Document::Make(float docWidth, float docHeight) { + auto doc = std::shared_ptr(new Document()); doc->width = docWidth; doc->height = docHeight; return doc; } -std::shared_ptr PAGXDocument::FromFile(const std::string& filePath) { +std::shared_ptr Document::FromFile(const std::string& filePath) { std::ifstream file(filePath, std::ios::binary); if (!file.is_open()) { return nullptr; @@ -74,19 +48,19 @@ std::shared_ptr PAGXDocument::FromFile(const std::string& filePath return doc; } -std::shared_ptr PAGXDocument::FromXML(const std::string& xmlContent) { +std::shared_ptr Document::FromXML(const std::string& xmlContent) { return FromXML(reinterpret_cast(xmlContent.data()), xmlContent.size()); } -std::shared_ptr PAGXDocument::FromXML(const uint8_t* data, size_t length) { +std::shared_ptr Document::FromXML(const uint8_t* data, size_t length) { return PAGXXMLParser::Parse(data, length); } -std::string PAGXDocument::toXML() const { +std::string Document::toXML() const { return PAGXXMLWriter::Write(*this); } -Node* PAGXDocument::findResource(const std::string& id) const { +Resource* Document::findResource(const std::string& id) const { if (resourceMapDirty) { rebuildResourceMap(); } @@ -94,7 +68,7 @@ Node* PAGXDocument::findResource(const std::string& id) const { return it != resourceMap.end() ? it->second : nullptr; } -Layer* PAGXDocument::findLayer(const std::string& id) const { +Layer* Document::findLayer(const std::string& id) const { // First search in top-level layers auto found = findLayerRecursive(layers, id); if (found) { @@ -102,7 +76,7 @@ Layer* PAGXDocument::findLayer(const std::string& id) const { } // Then search in Composition resources for (const auto& resource : resources) { - if (resource->type() == NodeType::Composition) { + if (resource->resourceType() == ResourceType::Composition) { auto comp = static_cast(resource.get()); found = findLayerRecursive(comp->layers, id); if (found) { @@ -113,10 +87,10 @@ Layer* PAGXDocument::findLayer(const std::string& id) const { return nullptr; } -void PAGXDocument::rebuildResourceMap() const { +void Document::rebuildResourceMap() const { resourceMap.clear(); for (const auto& resource : resources) { - auto id = getResourceId(resource.get()); + auto& id = resource->resourceId(); if (!id.empty()) { resourceMap[id] = resource.get(); } @@ -124,7 +98,7 @@ void PAGXDocument::rebuildResourceMap() const { resourceMapDirty = false; } -Layer* PAGXDocument::findLayerRecursive(const std::vector>& layers, +Layer* Document::findLayerRecursive(const std::vector>& layers, const std::string& id) { for (const auto& layer : layers) { if (layer->id == id) { diff --git a/pagx/src/PAGXElement.cpp b/pagx/src/PAGXElement.cpp index 15b8afec40..c60c07ef1d 100644 --- a/pagx/src/PAGXElement.cpp +++ b/pagx/src/PAGXElement.cpp @@ -16,45 +16,123 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "pagx/nodes/VectorElement.h" +#include "pagx/model/ColorSource.h" +#include "pagx/model/Element.h" +#include "pagx/model/LayerFilter.h" +#include "pagx/model/LayerStyle.h" +#include "pagx/model/Resource.h" namespace pagx { -const char* VectorElementTypeName(VectorElementType type) { +const char* ResourceTypeName(ResourceType type) { switch (type) { - case VectorElementType::Rectangle: + case ResourceType::Image: + return "Image"; + case ResourceType::PathData: + return "PathData"; + case ResourceType::Composition: + return "Composition"; + case ResourceType::SolidColor: + return "SolidColor"; + case ResourceType::LinearGradient: + return "LinearGradient"; + case ResourceType::RadialGradient: + return "RadialGradient"; + case ResourceType::ConicGradient: + return "ConicGradient"; + case ResourceType::DiamondGradient: + return "DiamondGradient"; + case ResourceType::ImagePattern: + return "ImagePattern"; + default: + return "Unknown"; + } +} + +const char* ElementTypeName(ElementType type) { + switch (type) { + case ElementType::Rectangle: return "Rectangle"; - case VectorElementType::Ellipse: + case ElementType::Ellipse: return "Ellipse"; - case VectorElementType::Polystar: + case ElementType::Polystar: return "Polystar"; - case VectorElementType::Path: + case ElementType::Path: return "Path"; - case VectorElementType::TextSpan: + case ElementType::TextSpan: return "TextSpan"; - case VectorElementType::Fill: + case ElementType::Fill: return "Fill"; - case VectorElementType::Stroke: + case ElementType::Stroke: return "Stroke"; - case VectorElementType::TrimPath: + case ElementType::TrimPath: return "TrimPath"; - case VectorElementType::RoundCorner: + case ElementType::RoundCorner: return "RoundCorner"; - case VectorElementType::MergePath: + case ElementType::MergePath: return "MergePath"; - case VectorElementType::TextModifier: + case ElementType::TextModifier: return "TextModifier"; - case VectorElementType::TextPath: + case ElementType::TextPath: return "TextPath"; - case VectorElementType::TextLayout: + case ElementType::TextLayout: return "TextLayout"; - case VectorElementType::Group: + case ElementType::Group: return "Group"; - case VectorElementType::Repeater: + case ElementType::Repeater: return "Repeater"; default: return "Unknown"; } } +const char* ColorSourceTypeName(ColorSourceType type) { + switch (type) { + case ColorSourceType::SolidColor: + return "SolidColor"; + case ColorSourceType::LinearGradient: + return "LinearGradient"; + case ColorSourceType::RadialGradient: + return "RadialGradient"; + case ColorSourceType::ConicGradient: + return "ConicGradient"; + case ColorSourceType::DiamondGradient: + return "DiamondGradient"; + case ColorSourceType::ImagePattern: + return "ImagePattern"; + default: + return "Unknown"; + } +} + +const char* LayerStyleTypeName(LayerStyleType type) { + switch (type) { + case LayerStyleType::DropShadowStyle: + return "DropShadowStyle"; + case LayerStyleType::InnerShadowStyle: + return "InnerShadowStyle"; + case LayerStyleType::BackgroundBlurStyle: + return "BackgroundBlurStyle"; + default: + return "Unknown"; + } +} + +const char* LayerFilterTypeName(LayerFilterType type) { + switch (type) { + case LayerFilterType::BlurFilter: + return "BlurFilter"; + case LayerFilterType::DropShadowFilter: + return "DropShadowFilter"; + case LayerFilterType::InnerShadowFilter: + return "InnerShadowFilter"; + case LayerFilterType::BlendFilter: + return "BlendFilter"; + case LayerFilterType::ColorMatrixFilter: + return "ColorMatrixFilter"; + default: + return "Unknown"; + } +} + } // namespace pagx diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp index 549702d10c..caaa486564 100644 --- a/pagx/src/PAGXTypes.cpp +++ b/pagx/src/PAGXTypes.cpp @@ -16,7 +16,9 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "pagx/PAGXTypes.h" +#include "pagx/model/types/Types.h" +#include "pagx/model/types/Enums.h" +#include "pagx/model/NodeType.h" #include #include #include @@ -428,4 +430,80 @@ DEFINE_ENUM_CONVERSION(RepeaterOrder, #undef DEFINE_ENUM_CONVERSION +const char* NodeTypeName(NodeType type) { + switch (type) { + case NodeType::SolidColor: + return "SolidColor"; + case NodeType::LinearGradient: + return "LinearGradient"; + case NodeType::RadialGradient: + return "RadialGradient"; + case NodeType::ConicGradient: + return "ConicGradient"; + case NodeType::DiamondGradient: + return "DiamondGradient"; + case NodeType::ImagePattern: + return "ImagePattern"; + case NodeType::ColorStop: + return "ColorStop"; + case NodeType::Rectangle: + return "Rectangle"; + case NodeType::Ellipse: + return "Ellipse"; + case NodeType::Polystar: + return "Polystar"; + case NodeType::Path: + return "Path"; + case NodeType::TextSpan: + return "TextSpan"; + case NodeType::Fill: + return "Fill"; + case NodeType::Stroke: + return "Stroke"; + case NodeType::TrimPath: + return "TrimPath"; + case NodeType::RoundCorner: + return "RoundCorner"; + case NodeType::MergePath: + return "MergePath"; + case NodeType::TextModifier: + return "TextModifier"; + case NodeType::TextPath: + return "TextPath"; + case NodeType::TextLayout: + return "TextLayout"; + case NodeType::RangeSelector: + return "RangeSelector"; + case NodeType::Repeater: + return "Repeater"; + case NodeType::Group: + return "Group"; + case NodeType::DropShadowStyle: + return "DropShadowStyle"; + case NodeType::InnerShadowStyle: + return "InnerShadowStyle"; + case NodeType::BackgroundBlurStyle: + return "BackgroundBlurStyle"; + case NodeType::BlurFilter: + return "BlurFilter"; + case NodeType::DropShadowFilter: + return "DropShadowFilter"; + case NodeType::InnerShadowFilter: + return "InnerShadowFilter"; + case NodeType::BlendFilter: + return "BlendFilter"; + case NodeType::ColorMatrixFilter: + return "ColorMatrixFilter"; + case NodeType::Image: + return "Image"; + case NodeType::PathData: + return "PathData"; + case NodeType::Composition: + return "Composition"; + case NodeType::Layer: + return "Layer"; + } + return "Unknown"; +} + } // namespace pagx diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 8aac3db848..a4779c0d5e 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -259,12 +259,12 @@ class XMLTokenizer { // PAGXXMLParser implementation //============================================================================== -std::shared_ptr PAGXXMLParser::Parse(const uint8_t* data, size_t length) { +std::shared_ptr PAGXXMLParser::Parse(const uint8_t* data, size_t length) { auto root = parseXML(data, length); if (!root || root->tag != "pagx") { return nullptr; } - auto doc = std::shared_ptr(new PAGXDocument()); + auto doc = std::shared_ptr(new Document()); parseDocument(root.get(), doc.get()); return doc; } @@ -274,7 +274,7 @@ std::unique_ptr PAGXXMLParser::parseXML(const uint8_t* data, size_t len return tokenizer.parse(); } -void PAGXXMLParser::parseDocument(const XMLNode* root, PAGXDocument* doc) { +void PAGXXMLParser::parseDocument(const XMLNode* root, Document* doc) { doc->version = getAttribute(root, "version", "1.0"); doc->width = getFloatAttribute(root, "width", 0); doc->height = getFloatAttribute(root, "height", 0); @@ -291,7 +291,7 @@ void PAGXXMLParser::parseDocument(const XMLNode* root, PAGXDocument* doc) { } } -void PAGXXMLParser::parseResources(const XMLNode* node, PAGXDocument* doc) { +void PAGXXMLParser::parseResources(const XMLNode* node, Document* doc) { for (const auto& child : node->children) { auto resource = parseResource(child.get()); if (resource) { @@ -300,7 +300,7 @@ void PAGXXMLParser::parseResources(const XMLNode* node, PAGXDocument* doc) { } } -std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { if (node->tag == "Image") { return parseImage(node); } @@ -308,40 +308,22 @@ std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { return parsePathData(node); } if (node->tag == "SolidColor") { - auto solidColor = parseSolidColor(node); - auto resource = std::make_unique(); - *resource = *solidColor; - return resource; + return parseSolidColor(node); } if (node->tag == "LinearGradient") { - auto gradient = parseLinearGradient(node); - auto resource = std::make_unique(); - *resource = *gradient; - return resource; + return parseLinearGradient(node); } if (node->tag == "RadialGradient") { - auto gradient = parseRadialGradient(node); - auto resource = std::make_unique(); - *resource = *gradient; - return resource; + return parseRadialGradient(node); } if (node->tag == "ConicGradient") { - auto gradient = parseConicGradient(node); - auto resource = std::make_unique(); - *resource = *gradient; - return resource; + return parseConicGradient(node); } if (node->tag == "DiamondGradient") { - auto gradient = parseDiamondGradient(node); - auto resource = std::make_unique(); - *resource = *gradient; - return resource; + return parseDiamondGradient(node); } if (node->tag == "ImagePattern") { - auto pattern = parseImagePattern(node); - auto resource = std::make_unique(); - *resource = *pattern; - return resource; + return parseImagePattern(node); } if (node->tag == "Composition") { return parseComposition(node); @@ -408,7 +390,7 @@ std::unique_ptr PAGXXMLParser::parseLayer(const XMLNode* node) { void PAGXXMLParser::parseContents(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { - auto element = parseVectorElement(child.get()); + auto element = parseElement(child.get()); if (element) { layer->contents.push_back(std::move(element)); } @@ -433,7 +415,7 @@ void PAGXXMLParser::parseFilters(const XMLNode* node, Layer* layer) { } } -std::unique_ptr PAGXXMLParser::parseVectorElement(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseElement(const XMLNode* node) { if (node->tag == "Rectangle") { return parseRectangle(node); } @@ -482,7 +464,7 @@ std::unique_ptr PAGXXMLParser::parseVectorElement(const XMLNode* node) { return nullptr; } -std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node) { if (node->tag == "SolidColor") { return parseSolidColor(node); } @@ -504,7 +486,7 @@ std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node) { return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) { if (node->tag == "DropShadowStyle") { return parseDropShadowStyle(node); } @@ -517,7 +499,7 @@ std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) { return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* node) { if (node->tag == "BlurFilter") { return parseBlurFilter(node); } @@ -757,7 +739,7 @@ std::unique_ptr PAGXXMLParser::parseGroup(const XMLNode* node) { group->alpha = getFloatAttribute(node, "alpha", 1); for (const auto& child : node->children) { - auto element = parseVectorElement(child.get()); + auto element = parseElement(child.get()); if (element) { group->elements.push_back(std::move(element)); } diff --git a/pagx/src/PAGXXMLParser.h b/pagx/src/PAGXXMLParser.h index f049a6cd3d..47fefc83a3 100644 --- a/pagx/src/PAGXXMLParser.h +++ b/pagx/src/PAGXXMLParser.h @@ -22,7 +22,7 @@ #include #include #include -#include "pagx/PAGXDocument.h" +#include "pagx/model/Document.h" namespace pagx { @@ -44,23 +44,23 @@ class PAGXXMLParser { /** * Parses XML data into a PAGXDocument. */ - static std::shared_ptr Parse(const uint8_t* data, size_t length); + static std::shared_ptr Parse(const uint8_t* data, size_t length); private: static std::unique_ptr parseXML(const uint8_t* data, size_t length); - static void parseDocument(const XMLNode* root, PAGXDocument* doc); - static void parseResources(const XMLNode* node, PAGXDocument* doc); - static std::unique_ptr parseResource(const XMLNode* node); + static void parseDocument(const XMLNode* root, Document* doc); + static void parseResources(const XMLNode* node, Document* doc); + static std::unique_ptr parseResource(const XMLNode* node); static std::unique_ptr parseLayer(const XMLNode* node); static void parseContents(const XMLNode* node, Layer* layer); static void parseStyles(const XMLNode* node, Layer* layer); static void parseFilters(const XMLNode* node, Layer* layer); - static std::unique_ptr parseVectorElement(const XMLNode* node); - static std::unique_ptr parseColorSource(const XMLNode* node); - static std::unique_ptr parseLayerStyle(const XMLNode* node); - static std::unique_ptr parseLayerFilter(const XMLNode* node); + static std::unique_ptr parseElement(const XMLNode* node); + static std::unique_ptr parseColorSource(const XMLNode* node); + static std::unique_ptr parseLayerStyle(const XMLNode* node); + static std::unique_ptr parseLayerFilter(const XMLNode* node); static std::unique_ptr parseRectangle(const XMLNode* node); static std::unique_ptr parseEllipse(const XMLNode* node); diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index 7b87d63c18..c328a90a57 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -20,6 +20,7 @@ #include #include #include +#include "pagx/model/Model.h" namespace pagx { @@ -187,7 +188,7 @@ static std::string floatListToString(const std::vector& values) { // ColorSource serialization helper //============================================================================== -static std::string colorSourceToKey(const Node* node) { +static std::string colorSourceToKey(const ColorSource* node) { if (!node) { return ""; } @@ -263,13 +264,13 @@ class ResourceContext { std::vector> pathDataResources = {}; // id -> svg string // All extracted ColorSource resources (ordered) - std::vector> colorSourceResources = {}; + std::vector> colorSourceResources = {};; int nextPathId = 1; int nextColorId = 1; // First pass: collect and count all resources - void collectFromDocument(const PAGXDocument& doc) { + void collectFromDocument(const Document& doc) { for (const auto& layer : doc.layers) { collectFromLayer(layer.get()); } @@ -296,7 +297,7 @@ class ResourceContext { } // Register ColorSource usage (for counting) - void registerColorSource(const Node* node) { + void registerColorSource(const ColorSource* node) { if (!node) { return; } @@ -322,7 +323,7 @@ class ResourceContext { } // Check if ColorSource should be extracted to Resources - bool shouldExtractColorSource(const Node* node) const { + bool shouldExtractColorSource(const ColorSource* node) const { if (!node) { return false; } @@ -332,7 +333,7 @@ class ResourceContext { } // Get ColorSource resource id (empty if should inline) - std::string getColorSourceId(const Node* node) const { + std::string getColorSourceId(const ColorSource* node) const { if (!node) { return ""; } @@ -354,7 +355,7 @@ class ResourceContext { } } - void collectFromVectorElement(const Node* element) { + void collectFromVectorElement(const Element* element) { switch (element->type()) { case NodeType::Path: { auto path = static_cast(element); @@ -394,12 +395,12 @@ class ResourceContext { // Forward declarations //============================================================================== -static void writeColorSource(XMLBuilder& xml, const Node* node, bool writeId); -static void writeVectorElement(XMLBuilder& xml, const Node* node, +static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId); +static void writeVectorElement(XMLBuilder& xml, const Element* node, const ResourceContext& ctx); -static void writeLayerStyle(XMLBuilder& xml, const Node* node); -static void writeLayerFilter(XMLBuilder& xml, const Node* node); -static void writeResource(XMLBuilder& xml, const Node* node, const ResourceContext& ctx); +static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node); +static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node); +static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceContext& ctx); static void writeLayer(XMLBuilder& xml, const Layer* node, const ResourceContext& ctx); //============================================================================== @@ -415,7 +416,7 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& stops } } -static void writeColorSource(XMLBuilder& xml, const Node* node, bool writeId) { +static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId) { switch (node->type()) { case NodeType::SolidColor: { auto solid = static_cast(node); @@ -544,7 +545,7 @@ static void writeColorSource(XMLBuilder& xml, const Node* node, bool writeId) { } // Write ColorSource with assigned id (for Resources section) -static void writeColorSourceWithId(XMLBuilder& xml, const Node* node, +static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, const std::string& id) { switch (node->type()) { case NodeType::SolidColor: { @@ -665,7 +666,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const Node* node, // VectorElement writing //============================================================================== -static void writeVectorElement(XMLBuilder& xml, const Node* node, +static void writeVectorElement(XMLBuilder& xml, const Element* node, const ResourceContext& ctx) { switch (node->type()) { case NodeType::Rectangle: { @@ -999,7 +1000,7 @@ static void writeVectorElement(XMLBuilder& xml, const Node* node, // LayerStyle writing //============================================================================== -static void writeLayerStyle(XMLBuilder& xml, const Node* node) { +static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { switch (node->type()) { case NodeType::DropShadowStyle: { auto style = static_cast(node); @@ -1053,7 +1054,7 @@ static void writeLayerStyle(XMLBuilder& xml, const Node* node) { // LayerFilter writing //============================================================================== -static void writeLayerFilter(XMLBuilder& xml, const Node* node) { +static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { switch (node->type()) { case NodeType::BlurFilter: { auto filter = static_cast(node); @@ -1117,7 +1118,7 @@ static void writeLayerFilter(XMLBuilder& xml, const Node* node) { // Resource writing //============================================================================== -static void writeResource(XMLBuilder& xml, const Node* node, const ResourceContext& ctx) { +static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceContext& ctx) { switch (node->type()) { case NodeType::Image: { auto image = static_cast(node); @@ -1158,7 +1159,7 @@ static void writeResource(XMLBuilder& xml, const Node* node, const ResourceConte case NodeType::ConicGradient: case NodeType::DiamondGradient: case NodeType::ImagePattern: - writeColorSource(xml, static_cast(node), true); + writeColorSource(xml, static_cast(node), true); break; default: break; @@ -1254,7 +1255,7 @@ static void writeLayer(XMLBuilder& xml, const Layer* node, const ResourceContext // Main Write function //============================================================================== -std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { +std::string PAGXXMLWriter::Write(const Document& doc) { // First pass: collect resources and count references ResourceContext ctx = {}; ctx.collectFromDocument(doc); @@ -1262,7 +1263,7 @@ std::string PAGXXMLWriter::Write(const PAGXDocument& doc) { // Build ColorSource resources list (only those with multiple references) // We need to store pointers to actual ColorSource nodes for writing - std::unordered_map colorSourceByKey = {}; + std::unordered_map colorSourceByKey = {}; for (const auto& layer : doc.layers) { std::function collectColorSources = [&](const Layer* layer) { for (const auto& element : layer->contents) { diff --git a/pagx/src/PAGXXMLWriter.h b/pagx/src/PAGXXMLWriter.h index 4ca2922a1d..9248705ce6 100644 --- a/pagx/src/PAGXXMLWriter.h +++ b/pagx/src/PAGXXMLWriter.h @@ -19,7 +19,7 @@ #pragma once #include -#include "pagx/PAGXDocument.h" +#include "pagx/model/Document.h" namespace pagx { @@ -31,7 +31,7 @@ class PAGXXMLWriter { /** * Writes a PAGXDocument to XML string. */ - static std::string Write(const PAGXDocument& doc); + static std::string Write(const Document& doc); }; } // namespace pagx diff --git a/pagx/src/PathData.cpp b/pagx/src/PathData.cpp index 57a1d8ccdd..abd206f666 100644 --- a/pagx/src/PathData.cpp +++ b/pagx/src/PathData.cpp @@ -16,7 +16,7 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "pagx/PathData.h" +#include "pagx/model/PathData.h" #include #include #include diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index bf8fba8fbf..310c474e73 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -21,12 +21,13 @@ #include #include #include +#include "pagx/PAGXModel.h" #include "SVGParserInternal.h" #include "xml/XMLDOM.h" namespace pagx { -std::shared_ptr PAGXSVGParser::Parse(const std::string& filePath, +std::shared_ptr PAGXSVGParser::Parse(const std::string& filePath, const Options& options) { SVGParserImpl parser(options); auto doc = parser.parseFile(filePath); @@ -39,13 +40,13 @@ std::shared_ptr PAGXSVGParser::Parse(const std::string& filePath, return doc; } -std::shared_ptr PAGXSVGParser::Parse(const uint8_t* data, size_t length, +std::shared_ptr PAGXSVGParser::Parse(const uint8_t* data, size_t length, const Options& options) { SVGParserImpl parser(options); return parser.parse(data, length); } -std::shared_ptr PAGXSVGParser::ParseString(const std::string& svgContent, +std::shared_ptr PAGXSVGParser::ParseString(const std::string& svgContent, const Options& options) { return Parse(reinterpret_cast(svgContent.data()), svgContent.size(), options); } @@ -55,7 +56,7 @@ std::shared_ptr PAGXSVGParser::ParseString(const std::string& svgC SVGParserImpl::SVGParserImpl(const PAGXSVGParser::Options& options) : _options(options) { } -std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { +std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { if (!data || length == 0) { return nullptr; } @@ -68,7 +69,7 @@ std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t l return parseDOM(dom); } -std::shared_ptr SVGParserImpl::parseFile(const std::string& filePath) { +std::shared_ptr SVGParserImpl::parseFile(const std::string& filePath) { auto dom = DOM::MakeFromFile(filePath); if (!dom) { return nullptr; @@ -84,7 +85,7 @@ std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, return found ? value : defaultValue; } -std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& dom) { +std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& dom) { auto root = dom->getRootNode(); if (!root || root->name != "svg") { return nullptr; @@ -113,7 +114,7 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr return nullptr; } - _document = PAGXDocument::Make(width, height); + _document = Document::Make(width, height); // Collect all IDs from the SVG to avoid conflicts when generating new IDs. collectAllIds(root); @@ -332,7 +333,7 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle) { const auto& tag = element->name; @@ -353,7 +354,7 @@ void SVGParserImpl::convertChildren(const std::shared_ptr& element, addFillStroke(element, contents, inheritedStyle); } -std::unique_ptr SVGParserImpl::convertElement( +std::unique_ptr SVGParserImpl::convertElement( const std::shared_ptr& element) { const auto& tag = element->name; @@ -414,7 +415,7 @@ std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& e return group; } -std::unique_ptr SVGParserImpl::convertRect( +std::unique_ptr SVGParserImpl::convertRect( const std::shared_ptr& element) { float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); @@ -437,7 +438,7 @@ std::unique_ptr SVGParserImpl::convertRect( return rect; } -std::unique_ptr SVGParserImpl::convertCircle( +std::unique_ptr SVGParserImpl::convertCircle( const std::shared_ptr& element) { float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); @@ -452,7 +453,7 @@ std::unique_ptr SVGParserImpl::convertCircle( return ellipse; } -std::unique_ptr SVGParserImpl::convertEllipse( +std::unique_ptr SVGParserImpl::convertEllipse( const std::shared_ptr& element) { float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); @@ -468,7 +469,7 @@ std::unique_ptr SVGParserImpl::convertEllipse( return ellipse; } -std::unique_ptr SVGParserImpl::convertLine( +std::unique_ptr SVGParserImpl::convertLine( const std::shared_ptr& element) { float x1 = parseLength(getAttribute(element, "x1"), _viewBoxWidth); float y1 = parseLength(getAttribute(element, "y1"), _viewBoxHeight); @@ -482,21 +483,21 @@ std::unique_ptr SVGParserImpl::convertLine( return path; } -std::unique_ptr SVGParserImpl::convertPolyline( +std::unique_ptr SVGParserImpl::convertPolyline( const std::shared_ptr& element) { auto path = std::make_unique(); path->data = parsePoints(getAttribute(element, "points"), false); return path; } -std::unique_ptr SVGParserImpl::convertPolygon( +std::unique_ptr SVGParserImpl::convertPolygon( const std::shared_ptr& element) { auto path = std::make_unique(); path->data = parsePoints(getAttribute(element, "points"), true); return path; } -std::unique_ptr SVGParserImpl::convertPath( +std::unique_ptr SVGParserImpl::convertPath( const std::shared_ptr& element) { auto path = std::make_unique(); std::string d = getAttribute(element, "d"); @@ -555,7 +556,7 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr return group; } -std::unique_ptr SVGParserImpl::convertUse( +std::unique_ptr SVGParserImpl::convertUse( const std::shared_ptr& element) { std::string href = getAttribute(element, "xlink:href"); if (href.empty()) { @@ -853,7 +854,7 @@ std::unique_ptr SVGParserImpl::convertPattern( } void SVGParserImpl::addFillStroke(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle) { // Get shape bounds for pattern calculations (computed once, used if needed). Rect shapeBounds = getShapeBounds(element); @@ -1502,7 +1503,7 @@ std::string SVGParserImpl::registerImageResource(const std::string& imageSource) } // Helper function to check if two VectorElement nodes are the same geometry. -static bool isSameGeometry(const Node* a, const Node* b) { +static bool isSameGeometry(const Element* a, const Element* b) { if (!a || !b || a->type() != b->type()) { return false; } @@ -1533,8 +1534,8 @@ static bool isSameGeometry(const Node* a, const Node* b) { } // Check if a layer is a simple shape layer (contains exactly one geometry and one Fill or Stroke). -static bool isSimpleShapeLayer(const Layer* layer, const Node*& outGeometry, - const Node*& outPainter) { +static bool isSimpleShapeLayer(const Layer* layer, const Element*& outGeometry, + const Element*& outPainter) { if (!layer || layer->contents.size() != 2) { return false; } @@ -1571,14 +1572,14 @@ void SVGParserImpl::mergeAdjacentLayers(std::vector>& lay size_t i = 0; while (i < layers.size()) { - const Node* geomA = nullptr; - const Node* painterA = nullptr; + const Element* geomA = nullptr; + const Element* painterA = nullptr; if (isSimpleShapeLayer(layers[i].get(), geomA, painterA)) { // Check if the next layer has the same geometry. if (i + 1 < layers.size()) { - const Node* geomB = nullptr; - const Node* painterB = nullptr; + const Element* geomB = nullptr; + const Element* painterB = nullptr; if (isSimpleShapeLayer(layers[i + 1].get(), geomB, painterB) && isSameGeometry(geomA, geomB)) { @@ -1649,7 +1650,7 @@ std::unique_ptr SVGParserImpl::convertMaskElement( void SVGParserImpl::convertFilterElement( const std::shared_ptr& filterElement, - std::vector>& filters) { + std::vector>& filters) { // Parse filter children to find effect elements. auto child = filterElement->getFirstChild(); while (child) { diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 8f2981f76b..4b05065ba6 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -23,7 +23,7 @@ #include #include #include -#include "pagx/PAGXDocument.h" +#include "pagx/model/Document.h" #include "pagx/PAGXSVGParser.h" #include "xml/XMLDOM.h" @@ -47,32 +47,32 @@ class SVGParserImpl { public: explicit SVGParserImpl(const PAGXSVGParser::Options& options); - std::shared_ptr parse(const uint8_t* data, size_t length); - std::shared_ptr parseFile(const std::string& filePath); + std::shared_ptr parse(const uint8_t* data, size_t length); + std::shared_ptr parseFile(const std::string& filePath); private: - std::shared_ptr parseDOM(const std::shared_ptr& dom); + std::shared_ptr parseDOM(const std::shared_ptr& dom); void parseDefs(const std::shared_ptr& defsNode); std::unique_ptr convertToLayer(const std::shared_ptr& element, const InheritedStyle& parentStyle); void convertChildren(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle); - std::unique_ptr convertElement(const std::shared_ptr& element); + std::unique_ptr convertElement(const std::shared_ptr& element); std::unique_ptr convertG(const std::shared_ptr& element, const InheritedStyle& inheritedStyle); - std::unique_ptr convertRect(const std::shared_ptr& element); - std::unique_ptr convertCircle(const std::shared_ptr& element); - std::unique_ptr convertEllipse(const std::shared_ptr& element); - std::unique_ptr convertLine(const std::shared_ptr& element); - std::unique_ptr convertPolyline(const std::shared_ptr& element); - std::unique_ptr convertPolygon(const std::shared_ptr& element); - std::unique_ptr convertPath(const std::shared_ptr& element); + std::unique_ptr convertRect(const std::shared_ptr& element); + std::unique_ptr convertCircle(const std::shared_ptr& element); + std::unique_ptr convertEllipse(const std::shared_ptr& element); + std::unique_ptr convertLine(const std::shared_ptr& element); + std::unique_ptr convertPolyline(const std::shared_ptr& element); + std::unique_ptr convertPolygon(const std::shared_ptr& element); + std::unique_ptr convertPath(const std::shared_ptr& element); std::unique_ptr convertText(const std::shared_ptr& element, const InheritedStyle& inheritedStyle); - std::unique_ptr convertUse(const std::shared_ptr& element); + std::unique_ptr convertUse(const std::shared_ptr& element); std::unique_ptr convertLinearGradient( const std::shared_ptr& element, const Rect& shapeBounds); @@ -84,10 +84,10 @@ class SVGParserImpl { std::unique_ptr convertMaskElement(const std::shared_ptr& maskElement, const InheritedStyle& parentStyle); void convertFilterElement(const std::shared_ptr& filterElement, - std::vector>& filters); + std::vector>& filters); void addFillStroke(const std::shared_ptr& element, - std::vector>& contents, + std::vector>& contents, const InheritedStyle& inheritedStyle); // Compute shape bounds from SVG element attributes. @@ -125,7 +125,7 @@ class SVGParserImpl { void parseCustomData(const std::shared_ptr& element, Layer* layer); PAGXSVGParser::Options _options = {}; - std::shared_ptr _document = nullptr; + std::shared_ptr _document = nullptr; std::unordered_map> _defs = {}; std::vector> _maskLayers = {}; std::unordered_map _imageSourceToId = {}; // Maps image source to resource ID. diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 25158a68b4..b94c1577b8 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -20,6 +20,7 @@ #include #include #include +#include "pagx/model/Model.h" #include "pagx/PAGXSVGParser.h" #include "tgfx/core/Data.h" #include "tgfx/core/Font.h" @@ -214,7 +215,7 @@ class LayerBuilderImpl { } } - PAGXContent build(const PAGXDocument& document) { + PAGXContent build(const Document& document) { // Cache resources for later lookup. _resources = &document.resources; @@ -313,7 +314,7 @@ class LayerBuilderImpl { return layer; } - std::shared_ptr convertVectorElement(const Node* node) { + std::shared_ptr convertVectorElement(const Element* node) { if (!node) { return nullptr; } @@ -470,7 +471,7 @@ class LayerBuilderImpl { return stroke; } - std::shared_ptr convertColorSource(const Node* node) { + std::shared_ptr convertColorSource(const ColorSource* node) { if (!node) { return nullptr; } @@ -676,7 +677,7 @@ class LayerBuilderImpl { } } - std::shared_ptr convertLayerStyle(const Node* node) { + std::shared_ptr convertLayerStyle(const LayerStyle* node) { if (!node) { return nullptr; } @@ -697,7 +698,7 @@ class LayerBuilderImpl { } } - std::shared_ptr convertLayerFilter(const Node* node) { + std::shared_ptr convertLayerFilter(const LayerFilter* node) { if (!node) { return nullptr; } @@ -718,7 +719,7 @@ class LayerBuilderImpl { } LayerBuilder::Options _options = {}; - const std::vector>* _resources = nullptr; + const std::vector>* _resources = nullptr; std::shared_ptr _textShaper = nullptr; std::unordered_map> _layerById = {}; std::vector, std::string, tgfx::LayerMaskType>> @@ -727,13 +728,13 @@ class LayerBuilderImpl { // Public API implementation -PAGXContent LayerBuilder::Build(const PAGXDocument& document, const Options& options) { +PAGXContent LayerBuilder::Build(const Document& document, const Options& options) { LayerBuilderImpl builder(options); return builder.build(document); } PAGXContent LayerBuilder::FromFile(const std::string& filePath, const Options& options) { - auto document = PAGXDocument::FromFile(filePath); + auto document = Document::FromFile(filePath); if (!document) { return {}; } @@ -750,7 +751,7 @@ PAGXContent LayerBuilder::FromFile(const std::string& filePath, const Options& o } PAGXContent LayerBuilder::FromData(const uint8_t* data, size_t length, const Options& options) { - auto document = PAGXDocument::FromXML(data, length); + auto document = Document::FromXML(data, length); if (!document) { return {}; } diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index bf14e4322d..5327492670 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -18,10 +18,10 @@ #include #include "pagx/LayerBuilder.h" -#include "pagx/PAGXDocument.h" +#include "pagx/model/Document.h" #include "pagx/PAGXSVGParser.h" #include "pagx/PAGXModel.h" -#include "pagx/PathData.h" +#include "pagx/model/PathData.h" #include "tgfx/core/Data.h" #include "tgfx/core/Stream.h" #include "tgfx/core/Surface.h" @@ -226,7 +226,7 @@ PAG_TEST(PAGXTest, PAGXNodeBasic) { * Test case: PAGXDocument creation and XML export */ PAG_TEST(PAGXTest, PAGXDocumentXMLExport) { - auto doc = pagx::PAGXDocument::Make(400, 300); + auto doc = pagx::Document::Make(400, 300); ASSERT_TRUE(doc != nullptr); EXPECT_EQ(doc->width, 400.0f); EXPECT_EQ(doc->height, 300.0f); @@ -271,7 +271,7 @@ PAG_TEST(PAGXTest, PAGXDocumentXMLExport) { */ PAG_TEST(PAGXTest, PAGXDocumentRoundTrip) { // Create a document - auto doc1 = pagx::PAGXDocument::Make(200, 150); + auto doc1 = pagx::Document::Make(200, 150); ASSERT_TRUE(doc1 != nullptr); auto layer = std::make_unique(); @@ -295,7 +295,7 @@ PAG_TEST(PAGXTest, PAGXDocumentRoundTrip) { EXPECT_FALSE(xml.empty()); // Parse the XML back - auto doc2 = pagx::PAGXDocument::FromXML(xml); + auto doc2 = pagx::Document::FromXML(xml); ASSERT_TRUE(doc2 != nullptr); // Verify the dimensions From 1d37b01cf0ba547a3f191000c0047a2dcb548c25 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:16:29 +0800 Subject: [PATCH 087/678] Remove obsolete pagx header directories in favor of model directory structure. --- pagx/include/pagx/PAGXDocument.h | 121 --------- pagx/include/pagx/PathData.h | 176 -------------- pagx/include/pagx/nodes/BackgroundBlurStyle.h | 46 ---- pagx/include/pagx/nodes/BlendFilter.h | 44 ---- pagx/include/pagx/nodes/BlurFilter.h | 44 ---- pagx/include/pagx/nodes/ColorMatrixFilter.h | 42 ---- pagx/include/pagx/nodes/ColorSource.h | 56 ----- pagx/include/pagx/nodes/ColorStop.h | 34 --- pagx/include/pagx/nodes/Composition.h | 45 ---- pagx/include/pagx/nodes/ConicGradient.h | 50 ---- pagx/include/pagx/nodes/DiamondGradient.h | 49 ---- pagx/include/pagx/nodes/DropShadowFilter.h | 47 ---- pagx/include/pagx/nodes/DropShadowStyle.h | 49 ---- pagx/include/pagx/nodes/Ellipse.h | 55 ----- pagx/include/pagx/nodes/Fill.h | 81 ------- pagx/include/pagx/nodes/Group.h | 84 ------- pagx/include/pagx/nodes/Image.h | 39 --- pagx/include/pagx/nodes/ImagePattern.h | 50 ---- pagx/include/pagx/nodes/InnerShadowFilter.h | 47 ---- pagx/include/pagx/nodes/InnerShadowStyle.h | 48 ---- pagx/include/pagx/nodes/Layer.h | 167 ------------- pagx/include/pagx/nodes/LayerFilter.h | 55 ----- pagx/include/pagx/nodes/LayerStyle.h | 53 ---- pagx/include/pagx/nodes/LinearGradient.h | 49 ---- pagx/include/pagx/nodes/MergePath.h | 46 ---- pagx/include/pagx/nodes/Node.h | 106 -------- pagx/include/pagx/nodes/Path.h | 51 ---- pagx/include/pagx/nodes/PathDataResource.h | 39 --- pagx/include/pagx/nodes/Polystar.h | 88 ------- pagx/include/pagx/nodes/RadialGradient.h | 49 ---- pagx/include/pagx/nodes/RangeSelector.h | 45 ---- pagx/include/pagx/nodes/Rectangle.h | 60 ----- pagx/include/pagx/nodes/Repeater.h | 89 ------- pagx/include/pagx/nodes/RoundCorner.h | 45 ---- pagx/include/pagx/nodes/SolidColor.h | 44 ---- pagx/include/pagx/nodes/Stroke.h | 113 --------- pagx/include/pagx/nodes/TextLayout.h | 79 ------ pagx/include/pagx/nodes/TextModifier.h | 101 -------- pagx/include/pagx/nodes/TextPath.h | 78 ------ pagx/include/pagx/nodes/TextSpan.h | 87 ------- pagx/include/pagx/nodes/TrimPath.h | 66 ----- pagx/include/pagx/nodes/VectorElement.h | 114 --------- pagx/include/pagx/types/BlendMode.h | 52 ---- pagx/include/pagx/types/Color.h | 170 ------------- pagx/include/pagx/types/Enums.h | 54 ----- pagx/include/pagx/types/FillRule.h | 36 --- pagx/include/pagx/types/FontStyle.h | 36 --- pagx/include/pagx/types/LineCap.h | 37 --- pagx/include/pagx/types/LineJoin.h | 37 --- pagx/include/pagx/types/MaskType.h | 37 --- pagx/include/pagx/types/Matrix.h | 160 ------------ pagx/include/pagx/types/MergePathMode.h | 39 --- pagx/include/pagx/types/Overflow.h | 37 --- pagx/include/pagx/types/Placement.h | 36 --- pagx/include/pagx/types/Point.h | 39 --- pagx/include/pagx/types/PolystarType.h | 36 --- pagx/include/pagx/types/Rect.h | 79 ------ pagx/include/pagx/types/RepeaterOrder.h | 36 --- pagx/include/pagx/types/SamplingMode.h | 37 --- pagx/include/pagx/types/SelectorMode.h | 40 --- pagx/include/pagx/types/SelectorShape.h | 40 --- pagx/include/pagx/types/SelectorUnit.h | 36 --- pagx/include/pagx/types/Size.h | 39 --- pagx/include/pagx/types/StrokeAlign.h | 37 --- pagx/include/pagx/types/TextAlign.h | 38 --- pagx/include/pagx/types/TextPathAlign.h | 37 --- pagx/include/pagx/types/TileMode.h | 38 --- pagx/include/pagx/types/TrimType.h | 36 --- pagx/include/pagx/types/Types.h | 229 ------------------ pagx/include/pagx/types/VerticalAlign.h | 37 --- pagx/src/PAGXNode.cpp | 153 ------------ 71 files changed, 4549 deletions(-) delete mode 100644 pagx/include/pagx/PAGXDocument.h delete mode 100644 pagx/include/pagx/PathData.h delete mode 100644 pagx/include/pagx/nodes/BackgroundBlurStyle.h delete mode 100644 pagx/include/pagx/nodes/BlendFilter.h delete mode 100644 pagx/include/pagx/nodes/BlurFilter.h delete mode 100644 pagx/include/pagx/nodes/ColorMatrixFilter.h delete mode 100644 pagx/include/pagx/nodes/ColorSource.h delete mode 100644 pagx/include/pagx/nodes/ColorStop.h delete mode 100644 pagx/include/pagx/nodes/Composition.h delete mode 100644 pagx/include/pagx/nodes/ConicGradient.h delete mode 100644 pagx/include/pagx/nodes/DiamondGradient.h delete mode 100644 pagx/include/pagx/nodes/DropShadowFilter.h delete mode 100644 pagx/include/pagx/nodes/DropShadowStyle.h delete mode 100644 pagx/include/pagx/nodes/Ellipse.h delete mode 100644 pagx/include/pagx/nodes/Fill.h delete mode 100644 pagx/include/pagx/nodes/Group.h delete mode 100644 pagx/include/pagx/nodes/Image.h delete mode 100644 pagx/include/pagx/nodes/ImagePattern.h delete mode 100644 pagx/include/pagx/nodes/InnerShadowFilter.h delete mode 100644 pagx/include/pagx/nodes/InnerShadowStyle.h delete mode 100644 pagx/include/pagx/nodes/Layer.h delete mode 100644 pagx/include/pagx/nodes/LayerFilter.h delete mode 100644 pagx/include/pagx/nodes/LayerStyle.h delete mode 100644 pagx/include/pagx/nodes/LinearGradient.h delete mode 100644 pagx/include/pagx/nodes/MergePath.h delete mode 100644 pagx/include/pagx/nodes/Node.h delete mode 100644 pagx/include/pagx/nodes/Path.h delete mode 100644 pagx/include/pagx/nodes/PathDataResource.h delete mode 100644 pagx/include/pagx/nodes/Polystar.h delete mode 100644 pagx/include/pagx/nodes/RadialGradient.h delete mode 100644 pagx/include/pagx/nodes/RangeSelector.h delete mode 100644 pagx/include/pagx/nodes/Rectangle.h delete mode 100644 pagx/include/pagx/nodes/Repeater.h delete mode 100644 pagx/include/pagx/nodes/RoundCorner.h delete mode 100644 pagx/include/pagx/nodes/SolidColor.h delete mode 100644 pagx/include/pagx/nodes/Stroke.h delete mode 100644 pagx/include/pagx/nodes/TextLayout.h delete mode 100644 pagx/include/pagx/nodes/TextModifier.h delete mode 100644 pagx/include/pagx/nodes/TextPath.h delete mode 100644 pagx/include/pagx/nodes/TextSpan.h delete mode 100644 pagx/include/pagx/nodes/TrimPath.h delete mode 100644 pagx/include/pagx/nodes/VectorElement.h delete mode 100644 pagx/include/pagx/types/BlendMode.h delete mode 100644 pagx/include/pagx/types/Color.h delete mode 100644 pagx/include/pagx/types/Enums.h delete mode 100644 pagx/include/pagx/types/FillRule.h delete mode 100644 pagx/include/pagx/types/FontStyle.h delete mode 100644 pagx/include/pagx/types/LineCap.h delete mode 100644 pagx/include/pagx/types/LineJoin.h delete mode 100644 pagx/include/pagx/types/MaskType.h delete mode 100644 pagx/include/pagx/types/Matrix.h delete mode 100644 pagx/include/pagx/types/MergePathMode.h delete mode 100644 pagx/include/pagx/types/Overflow.h delete mode 100644 pagx/include/pagx/types/Placement.h delete mode 100644 pagx/include/pagx/types/Point.h delete mode 100644 pagx/include/pagx/types/PolystarType.h delete mode 100644 pagx/include/pagx/types/Rect.h delete mode 100644 pagx/include/pagx/types/RepeaterOrder.h delete mode 100644 pagx/include/pagx/types/SamplingMode.h delete mode 100644 pagx/include/pagx/types/SelectorMode.h delete mode 100644 pagx/include/pagx/types/SelectorShape.h delete mode 100644 pagx/include/pagx/types/SelectorUnit.h delete mode 100644 pagx/include/pagx/types/Size.h delete mode 100644 pagx/include/pagx/types/StrokeAlign.h delete mode 100644 pagx/include/pagx/types/TextAlign.h delete mode 100644 pagx/include/pagx/types/TextPathAlign.h delete mode 100644 pagx/include/pagx/types/TileMode.h delete mode 100644 pagx/include/pagx/types/TrimType.h delete mode 100644 pagx/include/pagx/types/Types.h delete mode 100644 pagx/include/pagx/types/VerticalAlign.h delete mode 100644 pagx/src/PAGXNode.cpp diff --git a/pagx/include/pagx/PAGXDocument.h b/pagx/include/pagx/PAGXDocument.h deleted file mode 100644 index a16eae283c..0000000000 --- a/pagx/include/pagx/PAGXDocument.h +++ /dev/null @@ -1,121 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include -#include "pagx/PAGXModel.h" - -namespace pagx { - -class PAGXXMLParser; - -/** - * PAGXDocument is the root container for a PAGX document. - * It contains resources and layers, and provides methods for loading, saving, and manipulating - * the document. - */ -class PAGXDocument { - public: - /** - * Format version. - */ - std::string version = "1.0"; - - /** - * Canvas width. - */ - float width = 0; - - /** - * Canvas height. - */ - float height = 0; - - /** - * Resources (images, gradients, compositions, etc.). - * These can be referenced by "#id" in the document. - */ - std::vector> resources = {}; - - /** - * Top-level layers. - */ - std::vector> layers = {}; - - /** - * Base path for resolving relative resource paths. - */ - std::string basePath = {}; - - /** - * Creates an empty document with the specified size. - */ - static std::shared_ptr Make(float width, float height); - - /** - * Loads a document from a file. - * Returns nullptr if the file cannot be loaded. - */ - static std::shared_ptr FromFile(const std::string& filePath); - - /** - * Parses a document from XML content. - * Returns nullptr if parsing fails. - */ - static std::shared_ptr FromXML(const std::string& xmlContent); - - /** - * Parses a document from XML data. - * Returns nullptr if parsing fails. - */ - static std::shared_ptr FromXML(const uint8_t* data, size_t length); - - /** - * Exports the document to XML format. - */ - std::string toXML() const; - - /** - * Finds a resource by ID. - * Returns nullptr if not found. - */ - Node* findResource(const std::string& id) const; - - /** - * Finds a layer by ID (searches recursively). - * Returns nullptr if not found. - */ - Layer* findLayer(const std::string& id) const; - - private: - friend class PAGXXMLParser; - PAGXDocument() = default; - - mutable std::unordered_map resourceMap = {}; - mutable bool resourceMapDirty = true; - - void rebuildResourceMap() const; - static Layer* findLayerRecursive(const std::vector>& layers, - const std::string& id); -}; - -} // namespace pagx diff --git a/pagx/include/pagx/PathData.h b/pagx/include/pagx/PathData.h deleted file mode 100644 index d26d5873d6..0000000000 --- a/pagx/include/pagx/PathData.h +++ /dev/null @@ -1,176 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Path command types. - */ -enum class PathVerb : uint8_t { - Move, // 1 point: destination - Line, // 1 point: end point - Quad, // 2 points: control point, end point - Cubic, // 3 points: control point 1, control point 2, end point - Close // 0 points -}; - -/** - * PathData stores path commands in a format optimized for fast iteration - * and serialization. Unlike tgfx::Path, it exposes raw data arrays directly. - */ -class PathData { - public: - PathData() = default; - - /** - * Creates a PathData from an SVG path data string (d attribute). - */ - static PathData FromSVGString(const std::string& d); - - /** - * Starts a new contour at the specified point. - */ - void moveTo(float x, float y); - - /** - * Adds a line from the current point to the specified point. - */ - void lineTo(float x, float y); - - /** - * Adds a quadratic Bezier curve from the current point. - * @param cx control point x - * @param cy control point y - * @param x end point x - * @param y end point y - */ - void quadTo(float cx, float cy, float x, float y); - - /** - * Adds a cubic Bezier curve from the current point. - * @param c1x first control point x - * @param c1y first control point y - * @param c2x second control point x - * @param c2y second control point y - * @param x end point x - * @param y end point y - */ - void cubicTo(float c1x, float c1y, float c2x, float c2y, float x, float y); - - /** - * Closes the current contour by connecting to the starting point. - */ - void close(); - - /** - * Adds a rectangle to the path. - */ - void addRect(const Rect& rect); - - /** - * Adds an oval inscribed in the specified rectangle. - */ - void addOval(const Rect& rect); - - /** - * Adds a rounded rectangle to the path. - */ - void addRoundRect(const Rect& rect, float radiusX, float radiusY); - - /** - * Returns the array of path commands. - */ - const std::vector& verbs() const { - return _verbs; - } - - /** - * Returns the array of point coordinates. - * Points are stored as [x0, y0, x1, y1, ...]. - */ - const std::vector& points() const { - return _points; - } - - /** - * Returns the number of point coordinates. - */ - size_t countPoints() const { - return _points.size() / 2; - } - - /** - * Iterates over all path commands with a visitor function. - * The visitor receives the verb and a pointer to the point data. - */ - template - void forEach(Visitor&& visitor) const { - size_t pointIndex = 0; - for (auto verb : _verbs) { - const float* pts = _points.data() + pointIndex; - visitor(verb, pts); - pointIndex += PointsPerVerb(verb) * 2; - } - } - - /** - * Converts the path to an SVG path data string. - */ - std::string toSVGString() const; - - /** - * Returns the bounding rectangle of the path. - */ - Rect getBounds() const; - - /** - * Returns true if the path contains no commands. - */ - bool isEmpty() const { - return _verbs.empty(); - } - - /** - * Clears all path data. - */ - void clear(); - - /** - * Transforms all points in the path by the given matrix. - */ - void transform(const Matrix& matrix); - - /** - * Returns the number of points used by the given verb. - */ - static int PointsPerVerb(PathVerb verb); - - private: - std::vector _verbs = {}; - std::vector _points = {}; - mutable Rect _cachedBounds = {}; - mutable bool _boundsDirty = true; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/BackgroundBlurStyle.h b/pagx/include/pagx/nodes/BackgroundBlurStyle.h deleted file mode 100644 index 33b77c4ce7..0000000000 --- a/pagx/include/pagx/nodes/BackgroundBlurStyle.h +++ /dev/null @@ -1,46 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/LayerStyle.h" -#include "pagx/types/BlendMode.h" -#include "pagx/types/TileMode.h" - -namespace pagx { - -/** - * Background blur style. - */ -class BackgroundBlurStyle : public LayerStyle { - public: - float blurrinessX = 0; - float blurrinessY = 0; - TileMode tileMode = TileMode::Mirror; - BlendMode blendMode = BlendMode::Normal; - - NodeType type() const override { - return NodeType::BackgroundBlurStyle; - } - - LayerStyleType layerStyleType() const override { - return LayerStyleType::BackgroundBlurStyle; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/BlendFilter.h b/pagx/include/pagx/nodes/BlendFilter.h deleted file mode 100644 index 42dad0d306..0000000000 --- a/pagx/include/pagx/nodes/BlendFilter.h +++ /dev/null @@ -1,44 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/LayerFilter.h" -#include "pagx/types/BlendMode.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Blend filter. - */ -class BlendFilter : public LayerFilter { - public: - Color color = {}; - BlendMode blendMode = BlendMode::Normal; - - NodeType type() const override { - return NodeType::BlendFilter; - } - - LayerFilterType layerFilterType() const override { - return LayerFilterType::BlendFilter; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/BlurFilter.h b/pagx/include/pagx/nodes/BlurFilter.h deleted file mode 100644 index 825c0a9e67..0000000000 --- a/pagx/include/pagx/nodes/BlurFilter.h +++ /dev/null @@ -1,44 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/LayerFilter.h" -#include "pagx/types/TileMode.h" - -namespace pagx { - -/** - * Blur filter. - */ -class BlurFilter : public LayerFilter { - public: - float blurrinessX = 0; - float blurrinessY = 0; - TileMode tileMode = TileMode::Decal; - - NodeType type() const override { - return NodeType::BlurFilter; - } - - LayerFilterType layerFilterType() const override { - return LayerFilterType::BlurFilter; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/ColorMatrixFilter.h b/pagx/include/pagx/nodes/ColorMatrixFilter.h deleted file mode 100644 index 82be2947b7..0000000000 --- a/pagx/include/pagx/nodes/ColorMatrixFilter.h +++ /dev/null @@ -1,42 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include "pagx/nodes/LayerFilter.h" - -namespace pagx { - -/** - * Color matrix filter. - */ -class ColorMatrixFilter : public LayerFilter { - public: - std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; - - NodeType type() const override { - return NodeType::ColorMatrixFilter; - } - - LayerFilterType layerFilterType() const override { - return LayerFilterType::ColorMatrixFilter; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/ColorSource.h b/pagx/include/pagx/nodes/ColorSource.h deleted file mode 100644 index f351244ac8..0000000000 --- a/pagx/include/pagx/nodes/ColorSource.h +++ /dev/null @@ -1,56 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/Node.h" - -namespace pagx { - -/** - * Color source types. - */ -enum class ColorSourceType { - SolidColor, - LinearGradient, - RadialGradient, - ConicGradient, - DiamondGradient, - ImagePattern -}; - -/** - * Returns the string name of a color source type. - */ -const char* ColorSourceTypeName(ColorSourceType type); - -/** - * Base class for color sources (SolidColor, gradients, ImagePattern). - */ -class ColorSource : public Node { - public: - /** - * Returns the color source type of this color source. - */ - virtual ColorSourceType colorSourceType() const = 0; - - protected: - ColorSource() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/ColorStop.h b/pagx/include/pagx/nodes/ColorStop.h deleted file mode 100644 index 3c4d39dc04..0000000000 --- a/pagx/include/pagx/nodes/ColorStop.h +++ /dev/null @@ -1,34 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * A color stop in a gradient. - */ -class ColorStop { - public: - float offset = 0; - Color color = {}; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Composition.h b/pagx/include/pagx/nodes/Composition.h deleted file mode 100644 index 2eef5fcffd..0000000000 --- a/pagx/include/pagx/nodes/Composition.h +++ /dev/null @@ -1,45 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include "pagx/nodes/Node.h" - -namespace pagx { - -class Layer; - -/** - * Composition resource. - */ -class Composition : public Node { - public: - std::string id = {}; - float width = 0; - float height = 0; - std::vector> layers = {}; - - NodeType type() const override { - return NodeType::Composition; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/ConicGradient.h b/pagx/include/pagx/nodes/ConicGradient.h deleted file mode 100644 index 65713dd135..0000000000 --- a/pagx/include/pagx/nodes/ConicGradient.h +++ /dev/null @@ -1,50 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/ColorStop.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * A conic (sweep) gradient. - */ -class ConicGradient : public ColorSource { - public: - std::string id = {}; - Point center = {}; - float startAngle = 0; - float endAngle = 360; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::ConicGradient; - } - - ColorSourceType colorSourceType() const override { - return ColorSourceType::ConicGradient; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/DiamondGradient.h b/pagx/include/pagx/nodes/DiamondGradient.h deleted file mode 100644 index 1acecab9a1..0000000000 --- a/pagx/include/pagx/nodes/DiamondGradient.h +++ /dev/null @@ -1,49 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/ColorStop.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * A diamond gradient. - */ -class DiamondGradient : public ColorSource { - public: - std::string id = {}; - Point center = {}; - float halfDiagonal = 0; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::DiamondGradient; - } - - ColorSourceType colorSourceType() const override { - return ColorSourceType::DiamondGradient; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/DropShadowFilter.h b/pagx/include/pagx/nodes/DropShadowFilter.h deleted file mode 100644 index c2df906fd2..0000000000 --- a/pagx/include/pagx/nodes/DropShadowFilter.h +++ /dev/null @@ -1,47 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/LayerFilter.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Drop shadow filter. - */ -class DropShadowFilter : public LayerFilter { - public: - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - bool shadowOnly = false; - - NodeType type() const override { - return NodeType::DropShadowFilter; - } - - LayerFilterType layerFilterType() const override { - return LayerFilterType::DropShadowFilter; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/DropShadowStyle.h b/pagx/include/pagx/nodes/DropShadowStyle.h deleted file mode 100644 index 8dd0bc74c2..0000000000 --- a/pagx/include/pagx/nodes/DropShadowStyle.h +++ /dev/null @@ -1,49 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/LayerStyle.h" -#include "pagx/types/BlendMode.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Drop shadow style. - */ -class DropShadowStyle : public LayerStyle { - public: - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - bool showBehindLayer = true; - BlendMode blendMode = BlendMode::Normal; - - NodeType type() const override { - return NodeType::DropShadowStyle; - } - - LayerStyleType layerStyleType() const override { - return LayerStyleType::DropShadowStyle; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Ellipse.h b/pagx/include/pagx/nodes/Ellipse.h deleted file mode 100644 index 39dbe439f2..0000000000 --- a/pagx/include/pagx/nodes/Ellipse.h +++ /dev/null @@ -1,55 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Ellipse represents an ellipse shape defined by a center point and size. - */ -class Ellipse : public VectorElement { - public: - /** - * The center point of the ellipse. - */ - Point center = {}; - - /** - * The size of the ellipse. The default value is {100, 100}. - */ - Size size = {100, 100}; - - /** - * Whether the path direction is reversed. The default value is false. - */ - bool reversed = false; - - NodeType type() const override { - return NodeType::Ellipse; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::Ellipse; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Fill.h b/pagx/include/pagx/nodes/Fill.h deleted file mode 100644 index 7de9bbc2a9..0000000000 --- a/pagx/include/pagx/nodes/Fill.h +++ /dev/null @@ -1,81 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/BlendMode.h" -#include "pagx/types/FillRule.h" -#include "pagx/types/Placement.h" - -namespace pagx { - -/** - * Fill represents a fill painter that fills shapes with a solid color, gradient, or pattern. The - * color can be specified as a simple color string (e.g., "#FF0000"), a reference to a defined - * color source (e.g., "#gradientId"), or an inline ColorSource node. - */ -class Fill : public VectorElement { - public: - /** - * The fill color as a string. Can be a hex color (e.g., "#FF0000"), a reference to a color - * source (e.g., "#gradientId"), or empty if colorSource is used. - */ - std::string color = {}; - - /** - * An inline color source node (SolidColor, LinearGradient, etc.) for complex fills. If provided, - * this takes precedence over the color string. - */ - std::unique_ptr colorSource = nullptr; - - /** - * The opacity of the fill, ranging from 0 (transparent) to 1 (opaque). The default value is 1. - */ - float alpha = 1; - - /** - * The blend mode used when compositing the fill. The default value is Normal. - */ - BlendMode blendMode = BlendMode::Normal; - - /** - * The fill rule that determines the interior of self-intersecting paths. The default value is - * Winding. - */ - FillRule fillRule = FillRule::Winding; - - /** - * The placement of the fill relative to strokes (Background or Foreground). The default value is - * Background. - */ - Placement placement = Placement::Background; - - NodeType type() const override { - return NodeType::Fill; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::Fill; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Group.h b/pagx/include/pagx/nodes/Group.h deleted file mode 100644 index 70a3695288..0000000000 --- a/pagx/include/pagx/nodes/Group.h +++ /dev/null @@ -1,84 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Group is a container that groups multiple vector elements with its own transform. It can contain - * shapes, painters, modifiers, and nested groups, allowing for hierarchical organization of - * content. - */ -class Group : public VectorElement { - public: - /** - * The anchor point for transformations. - */ - Point anchorPoint = {}; - - /** - * The position offset of the group. - */ - Point position = {}; - - /** - * The rotation angle in degrees. The default value is 0. - */ - float rotation = 0; - - /** - * The scale factor as (scaleX, scaleY). The default value is {1, 1}. - */ - Point scale = {1, 1}; - - /** - * The skew angle in degrees. The default value is 0. - */ - float skew = 0; - - /** - * The axis angle in degrees for the skew transformation. The default value is 0. - */ - float skewAxis = 0; - - /** - * The opacity of the group, ranging from 0 to 1. The default value is 1. - */ - float alpha = 1; - - /** - * The child elements contained in this group. - */ - std::vector> elements = {}; - - NodeType type() const override { - return NodeType::Group; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::Group; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Image.h b/pagx/include/pagx/nodes/Image.h deleted file mode 100644 index cda487dc45..0000000000 --- a/pagx/include/pagx/nodes/Image.h +++ /dev/null @@ -1,39 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include "pagx/nodes/Node.h" - -namespace pagx { - -/** - * Image resource. - */ -class Image : public Node { - public: - std::string id = {}; - std::string source = {}; - - NodeType type() const override { - return NodeType::Image; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/ImagePattern.h b/pagx/include/pagx/nodes/ImagePattern.h deleted file mode 100644 index 612227dc32..0000000000 --- a/pagx/include/pagx/nodes/ImagePattern.h +++ /dev/null @@ -1,50 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include "pagx/nodes/ColorSource.h" -#include "pagx/types/SamplingMode.h" -#include "pagx/types/TileMode.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * An image pattern. - */ -class ImagePattern : public ColorSource { - public: - std::string id = {}; - std::string image = {}; - TileMode tileModeX = TileMode::Clamp; - TileMode tileModeY = TileMode::Clamp; - SamplingMode sampling = SamplingMode::Linear; - Matrix matrix = {}; - - NodeType type() const override { - return NodeType::ImagePattern; - } - - ColorSourceType colorSourceType() const override { - return ColorSourceType::ImagePattern; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/InnerShadowFilter.h b/pagx/include/pagx/nodes/InnerShadowFilter.h deleted file mode 100644 index 06bdf6b139..0000000000 --- a/pagx/include/pagx/nodes/InnerShadowFilter.h +++ /dev/null @@ -1,47 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/LayerFilter.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Inner shadow filter. - */ -class InnerShadowFilter : public LayerFilter { - public: - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - bool shadowOnly = false; - - NodeType type() const override { - return NodeType::InnerShadowFilter; - } - - LayerFilterType layerFilterType() const override { - return LayerFilterType::InnerShadowFilter; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/InnerShadowStyle.h b/pagx/include/pagx/nodes/InnerShadowStyle.h deleted file mode 100644 index 3e717db10e..0000000000 --- a/pagx/include/pagx/nodes/InnerShadowStyle.h +++ /dev/null @@ -1,48 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/LayerStyle.h" -#include "pagx/types/BlendMode.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Inner shadow style. - */ -class InnerShadowStyle : public LayerStyle { - public: - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; - Color color = {}; - BlendMode blendMode = BlendMode::Normal; - - NodeType type() const override { - return NodeType::InnerShadowStyle; - } - - LayerStyleType layerStyleType() const override { - return LayerStyleType::InnerShadowStyle; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Layer.h b/pagx/include/pagx/nodes/Layer.h deleted file mode 100644 index dbb26d201c..0000000000 --- a/pagx/include/pagx/nodes/Layer.h +++ /dev/null @@ -1,167 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include -#include "pagx/nodes/LayerFilter.h" -#include "pagx/nodes/LayerStyle.h" -#include "pagx/nodes/Node.h" -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/BlendMode.h" -#include "pagx/types/MaskType.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Layer represents a layer node that can contain vector elements, layer styles, filters, and child - * layers. It is the main building block for composing visual content in a PAGX document. - */ -class Layer : public Node { - public: - /** - * The unique identifier of the layer. - */ - std::string id = {}; - - /** - * The display name of the layer. - */ - std::string name = {}; - - /** - * Whether the layer is visible. The default value is true. - */ - bool visible = true; - - /** - * The opacity of the layer, ranging from 0 to 1. The default value is 1. - */ - float alpha = 1; - - /** - * The blend mode used when compositing the layer. The default value is Normal. - */ - BlendMode blendMode = BlendMode::Normal; - - /** - * The x-coordinate of the layer position. The default value is 0. - */ - float x = 0; - - /** - * The y-coordinate of the layer position. The default value is 0. - */ - float y = 0; - - /** - * The 2D transformation matrix of the layer. - */ - Matrix matrix = {}; - - /** - * The 3D transformation matrix as a 16-element array in column-major order. - */ - std::vector matrix3D = {}; - - /** - * Whether to preserve 3D transformations for child layers. The default value is false. - */ - bool preserve3D = false; - - /** - * Whether to apply anti-aliasing to the layer edges. The default value is true. - */ - bool antiAlias = true; - - /** - * Whether to use group opacity mode for the layer and its children. The default value is false. - */ - bool groupOpacity = false; - - /** - * Whether layer effects pass through to the background. The default value is true. - */ - bool passThroughBackground = true; - - /** - * Whether to exclude child effects when applying layer styles. The default value is false. - */ - bool excludeChildEffectsInLayerStyle = false; - - /** - * The scroll rectangle for clipping the layer content. - */ - Rect scrollRect = {}; - - /** - * Whether a scroll rectangle is applied to the layer. The default value is false. - */ - bool hasScrollRect = false; - - /** - * A reference to a mask layer (e.g., "#maskId"). - */ - std::string mask = {}; - - /** - * The type of masking to apply (Alpha, Luminosity, InvertedAlpha, or InvertedLuminosity). The - * default value is Alpha. - */ - MaskType maskType = MaskType::Alpha; - - /** - * A reference to a composition (e.g., "#compositionId") used as the layer content. - */ - std::string composition = {}; - - /** - * The vector elements contained in this layer (shapes, painters, modifiers, etc.). - */ - std::vector> contents = {}; - - /** - * The layer styles applied to this layer (drop shadow, inner shadow, etc.). - */ - std::vector> styles = {}; - - /** - * The filters applied to this layer (blur, color matrix, etc.). - */ - std::vector> filters = {}; - - /** - * The child layers contained in this layer. - */ - std::vector> children = {}; - - /** - * Custom data from SVG data-* attributes. The keys are stored without the "data-" prefix. - */ - std::unordered_map customData = {}; - - NodeType type() const override { - return NodeType::Layer; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/LayerFilter.h b/pagx/include/pagx/nodes/LayerFilter.h deleted file mode 100644 index 7b5e076a1b..0000000000 --- a/pagx/include/pagx/nodes/LayerFilter.h +++ /dev/null @@ -1,55 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/Node.h" - -namespace pagx { - -/** - * Layer filter types. - */ -enum class LayerFilterType { - BlurFilter, - DropShadowFilter, - InnerShadowFilter, - BlendFilter, - ColorMatrixFilter -}; - -/** - * Returns the string name of a layer filter type. - */ -const char* LayerFilterTypeName(LayerFilterType type); - -/** - * Base class for layer filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, ColorMatrixFilter). - */ -class LayerFilter : public Node { - public: - /** - * Returns the layer filter type of this layer filter. - */ - virtual LayerFilterType layerFilterType() const = 0; - - protected: - LayerFilter() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/LayerStyle.h b/pagx/include/pagx/nodes/LayerStyle.h deleted file mode 100644 index f737bcc11d..0000000000 --- a/pagx/include/pagx/nodes/LayerStyle.h +++ /dev/null @@ -1,53 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/Node.h" - -namespace pagx { - -/** - * Layer style types. - */ -enum class LayerStyleType { - DropShadowStyle, - InnerShadowStyle, - BackgroundBlurStyle -}; - -/** - * Returns the string name of a layer style type. - */ -const char* LayerStyleTypeName(LayerStyleType type); - -/** - * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle). - */ -class LayerStyle : public Node { - public: - /** - * Returns the layer style type of this layer style. - */ - virtual LayerStyleType layerStyleType() const = 0; - - protected: - LayerStyle() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/LinearGradient.h b/pagx/include/pagx/nodes/LinearGradient.h deleted file mode 100644 index bcc343fe32..0000000000 --- a/pagx/include/pagx/nodes/LinearGradient.h +++ /dev/null @@ -1,49 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/ColorStop.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * A linear gradient. - */ -class LinearGradient : public ColorSource { - public: - std::string id = {}; - Point startPoint = {}; - Point endPoint = {}; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::LinearGradient; - } - - ColorSourceType colorSourceType() const override { - return ColorSourceType::LinearGradient; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/MergePath.h b/pagx/include/pagx/nodes/MergePath.h deleted file mode 100644 index b73ffd60d9..0000000000 --- a/pagx/include/pagx/nodes/MergePath.h +++ /dev/null @@ -1,46 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/MergePathMode.h" - -namespace pagx { - -/** - * MergePath is a path modifier that merges multiple paths using boolean operations. It can append, - * add, subtract, intersect, or exclude paths from each other. - */ -class MergePath : public VectorElement { - public: - /** - * The merge mode that determines how paths are combined. The default value is Append. - */ - MergePathMode mode = MergePathMode::Append; - - NodeType type() const override { - return NodeType::MergePath; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::MergePath; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Node.h b/pagx/include/pagx/nodes/Node.h deleted file mode 100644 index 38b64d9185..0000000000 --- a/pagx/include/pagx/nodes/Node.h +++ /dev/null @@ -1,106 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -namespace pagx { - -/** - * Node types in PAGX document. - */ -enum class NodeType { - // Color sources - SolidColor, - LinearGradient, - RadialGradient, - ConicGradient, - DiamondGradient, - ImagePattern, - ColorStop, - - // Geometry elements - Rectangle, - Ellipse, - Polystar, - Path, - TextSpan, - - // Painters - Fill, - Stroke, - - // Shape modifiers - TrimPath, - RoundCorner, - MergePath, - - // Text modifiers - TextModifier, - TextPath, - TextLayout, - RangeSelector, - - // Repeater - Repeater, - - // Container - Group, - - // Layer styles - DropShadowStyle, - InnerShadowStyle, - BackgroundBlurStyle, - - // Layer filters - BlurFilter, - DropShadowFilter, - InnerShadowFilter, - BlendFilter, - ColorMatrixFilter, - - // Resources - Image, - PathData, - Composition, - - // Layer - Layer -}; - -/** - * Returns the string name of a node type. - */ -const char* NodeTypeName(NodeType type); - -/** - * Base class for all PAGX nodes. - */ -class Node { - public: - virtual ~Node() = default; - - /** - * Returns the type of this node. - */ - virtual NodeType type() const = 0; - - protected: - Node() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Path.h b/pagx/include/pagx/nodes/Path.h deleted file mode 100644 index 4d589ff88f..0000000000 --- a/pagx/include/pagx/nodes/Path.h +++ /dev/null @@ -1,51 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/PathData.h" -#include "pagx/nodes/VectorElement.h" - -namespace pagx { - -/** - * Path represents a freeform shape defined by a PathData containing vertices, in-tangents, and - * out-tangents. - */ -class Path : public VectorElement { - public: - /** - * The path data containing vertices and control points. - */ - PathData data = {}; - - /** - * Whether the path direction is reversed. The default value is false. - */ - bool reversed = false; - - NodeType type() const override { - return NodeType::Path; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::Path; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/PathDataResource.h b/pagx/include/pagx/nodes/PathDataResource.h deleted file mode 100644 index 8c1ecfb14a..0000000000 --- a/pagx/include/pagx/nodes/PathDataResource.h +++ /dev/null @@ -1,39 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include "pagx/nodes/Node.h" - -namespace pagx { - -/** - * PathData resource - stores reusable path data. - */ -class PathDataResource : public Node { - public: - std::string id = {}; - std::string data = {}; // SVG path data string - - NodeType type() const override { - return NodeType::PathData; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Polystar.h b/pagx/include/pagx/nodes/Polystar.h deleted file mode 100644 index a751cc758e..0000000000 --- a/pagx/include/pagx/nodes/Polystar.h +++ /dev/null @@ -1,88 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/PolystarType.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Polystar represents a polygon or star shape with configurable points, radii, and roundness. - */ -class Polystar : public VectorElement { - public: - /** - * The center point of the polystar. - */ - Point center = {}; - - /** - * The type of polystar shape, either Star or Polygon. The default value is Star. - */ - PolystarType polystarType = PolystarType::Star; - - /** - * The number of points in the polystar. The default value is 5. - */ - float pointCount = 5; - - /** - * The outer radius of the polystar. The default value is 100. - */ - float outerRadius = 100; - - /** - * The inner radius of the polystar. Only applies when polystarType is Star. The default value - * is 50. - */ - float innerRadius = 50; - - /** - * The rotation angle in degrees. The default value is 0. - */ - float rotation = 0; - - /** - * The roundness of the outer points, ranging from 0 to 100. The default value is 0. - */ - float outerRoundness = 0; - - /** - * The roundness of the inner points, ranging from 0 to 100. Only applies when polystarType is - * Star. The default value is 0. - */ - float innerRoundness = 0; - - /** - * Whether the path direction is reversed. The default value is false. - */ - bool reversed = false; - - NodeType type() const override { - return NodeType::Polystar; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::Polystar; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/RadialGradient.h b/pagx/include/pagx/nodes/RadialGradient.h deleted file mode 100644 index 08f1eb45fa..0000000000 --- a/pagx/include/pagx/nodes/RadialGradient.h +++ /dev/null @@ -1,49 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/ColorStop.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * A radial gradient. - */ -class RadialGradient : public ColorSource { - public: - std::string id = {}; - Point center = {}; - float radius = 0; - Matrix matrix = {}; - std::vector colorStops = {}; - - NodeType type() const override { - return NodeType::RadialGradient; - } - - ColorSourceType colorSourceType() const override { - return ColorSourceType::RadialGradient; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/RangeSelector.h b/pagx/include/pagx/nodes/RangeSelector.h deleted file mode 100644 index 46787659c1..0000000000 --- a/pagx/include/pagx/nodes/RangeSelector.h +++ /dev/null @@ -1,45 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/types/SelectorMode.h" -#include "pagx/types/SelectorShape.h" -#include "pagx/types/SelectorUnit.h" - -namespace pagx { - -/** - * Range selector for text modifier. - */ -class RangeSelector { - public: - float start = 0; - float end = 1; - float offset = 0; - SelectorUnit unit = SelectorUnit::Percentage; - SelectorShape shape = SelectorShape::Square; - float easeIn = 0; - float easeOut = 0; - SelectorMode mode = SelectorMode::Add; - float weight = 1; - bool randomizeOrder = false; - int randomSeed = 0; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Rectangle.h b/pagx/include/pagx/nodes/Rectangle.h deleted file mode 100644 index 33f5f1c1b3..0000000000 --- a/pagx/include/pagx/nodes/Rectangle.h +++ /dev/null @@ -1,60 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Rectangle represents a rectangle shape with optional rounded corners. - */ -class Rectangle : public VectorElement { - public: - /** - * The center point of the rectangle. - */ - Point center = {}; - - /** - * The size of the rectangle. The default value is {100, 100}. - */ - Size size = {100, 100}; - - /** - * The corner roundness of the rectangle, ranging from 0 to 100. The default value is 0. - */ - float roundness = 0; - - /** - * Whether the path direction is reversed. The default value is false. - */ - bool reversed = false; - - NodeType type() const override { - return NodeType::Rectangle; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::Rectangle; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Repeater.h b/pagx/include/pagx/nodes/Repeater.h deleted file mode 100644 index f0f11f17c9..0000000000 --- a/pagx/include/pagx/nodes/Repeater.h +++ /dev/null @@ -1,89 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/RepeaterOrder.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * Repeater is a modifier that creates multiple copies of preceding elements with progressive - * transformations. Each copy can have an incremental offset in position, rotation, scale, and - * opacity. - */ -class Repeater : public VectorElement { - public: - /** - * The number of copies to create. The default value is 3. - */ - float copies = 3; - - /** - * The offset applied to the copy index, allowing fractional copies. The default value is 0. - */ - float offset = 0; - - /** - * The stacking order of copies (BelowOriginal or AboveOriginal). The default value is - * BelowOriginal. - */ - RepeaterOrder order = RepeaterOrder::BelowOriginal; - - /** - * The anchor point for transformations. - */ - Point anchorPoint = {}; - - /** - * The position offset applied between each copy. The default value is {100, 100}. - */ - Point position = {100, 100}; - - /** - * The rotation angle in degrees applied between each copy. The default value is 0. - */ - float rotation = 0; - - /** - * The scale factor applied between each copy. The default value is {1, 1}. - */ - Point scale = {1, 1}; - - /** - * The starting opacity for the first copy, ranging from 0 to 1. The default value is 1. - */ - float startAlpha = 1; - - /** - * The ending opacity for the last copy, ranging from 0 to 1. The default value is 1. - */ - float endAlpha = 1; - - NodeType type() const override { - return NodeType::Repeater; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::Repeater; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/RoundCorner.h b/pagx/include/pagx/nodes/RoundCorner.h deleted file mode 100644 index a86000bc4e..0000000000 --- a/pagx/include/pagx/nodes/RoundCorner.h +++ /dev/null @@ -1,45 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/VectorElement.h" - -namespace pagx { - -/** - * RoundCorner is a path modifier that rounds the corners of shapes by adding smooth curves at - * sharp vertices. - */ -class RoundCorner : public VectorElement { - public: - /** - * The radius of the rounded corners in pixels. The default value is 10. - */ - float radius = 10; - - NodeType type() const override { - return NodeType::RoundCorner; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::RoundCorner; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/SolidColor.h b/pagx/include/pagx/nodes/SolidColor.h deleted file mode 100644 index 1101e2cd7f..0000000000 --- a/pagx/include/pagx/nodes/SolidColor.h +++ /dev/null @@ -1,44 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include "pagx/nodes/ColorSource.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * A solid color. - */ -class SolidColor : public ColorSource { - public: - std::string id = {}; - Color color = {}; - - NodeType type() const override { - return NodeType::SolidColor; - } - - ColorSourceType colorSourceType() const override { - return ColorSourceType::SolidColor; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/Stroke.h b/pagx/include/pagx/nodes/Stroke.h deleted file mode 100644 index 3128359c4d..0000000000 --- a/pagx/include/pagx/nodes/Stroke.h +++ /dev/null @@ -1,113 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/BlendMode.h" -#include "pagx/types/LineCap.h" -#include "pagx/types/LineJoin.h" -#include "pagx/types/Placement.h" -#include "pagx/types/StrokeAlign.h" - -namespace pagx { - -/** - * Stroke represents a stroke painter that outlines shapes with a solid color, gradient, or - * pattern. It supports various line cap styles, join styles, dash patterns, and stroke alignment. - */ -class Stroke : public VectorElement { - public: - /** - * The stroke color as a string. Can be a hex color (e.g., "#FF0000"), a reference to a color - * source (e.g., "#gradientId"), or empty if colorSource is used. - */ - std::string color = {}; - - /** - * An inline color source node (SolidColor, LinearGradient, etc.) for complex strokes. If - * provided, this takes precedence over the color string. - */ - std::unique_ptr colorSource = nullptr; - - /** - * The stroke width in pixels. The default value is 1. - */ - float width = 1; - - /** - * The opacity of the stroke, ranging from 0 (transparent) to 1 (opaque). The default value is 1. - */ - float alpha = 1; - - /** - * The blend mode used when compositing the stroke. The default value is Normal. - */ - BlendMode blendMode = BlendMode::Normal; - - /** - * The line cap style at the endpoints of open paths. The default value is Butt. - */ - LineCap cap = LineCap::Butt; - - /** - * The line join style at path corners. The default value is Miter. - */ - LineJoin join = LineJoin::Miter; - - /** - * The limit for miter joins before they are beveled. The default value is 4. - */ - float miterLimit = 4; - - /** - * The dash pattern as an array of dash and gap lengths. An empty array means a solid line. - */ - std::vector dashes = {}; - - /** - * The offset into the dash pattern. The default value is 0. - */ - float dashOffset = 0; - - /** - * The alignment of the stroke relative to the path (Center, Inner, or Outer). The default value - * is Center. - */ - StrokeAlign align = StrokeAlign::Center; - - /** - * The placement of the stroke relative to fills (Background or Foreground). The default value is - * Background. - */ - Placement placement = Placement::Background; - - NodeType type() const override { - return NodeType::Stroke; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::Stroke; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/TextLayout.h b/pagx/include/pagx/nodes/TextLayout.h deleted file mode 100644 index aa71723088..0000000000 --- a/pagx/include/pagx/nodes/TextLayout.h +++ /dev/null @@ -1,79 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/Overflow.h" -#include "pagx/types/TextAlign.h" -#include "pagx/types/VerticalAlign.h" - -namespace pagx { - -/** - * TextLayout is a text animator that controls text layout within a bounding box. It provides - * options for text alignment, line height, indentation, and overflow handling. - */ -class TextLayout : public VectorElement { - public: - /** - * The width of the text box in pixels. A value of 0 means auto-width. The default value is 0. - */ - float width = 0; - - /** - * The height of the text box in pixels. A value of 0 means auto-height. The default value is 0. - */ - float height = 0; - - /** - * The horizontal text alignment (Left, Center, Right, or Justify). The default value is Left. - */ - TextAlign textAlign = TextAlign::Left; - - /** - * The vertical text alignment (Top, Middle, or Bottom). The default value is Top. - */ - VerticalAlign verticalAlign = VerticalAlign::Top; - - /** - * The line height multiplier. The default value is 1.2. - */ - float lineHeight = 1.2f; - - /** - * The first-line indent in pixels. The default value is 0. - */ - float indent = 0; - - /** - * The overflow behavior when text exceeds the bounding box (Clip, Visible, or Scroll). The - * default value is Clip. - */ - Overflow overflow = Overflow::Clip; - - NodeType type() const override { - return NodeType::TextLayout; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::TextLayout; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/TextModifier.h b/pagx/include/pagx/nodes/TextModifier.h deleted file mode 100644 index 96cfab1228..0000000000 --- a/pagx/include/pagx/nodes/TextModifier.h +++ /dev/null @@ -1,101 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/nodes/RangeSelector.h" -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/Types.h" - -namespace pagx { - -/** - * TextModifier is a text animator that modifies text appearance with range-based transformations. - * It applies transformations like position, rotation, scale, and color changes to selected ranges - * of characters. - */ -class TextModifier : public VectorElement { - public: - /** - * The anchor point for transformations. - */ - Point anchorPoint = {}; - - /** - * The position offset applied to selected characters. - */ - Point position = {}; - - /** - * The rotation angle in degrees applied to selected characters. The default value is 0. - */ - float rotation = 0; - - /** - * The scale factor applied to selected characters. The default value is {1, 1}. - */ - Point scale = {1, 1}; - - /** - * The skew angle in degrees applied to selected characters. The default value is 0. - */ - float skew = 0; - - /** - * The axis angle in degrees for the skew transformation. The default value is 0. - */ - float skewAxis = 0; - - /** - * The opacity applied to selected characters, ranging from 0 to 1. The default value is 1. - */ - float alpha = 1; - - /** - * The fill color override for selected characters as a hex color string. - */ - std::string fillColor = {}; - - /** - * The stroke color override for selected characters as a hex color string. - */ - std::string strokeColor = {}; - - /** - * The stroke width override for selected characters. A value of -1 means no override. The - * default value is -1. - */ - float strokeWidth = -1; - - /** - * The range selectors that determine which characters are affected by this modifier. - */ - std::vector rangeSelectors = {}; - - NodeType type() const override { - return NodeType::TextModifier; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::TextModifier; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/TextPath.h b/pagx/include/pagx/nodes/TextPath.h deleted file mode 100644 index 811d4fe1b7..0000000000 --- a/pagx/include/pagx/nodes/TextPath.h +++ /dev/null @@ -1,78 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/TextPathAlign.h" - -namespace pagx { - -/** - * TextPath is a text animator that places text along a path. It allows text to follow the contour - * of a referenced path shape. - */ -class TextPath : public VectorElement { - public: - /** - * A reference to the path shape (e.g., "#pathId") that the text follows. - */ - std::string path = {}; - - /** - * The alignment of text along the path (Start, Center, or Justify). The default value is Start. - */ - TextPathAlign pathAlign = TextPathAlign::Start; - - /** - * The margin from the start of the path in pixels. The default value is 0. - */ - float firstMargin = 0; - - /** - * The margin from the end of the path in pixels. The default value is 0. - */ - float lastMargin = 0; - - /** - * Whether characters are rotated to be perpendicular to the path. The default value is true. - */ - bool perpendicularToPath = true; - - /** - * Whether to reverse the direction of the path. The default value is false. - */ - bool reversed = false; - - /** - * Whether to force text alignment to the path even when it exceeds the path length. The default - * value is false. - */ - bool forceAlignment = false; - - NodeType type() const override { - return NodeType::TextPath; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::TextPath; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/TextSpan.h b/pagx/include/pagx/nodes/TextSpan.h deleted file mode 100644 index 5ab74dca89..0000000000 --- a/pagx/include/pagx/nodes/TextSpan.h +++ /dev/null @@ -1,87 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/FontStyle.h" - -namespace pagx { - -/** - * TextSpan represents a text span that generates glyph paths for rendering. It defines the text - * content, font properties, and positioning within a shape layer. - */ -class TextSpan : public VectorElement { - public: - /** - * The x-coordinate of the text baseline starting point. The default value is 0. - */ - float x = 0; - - /** - * The y-coordinate of the text baseline starting point. The default value is 0. - */ - float y = 0; - - /** - * The font family name. - */ - std::string font = {}; - - /** - * The font size in pixels. The default value is 12. - */ - float fontSize = 12; - - /** - * The font weight, ranging from 100 to 900. The default value is 400 (normal). - */ - int fontWeight = 400; - - /** - * The font style (Normal, Italic, or Oblique). The default value is Normal. - */ - FontStyle fontStyle = FontStyle::Normal; - - /** - * The tracking value that adjusts spacing between characters. The default value is 0. - */ - float tracking = 0; - - /** - * The baseline shift in pixels, positive values shift the text up. The default value is 0. - */ - float baselineShift = 0; - - /** - * The text content to render. - */ - std::string text = {}; - - NodeType type() const override { - return NodeType::TextSpan; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::TextSpan; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/TrimPath.h b/pagx/include/pagx/nodes/TrimPath.h deleted file mode 100644 index a6b347ef09..0000000000 --- a/pagx/include/pagx/nodes/TrimPath.h +++ /dev/null @@ -1,66 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/VectorElement.h" -#include "pagx/types/TrimType.h" - -namespace pagx { - -/** - * TrimPath is a path modifier that trims paths to a specified range. It can be used to animate - * path drawing or reveal effects by adjusting the start and end values. - */ -class TrimPath : public VectorElement { - public: - /** - * The starting point of the trim as a percentage of the path length, ranging from 0 to 1. The - * default value is 0. - */ - float start = 0; - - /** - * The ending point of the trim as a percentage of the path length, ranging from 0 to 1. The - * default value is 1. - */ - float end = 1; - - /** - * The offset to shift the trim range along the path, where 1 represents a full path length. The - * default value is 0. - */ - float offset = 0; - - /** - * The trim type that determines how multiple paths are trimmed. Separate trims each path - * individually, while Simultaneous trims all paths as one continuous path. The default value is - * Separate. - */ - TrimType trimType = TrimType::Separate; - - NodeType type() const override { - return NodeType::TrimPath; - } - - VectorElementType vectorElementType() const override { - return VectorElementType::TrimPath; - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/nodes/VectorElement.h b/pagx/include/pagx/nodes/VectorElement.h deleted file mode 100644 index 693b93e73f..0000000000 --- a/pagx/include/pagx/nodes/VectorElement.h +++ /dev/null @@ -1,114 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/nodes/Node.h" - -namespace pagx { - -/** - * VectorElementType enumerates all types of vector elements that can be placed in Layer.contents - * or Group.elements. - */ -enum class VectorElementType { - /** - * A rectangle shape with optional rounded corners. - */ - Rectangle, - /** - * An ellipse shape defined by center point and size. - */ - Ellipse, - /** - * A polygon or star shape with configurable points and roundness. - */ - Polystar, - /** - * A freeform path shape defined by a PathData. - */ - Path, - /** - * A text span that generates glyph paths for rendering. - */ - TextSpan, - /** - * A fill painter that fills shapes with a color or gradient. - */ - Fill, - /** - * A stroke painter that outlines shapes with a color or gradient. - */ - Stroke, - /** - * A path modifier that trims paths to a specified range. - */ - TrimPath, - /** - * A path modifier that rounds the corners of shapes. - */ - RoundCorner, - /** - * A path modifier that merges multiple paths using boolean operations. - */ - MergePath, - /** - * A text animator that modifies text appearance with range-based transformations. - */ - TextModifier, - /** - * A text animator that places text along a path. - */ - TextPath, - /** - * A text animator that controls text layout within a bounding box. - */ - TextLayout, - /** - * A container that groups multiple elements with its own transform. - */ - Group, - /** - * A modifier that creates multiple copies of preceding elements. - */ - Repeater -}; - -/** - * Returns the string name of a vector element type. - */ -const char* VectorElementTypeName(VectorElementType type); - -/** - * VectorElement is the base class for all vector elements in a shape layer. It includes shapes - * (Rectangle, Ellipse, Polystar, Path, TextSpan), painters (Fill, Stroke), modifiers (TrimPath, - * RoundCorner, MergePath), text elements (TextModifier, TextPath, TextLayout), and containers - * (Group, Repeater). - */ -class VectorElement : public Node { - public: - /** - * Returns the vector element type of this element. - */ - virtual VectorElementType vectorElementType() const = 0; - - protected: - VectorElement() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/types/BlendMode.h b/pagx/include/pagx/types/BlendMode.h deleted file mode 100644 index 10b90cbdcb..0000000000 --- a/pagx/include/pagx/types/BlendMode.h +++ /dev/null @@ -1,52 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Blend modes for compositing. - */ -enum class BlendMode { - Normal, - Multiply, - Screen, - Overlay, - Darken, - Lighten, - ColorDodge, - ColorBurn, - HardLight, - SoftLight, - Difference, - Exclusion, - Hue, - Saturation, - Color, - Luminosity, - PlusLighter, - PlusDarker -}; - -std::string BlendModeToString(BlendMode mode); -BlendMode BlendModeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/Color.h b/pagx/include/pagx/types/Color.h deleted file mode 100644 index 04eaf24c27..0000000000 --- a/pagx/include/pagx/types/Color.h +++ /dev/null @@ -1,170 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -namespace pagx { - -namespace detail { -inline int ParseHexDigit(char c) { - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'a' && c <= 'f') { - return c - 'a' + 10; - } - if (c >= 'A' && c <= 'F') { - return c - 'A' + 10; - } - return 0; -} -} // namespace detail - -/** - * An RGBA color with floating-point components in [0, 1]. - */ -struct Color { - float red = 0; - float green = 0; - float blue = 0; - float alpha = 1; - - /** - * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). - */ - static Color FromHex(uint32_t hex, bool hasAlpha = false) { - Color color = {}; - if (hasAlpha) { - color.red = static_cast((hex >> 24) & 0xFF) / 255.0f; - color.green = static_cast((hex >> 16) & 0xFF) / 255.0f; - color.blue = static_cast((hex >> 8) & 0xFF) / 255.0f; - color.alpha = static_cast(hex & 0xFF) / 255.0f; - } else { - color.red = static_cast((hex >> 16) & 0xFF) / 255.0f; - color.green = static_cast((hex >> 8) & 0xFF) / 255.0f; - color.blue = static_cast(hex & 0xFF) / 255.0f; - color.alpha = 1.0f; - } - return color; - } - - /** - * Returns a Color from RGBA components in [0, 1]. - */ - static Color FromRGBA(float r, float g, float b, float a = 1) { - return {r, g, b, a}; - } - - /** - * Parses a color string. Supports: - * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" - * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" - * Returns black if parsing fails. - */ - static Color Parse(const std::string& str) { - if (str.empty()) { - return {}; - } - if (str[0] == '#') { - auto hex = str.substr(1); - if (hex.size() == 3) { - int r = detail::ParseHexDigit(hex[0]); - int g = detail::ParseHexDigit(hex[1]); - int b = detail::ParseHexDigit(hex[2]); - return Color::FromRGBA(static_cast(r * 17) / 255.0f, - static_cast(g * 17) / 255.0f, - static_cast(b * 17) / 255.0f, 1.0f); - } - if (hex.size() == 6) { - int r = detail::ParseHexDigit(hex[0]) * 16 + detail::ParseHexDigit(hex[1]); - int g = detail::ParseHexDigit(hex[2]) * 16 + detail::ParseHexDigit(hex[3]); - int b = detail::ParseHexDigit(hex[4]) * 16 + detail::ParseHexDigit(hex[5]); - return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, - static_cast(b) / 255.0f, 1.0f); - } - if (hex.size() == 8) { - int r = detail::ParseHexDigit(hex[0]) * 16 + detail::ParseHexDigit(hex[1]); - int g = detail::ParseHexDigit(hex[2]) * 16 + detail::ParseHexDigit(hex[3]); - int b = detail::ParseHexDigit(hex[4]) * 16 + detail::ParseHexDigit(hex[5]); - int a = detail::ParseHexDigit(hex[6]) * 16 + detail::ParseHexDigit(hex[7]); - return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, - static_cast(b) / 255.0f, static_cast(a) / 255.0f); - } - } - if (str.substr(0, 4) == "rgb(" || str.substr(0, 5) == "rgba(") { - auto start = str.find('('); - auto end = str.find(')'); - if (start != std::string::npos && end != std::string::npos) { - auto values = str.substr(start + 1, end - start - 1); - std::istringstream iss(values); - std::string token = {}; - std::vector components = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - components.push_back(std::stof(trimmed)); - } - if (components.size() >= 3) { - float r = components[0] / 255.0f; - float g = components[1] / 255.0f; - float b = components[2] / 255.0f; - float a = components.size() >= 4 ? components[3] : 1.0f; - return Color::FromRGBA(r, g, b, a); - } - } - } - return {}; - } - - /** - * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". - */ - std::string toHexString(bool includeAlpha = false) const { - auto toHex = [](float v) { - int i = static_cast(std::round(v * 255.0f)); - i = std::max(0, std::min(255, i)); - char buf[3] = {}; - snprintf(buf, sizeof(buf), "%02X", i); - return std::string(buf); - }; - std::string result = "#" + toHex(red) + toHex(green) + toHex(blue); - if (includeAlpha && alpha < 1.0f) { - result += toHex(alpha); - } - return result; - } - - bool operator==(const Color& other) const { - return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; - } - - bool operator!=(const Color& other) const { - return !(*this == other); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/types/Enums.h b/pagx/include/pagx/types/Enums.h deleted file mode 100644 index 1fbc598ac2..0000000000 --- a/pagx/include/pagx/types/Enums.h +++ /dev/null @@ -1,54 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -// Layer related -#include "pagx/types/BlendMode.h" -#include "pagx/types/MaskType.h" - -// Painter related -#include "pagx/types/FillRule.h" -#include "pagx/types/LineCap.h" -#include "pagx/types/LineJoin.h" -#include "pagx/types/Placement.h" -#include "pagx/types/StrokeAlign.h" - -// Color source related -#include "pagx/types/SamplingMode.h" -#include "pagx/types/TileMode.h" - -// Geometry related -#include "pagx/types/PolystarType.h" - -// Path modifier related -#include "pagx/types/MergePathMode.h" -#include "pagx/types/TrimType.h" - -// Text modifier related -#include "pagx/types/FontStyle.h" -#include "pagx/types/Overflow.h" -#include "pagx/types/SelectorMode.h" -#include "pagx/types/SelectorShape.h" -#include "pagx/types/SelectorUnit.h" -#include "pagx/types/TextAlign.h" -#include "pagx/types/TextPathAlign.h" -#include "pagx/types/VerticalAlign.h" - -// Repeater related -#include "pagx/types/RepeaterOrder.h" diff --git a/pagx/include/pagx/types/FillRule.h b/pagx/include/pagx/types/FillRule.h deleted file mode 100644 index 5ae1093101..0000000000 --- a/pagx/include/pagx/types/FillRule.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Fill rules for paths. - */ -enum class FillRule { - Winding, - EvenOdd -}; - -std::string FillRuleToString(FillRule rule); -FillRule FillRuleFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/FontStyle.h b/pagx/include/pagx/types/FontStyle.h deleted file mode 100644 index 59568b80ff..0000000000 --- a/pagx/include/pagx/types/FontStyle.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Font style. - */ -enum class FontStyle { - Normal, - Italic -}; - -std::string FontStyleToString(FontStyle style); -FontStyle FontStyleFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/LineCap.h b/pagx/include/pagx/types/LineCap.h deleted file mode 100644 index dfb779acca..0000000000 --- a/pagx/include/pagx/types/LineCap.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Line cap styles for strokes. - */ -enum class LineCap { - Butt, - Round, - Square -}; - -std::string LineCapToString(LineCap cap); -LineCap LineCapFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/LineJoin.h b/pagx/include/pagx/types/LineJoin.h deleted file mode 100644 index e6b12c6f57..0000000000 --- a/pagx/include/pagx/types/LineJoin.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Line join styles for strokes. - */ -enum class LineJoin { - Miter, - Round, - Bevel -}; - -std::string LineJoinToString(LineJoin join); -LineJoin LineJoinFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/MaskType.h b/pagx/include/pagx/types/MaskType.h deleted file mode 100644 index f274bd9a7c..0000000000 --- a/pagx/include/pagx/types/MaskType.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Mask types for layer masking. - */ -enum class MaskType { - Alpha, - Luminance, - Contour -}; - -std::string MaskTypeToString(MaskType type); -MaskType MaskTypeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/Matrix.h b/pagx/include/pagx/types/Matrix.h deleted file mode 100644 index 2d911d5a8c..0000000000 --- a/pagx/include/pagx/types/Matrix.h +++ /dev/null @@ -1,160 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include -#include -#include "pagx/types/Point.h" - -namespace pagx { - -/** - * A 2D affine transformation matrix. - * Matrix form: - * | a c tx | - * | b d ty | - * | 0 0 1 | - */ -struct Matrix { - float a = 1; // scaleX - float b = 0; // skewY - float c = 0; // skewX - float d = 1; // scaleY - float tx = 0; // transX - float ty = 0; // transY - - /** - * Returns the identity matrix. - */ - static Matrix Identity() { - return {}; - } - - /** - * Returns a translation matrix. - */ - static Matrix Translate(float x, float y) { - Matrix m = {}; - m.tx = x; - m.ty = y; - return m; - } - - /** - * Returns a scale matrix. - */ - static Matrix Scale(float sx, float sy) { - Matrix m = {}; - m.a = sx; - m.d = sy; - return m; - } - - /** - * Returns a rotation matrix (angle in degrees). - */ - static Matrix Rotate(float degrees) { - Matrix m = {}; - float radians = degrees * 3.14159265358979323846f / 180.0f; - float cosVal = std::cos(radians); - float sinVal = std::sin(radians); - m.a = cosVal; - m.b = sinVal; - m.c = -sinVal; - m.d = cosVal; - return m; - } - - /** - * Parses a matrix string "a,b,c,d,tx,ty". - */ - static Matrix Parse(const std::string& str) { - Matrix m = {}; - std::istringstream iss(str); - std::string token = {}; - std::vector values = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - values.push_back(std::stof(trimmed)); - } - } - if (values.size() >= 6) { - m.a = values[0]; - m.b = values[1]; - m.c = values[2]; - m.d = values[3]; - m.tx = values[4]; - m.ty = values[5]; - } - return m; - } - - /** - * Returns the matrix as a string "a,b,c,d,tx,ty". - */ - std::string toString() const { - std::ostringstream oss = {}; - oss << a << "," << b << "," << c << "," << d << "," << tx << "," << ty; - return oss.str(); - } - - /** - * Returns true if this is the identity matrix. - */ - bool isIdentity() const { - return a == 1 && b == 0 && c == 0 && d == 1 && tx == 0 && ty == 0; - } - - /** - * Concatenates this matrix with another. - */ - Matrix operator*(const Matrix& other) const { - Matrix result = {}; - result.a = a * other.a + c * other.b; - result.b = b * other.a + d * other.b; - result.c = a * other.c + c * other.d; - result.d = b * other.c + d * other.d; - result.tx = a * other.tx + c * other.ty + tx; - result.ty = b * other.tx + d * other.ty + ty; - return result; - } - - /** - * Transforms a point by this matrix. - */ - Point mapPoint(const Point& point) const { - return {a * point.x + c * point.y + tx, b * point.x + d * point.y + ty}; - } - - bool operator==(const Matrix& other) const { - return a == other.a && b == other.b && c == other.c && d == other.d && tx == other.tx && - ty == other.ty; - } - - bool operator!=(const Matrix& other) const { - return !(*this == other); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/types/MergePathMode.h b/pagx/include/pagx/types/MergePathMode.h deleted file mode 100644 index 94a17879ec..0000000000 --- a/pagx/include/pagx/types/MergePathMode.h +++ /dev/null @@ -1,39 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Path merge modes (boolean operations). - */ -enum class MergePathMode { - Append, - Union, - Intersect, - Xor, - Difference -}; - -std::string MergePathModeToString(MergePathMode mode); -MergePathMode MergePathModeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/Overflow.h b/pagx/include/pagx/types/Overflow.h deleted file mode 100644 index 4a6868d063..0000000000 --- a/pagx/include/pagx/types/Overflow.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Text overflow handling. - */ -enum class Overflow { - Clip, - Visible, - Ellipsis -}; - -std::string OverflowToString(Overflow overflow); -Overflow OverflowFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/Placement.h b/pagx/include/pagx/types/Placement.h deleted file mode 100644 index 2cb3aaf32b..0000000000 --- a/pagx/include/pagx/types/Placement.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Placement of fill/stroke relative to child layers. - */ -enum class Placement { - Background, - Foreground -}; - -std::string PlacementToString(Placement placement); -Placement PlacementFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/Point.h b/pagx/include/pagx/types/Point.h deleted file mode 100644 index e4996dcf21..0000000000 --- a/pagx/include/pagx/types/Point.h +++ /dev/null @@ -1,39 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -namespace pagx { - -/** - * A point with x and y coordinates. - */ -struct Point { - float x = 0; - float y = 0; - - bool operator==(const Point& other) const { - return x == other.x && y == other.y; - } - - bool operator!=(const Point& other) const { - return !(*this == other); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/types/PolystarType.h b/pagx/include/pagx/types/PolystarType.h deleted file mode 100644 index 5834dc05c2..0000000000 --- a/pagx/include/pagx/types/PolystarType.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Polystar types. - */ -enum class PolystarType { - Polygon, - Star -}; - -std::string PolystarTypeToString(PolystarType type); -PolystarType PolystarTypeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/Rect.h b/pagx/include/pagx/types/Rect.h deleted file mode 100644 index 5adea6206d..0000000000 --- a/pagx/include/pagx/types/Rect.h +++ /dev/null @@ -1,79 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -namespace pagx { - -/** - * A rectangle defined by position and size. - */ -struct Rect { - float x = 0; - float y = 0; - float width = 0; - float height = 0; - - /** - * Returns a Rect from left, top, right, bottom coordinates. - */ - static Rect MakeLTRB(float l, float t, float r, float b) { - return {l, t, r - l, b - t}; - } - - /** - * Returns a Rect from position and size. - */ - static Rect MakeXYWH(float x, float y, float w, float h) { - return {x, y, w, h}; - } - - float left() const { - return x; - } - - float top() const { - return y; - } - - float right() const { - return x + width; - } - - float bottom() const { - return y + height; - } - - bool isEmpty() const { - return width <= 0 || height <= 0; - } - - void setEmpty() { - x = y = width = height = 0; - } - - bool operator==(const Rect& other) const { - return x == other.x && y == other.y && width == other.width && height == other.height; - } - - bool operator!=(const Rect& other) const { - return !(*this == other); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/types/RepeaterOrder.h b/pagx/include/pagx/types/RepeaterOrder.h deleted file mode 100644 index e98b5a3439..0000000000 --- a/pagx/include/pagx/types/RepeaterOrder.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Repeater stacking order. - */ -enum class RepeaterOrder { - BelowOriginal, - AboveOriginal -}; - -std::string RepeaterOrderToString(RepeaterOrder order); -RepeaterOrder RepeaterOrderFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/SamplingMode.h b/pagx/include/pagx/types/SamplingMode.h deleted file mode 100644 index 270a5160e5..0000000000 --- a/pagx/include/pagx/types/SamplingMode.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Sampling modes for images. - */ -enum class SamplingMode { - Nearest, - Linear, - Mipmap -}; - -std::string SamplingModeToString(SamplingMode mode); -SamplingMode SamplingModeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/SelectorMode.h b/pagx/include/pagx/types/SelectorMode.h deleted file mode 100644 index c6794115ee..0000000000 --- a/pagx/include/pagx/types/SelectorMode.h +++ /dev/null @@ -1,40 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Range selector combination mode. - */ -enum class SelectorMode { - Add, - Subtract, - Intersect, - Min, - Max, - Difference -}; - -std::string SelectorModeToString(SelectorMode mode); -SelectorMode SelectorModeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/SelectorShape.h b/pagx/include/pagx/types/SelectorShape.h deleted file mode 100644 index e61239f4e8..0000000000 --- a/pagx/include/pagx/types/SelectorShape.h +++ /dev/null @@ -1,40 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Range selector shape. - */ -enum class SelectorShape { - Square, - RampUp, - RampDown, - Triangle, - Round, - Smooth -}; - -std::string SelectorShapeToString(SelectorShape shape); -SelectorShape SelectorShapeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/SelectorUnit.h b/pagx/include/pagx/types/SelectorUnit.h deleted file mode 100644 index 759da0682d..0000000000 --- a/pagx/include/pagx/types/SelectorUnit.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Range selector unit. - */ -enum class SelectorUnit { - Index, - Percentage -}; - -std::string SelectorUnitToString(SelectorUnit unit); -SelectorUnit SelectorUnitFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/Size.h b/pagx/include/pagx/types/Size.h deleted file mode 100644 index bb0ea3e110..0000000000 --- a/pagx/include/pagx/types/Size.h +++ /dev/null @@ -1,39 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -namespace pagx { - -/** - * A size with width and height. - */ -struct Size { - float width = 0; - float height = 0; - - bool operator==(const Size& other) const { - return width == other.width && height == other.height; - } - - bool operator!=(const Size& other) const { - return !(*this == other); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/types/StrokeAlign.h b/pagx/include/pagx/types/StrokeAlign.h deleted file mode 100644 index adccb2ed9f..0000000000 --- a/pagx/include/pagx/types/StrokeAlign.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Stroke alignment relative to path. - */ -enum class StrokeAlign { - Center, - Inside, - Outside -}; - -std::string StrokeAlignToString(StrokeAlign align); -StrokeAlign StrokeAlignFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/TextAlign.h b/pagx/include/pagx/types/TextAlign.h deleted file mode 100644 index 88ba8c64b2..0000000000 --- a/pagx/include/pagx/types/TextAlign.h +++ /dev/null @@ -1,38 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Text horizontal alignment. - */ -enum class TextAlign { - Left, - Center, - Right, - Justify -}; - -std::string TextAlignToString(TextAlign align); -TextAlign TextAlignFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/TextPathAlign.h b/pagx/include/pagx/types/TextPathAlign.h deleted file mode 100644 index 7957c8679b..0000000000 --- a/pagx/include/pagx/types/TextPathAlign.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Text path alignment. - */ -enum class TextPathAlign { - Start, - Center, - End -}; - -std::string TextPathAlignToString(TextPathAlign align); -TextPathAlign TextPathAlignFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/TileMode.h b/pagx/include/pagx/types/TileMode.h deleted file mode 100644 index 758865336c..0000000000 --- a/pagx/include/pagx/types/TileMode.h +++ /dev/null @@ -1,38 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Tile modes for patterns and gradients. - */ -enum class TileMode { - Clamp, - Repeat, - Mirror, - Decal -}; - -std::string TileModeToString(TileMode mode); -TileMode TileModeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/TrimType.h b/pagx/include/pagx/types/TrimType.h deleted file mode 100644 index 31b3aab19f..0000000000 --- a/pagx/include/pagx/types/TrimType.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Trim path types. - */ -enum class TrimType { - Separate, - Continuous -}; - -std::string TrimTypeToString(TrimType type); -TrimType TrimTypeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/types/Types.h b/pagx/include/pagx/types/Types.h deleted file mode 100644 index 3c17baaadf..0000000000 --- a/pagx/include/pagx/types/Types.h +++ /dev/null @@ -1,229 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include - -namespace pagx { - -/** - * A point with x and y coordinates. - */ -struct Point { - float x = 0; - float y = 0; - - bool operator==(const Point& other) const { - return x == other.x && y == other.y; - } - - bool operator!=(const Point& other) const { - return !(*this == other); - } -}; - -/** - * A size with width and height. - */ -struct Size { - float width = 0; - float height = 0; - - bool operator==(const Size& other) const { - return width == other.width && height == other.height; - } - - bool operator!=(const Size& other) const { - return !(*this == other); - } -}; - -/** - * A rectangle defined by position and size. - */ -struct Rect { - float x = 0; - float y = 0; - float width = 0; - float height = 0; - - /** - * Returns a Rect from left, top, right, bottom coordinates. - */ - static Rect MakeLTRB(float l, float t, float r, float b) { - return {l, t, r - l, b - t}; - } - - /** - * Returns a Rect from position and size. - */ - static Rect MakeXYWH(float x, float y, float w, float h) { - return {x, y, w, h}; - } - - float left() const { - return x; - } - - float top() const { - return y; - } - - float right() const { - return x + width; - } - - float bottom() const { - return y + height; - } - - bool isEmpty() const { - return width <= 0 || height <= 0; - } - - void setEmpty() { - x = y = width = height = 0; - } - - bool operator==(const Rect& other) const { - return x == other.x && y == other.y && width == other.width && height == other.height; - } - - bool operator!=(const Rect& other) const { - return !(*this == other); - } -}; - -/** - * An RGBA color with floating-point components in [0, 1]. - */ -struct Color { - float red = 0; - float green = 0; - float blue = 0; - float alpha = 1; - - /** - * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). - */ - static Color FromHex(uint32_t hex, bool hasAlpha = false); - - /** - * Returns a Color from RGBA components in [0, 1]. - */ - static Color FromRGBA(float r, float g, float b, float a = 1); - - /** - * Parses a color string. Supports: - * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" - * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" - * Returns black if parsing fails. - */ - static Color Parse(const std::string& str); - - /** - * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". - */ - std::string toHexString(bool includeAlpha = false) const; - - bool operator==(const Color& other) const { - return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; - } - - bool operator!=(const Color& other) const { - return !(*this == other); - } -}; - -/** - * A 2D affine transformation matrix. - * Matrix form: - * | a c tx | - * | b d ty | - * | 0 0 1 | - */ -struct Matrix { - float a = 1; // scaleX - float b = 0; // skewY - float c = 0; // skewX - float d = 1; // scaleY - float tx = 0; // transX - float ty = 0; // transY - - /** - * Returns the identity matrix. - */ - static Matrix Identity() { - return {}; - } - - /** - * Returns a translation matrix. - */ - static Matrix Translate(float x, float y); - - /** - * Returns a scale matrix. - */ - static Matrix Scale(float sx, float sy); - - /** - * Returns a rotation matrix (angle in degrees). - */ - static Matrix Rotate(float degrees); - - /** - * Parses a matrix string "a,b,c,d,tx,ty". - */ - static Matrix Parse(const std::string& str); - - /** - * Returns the matrix as a string "a,b,c,d,tx,ty". - */ - std::string toString() const; - - /** - * Returns true if this is the identity matrix. - */ - bool isIdentity() const { - return a == 1 && b == 0 && c == 0 && d == 1 && tx == 0 && ty == 0; - } - - /** - * Concatenates this matrix with another. - */ - Matrix operator*(const Matrix& other) const; - - /** - * Transforms a point by this matrix. - */ - Point mapPoint(const Point& point) const; - - bool operator==(const Matrix& other) const { - return a == other.a && b == other.b && c == other.c && d == other.d && tx == other.tx && - ty == other.ty; - } - - bool operator!=(const Matrix& other) const { - return !(*this == other); - } -}; - -} // namespace pagx diff --git a/pagx/include/pagx/types/VerticalAlign.h b/pagx/include/pagx/types/VerticalAlign.h deleted file mode 100644 index e1db9e2487..0000000000 --- a/pagx/include/pagx/types/VerticalAlign.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Text vertical alignment. - */ -enum class VerticalAlign { - Top, - Center, - Bottom -}; - -std::string VerticalAlignToString(VerticalAlign align); -VerticalAlign VerticalAlignFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/src/PAGXNode.cpp b/pagx/src/PAGXNode.cpp deleted file mode 100644 index b25eb7918f..0000000000 --- a/pagx/src/PAGXNode.cpp +++ /dev/null @@ -1,153 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "pagx/nodes/ColorSource.h" -#include "pagx/nodes/LayerFilter.h" -#include "pagx/nodes/LayerStyle.h" -#include "pagx/nodes/Node.h" -#include "pagx/nodes/VectorElement.h" - -namespace pagx { - -const char* NodeTypeName(NodeType type) { - switch (type) { - case NodeType::SolidColor: - return "SolidColor"; - case NodeType::LinearGradient: - return "LinearGradient"; - case NodeType::RadialGradient: - return "RadialGradient"; - case NodeType::ConicGradient: - return "ConicGradient"; - case NodeType::DiamondGradient: - return "DiamondGradient"; - case NodeType::ImagePattern: - return "ImagePattern"; - case NodeType::ColorStop: - return "ColorStop"; - case NodeType::Rectangle: - return "Rectangle"; - case NodeType::Ellipse: - return "Ellipse"; - case NodeType::Polystar: - return "Polystar"; - case NodeType::Path: - return "Path"; - case NodeType::TextSpan: - return "TextSpan"; - case NodeType::Fill: - return "Fill"; - case NodeType::Stroke: - return "Stroke"; - case NodeType::TrimPath: - return "TrimPath"; - case NodeType::RoundCorner: - return "RoundCorner"; - case NodeType::MergePath: - return "MergePath"; - case NodeType::TextModifier: - return "TextModifier"; - case NodeType::TextPath: - return "TextPath"; - case NodeType::TextLayout: - return "TextLayout"; - case NodeType::RangeSelector: - return "RangeSelector"; - case NodeType::Repeater: - return "Repeater"; - case NodeType::Group: - return "Group"; - case NodeType::DropShadowStyle: - return "DropShadowStyle"; - case NodeType::InnerShadowStyle: - return "InnerShadowStyle"; - case NodeType::BackgroundBlurStyle: - return "BackgroundBlurStyle"; - case NodeType::BlurFilter: - return "BlurFilter"; - case NodeType::DropShadowFilter: - return "DropShadowFilter"; - case NodeType::InnerShadowFilter: - return "InnerShadowFilter"; - case NodeType::BlendFilter: - return "BlendFilter"; - case NodeType::ColorMatrixFilter: - return "ColorMatrixFilter"; - case NodeType::Image: - return "Image"; - case NodeType::PathData: - return "PathData"; - case NodeType::Composition: - return "Composition"; - case NodeType::Layer: - return "Layer"; - default: - return "Unknown"; - } -} - -const char* ColorSourceTypeName(ColorSourceType type) { - switch (type) { - case ColorSourceType::SolidColor: - return "SolidColor"; - case ColorSourceType::LinearGradient: - return "LinearGradient"; - case ColorSourceType::RadialGradient: - return "RadialGradient"; - case ColorSourceType::ConicGradient: - return "ConicGradient"; - case ColorSourceType::DiamondGradient: - return "DiamondGradient"; - case ColorSourceType::ImagePattern: - return "ImagePattern"; - default: - return "Unknown"; - } -} - -const char* LayerStyleTypeName(LayerStyleType type) { - switch (type) { - case LayerStyleType::DropShadowStyle: - return "DropShadowStyle"; - case LayerStyleType::InnerShadowStyle: - return "InnerShadowStyle"; - case LayerStyleType::BackgroundBlurStyle: - return "BackgroundBlurStyle"; - default: - return "Unknown"; - } -} - -const char* LayerFilterTypeName(LayerFilterType type) { - switch (type) { - case LayerFilterType::BlurFilter: - return "BlurFilter"; - case LayerFilterType::DropShadowFilter: - return "DropShadowFilter"; - case LayerFilterType::InnerShadowFilter: - return "InnerShadowFilter"; - case LayerFilterType::BlendFilter: - return "BlendFilter"; - case LayerFilterType::ColorMatrixFilter: - return "ColorMatrixFilter"; - default: - return "Unknown"; - } -} - -} // namespace pagx From aed32d2d0cb4bf126a069707d3c8c9ae9d317d09 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:17:50 +0800 Subject: [PATCH 088/678] Fix appendix and TextModifier documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A.1: Move RangeSelector to separate "选择器" category (not a VectorElement) - A.2: Update Layer structure to remove obsolete contents/styles/filters wrappers - A.3: Update description to reflect simplified Layer structure - TextModifier: Add note that it can contain multiple RangeSelector children --- pagx/docs/pagx_spec.md | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index f803346638..93dd52ddb2 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1302,11 +1302,12 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: #### 5.5.3 文本变换器(TextModifier) -对选定范围内的字形应用变换和样式覆盖。 +对选定范围内的字形应用变换和样式覆盖。TextModifier 可包含多个 RangeSelector 子元素,用于定义不同的选择范围和影响因子。 ```xml - + + ``` @@ -1735,7 +1736,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | **几何元素** | `Rectangle`, `Ellipse`, `Polystar`, `Path`, `TextSpan` | | **绘制器** | `Fill`, `Stroke` | | **形状修改器** | `TrimPath`, `RoundCorner`, `MergePath` | -| **文本修改器** | `TextModifier`, `RangeSelector`, `TextPath`, `TextLayout` | +| **文本修改器** | `TextModifier`, `TextPath`, `TextLayout` | +| **选择器** | `RangeSelector`(TextModifier 子元素) | | **其他** | `Repeater`, `Group` | ### A.2 文档包含关系 @@ -1754,27 +1756,24 @@ pagx │ └── Composition → Layer* │ └── Layer* - ├── contents - │ └── VectorElement*(见下方) - ├── styles - │ ├── DropShadowStyle - │ ├── InnerShadowStyle - │ └── BackgroundBlurStyle - ├── filters - │ ├── BlurFilter - │ ├── DropShadowFilter - │ ├── InnerShadowFilter - │ ├── BlendFilter - │ └── ColorMatrixFilter + ├── VectorElement*(见 A.3) + ├── DropShadowStyle* + ├── InnerShadowStyle* + ├── BackgroundBlurStyle* + ├── BlurFilter* + ├── DropShadowFilter* + ├── InnerShadowFilter* + ├── BlendFilter* + ├── ColorMatrixFilter* └── Layer*(子图层) ``` ### A.3 VectorElement 包含关系 -`Layer.contents` 和 `Group` 可包含以下 VectorElement: +`Layer` 和 `Group` 可包含以下 VectorElement: ``` -Layer.contents / Group +Layer / Group ├── Rectangle ├── Ellipse ├── Polystar From a597587115945969176020966d5f82408b3f9757 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:19:31 +0800 Subject: [PATCH 089/678] =?UTF-8?q?Rename=20'=E9=80=89=E6=8B=A9=E5=99=A8'?= =?UTF-8?q?=20to=20'=E6=96=87=E6=9C=AC=E9=80=89=E6=8B=A9=E5=99=A8'=20for?= =?UTF-8?q?=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pagx/docs/pagx_spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 93dd52ddb2..1aaa474056 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1737,7 +1737,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | **绘制器** | `Fill`, `Stroke` | | **形状修改器** | `TrimPath`, `RoundCorner`, `MergePath` | | **文本修改器** | `TextModifier`, `TextPath`, `TextLayout` | -| **选择器** | `RangeSelector`(TextModifier 子元素) | +| **文本选择器** | `RangeSelector`(TextModifier 子元素) | | **其他** | `Repeater`, `Group` | ### A.2 文档包含关系 From 18d97c04cce5b11a26a474f5162545b9a5116d09 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:21:29 +0800 Subject: [PATCH 090/678] Add child element descriptions to Layer and Group in Appendix C --- pagx/docs/pagx_spec.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 1aaa474056..de72980c4d 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -2081,6 +2081,8 @@ Layer / Group | `maskType` | MaskType | alpha | | `composition` | idref | - | +子元素:`VectorElement`*、`LayerStyle`*、`LayerFilter`*、`Layer`*(按类型自动归类) + ### C.4 图层样式节点 #### DropShadowStyle @@ -2353,6 +2355,8 @@ Layer / Group | `skewAxis` | float | 0 | | `alpha` | float | 1 | +子元素:`VectorElement`*(递归包含 Group) + ### C.11 枚举类型 #### 图层相关 From 62f13df2bf59e8bc566ac9bace8b741bc0c8c4be Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:22:47 +0800 Subject: [PATCH 091/678] Fix stray closing tag in masking example --- pagx/docs/pagx_spec.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index de72980c4d..5dfb38512a 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -791,8 +791,6 @@ Layer 的子元素按类型自动归类为四个集合: ``` - -``` **遮罩规则**: - 遮罩图层自身不渲染(`visible` 属性被忽略) From 3bab538f57fbc22929efb6881b33824b9b8e3ff0 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:24:58 +0800 Subject: [PATCH 092/678] Fix minor issues in PAGX spec document - Fix `type` to `polystarType` in Polystar mode descriptions - Remove duplicate horizontal rule before Appendix A --- pagx/docs/pagx_spec.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 5dfb38512a..5152462fac 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -949,11 +949,11 @@ boundingRect.bottom = center.y + size.height / 2 | `polygon` | 正多边形:只使用外半径 | | `star` | 星形:使用外半径和内半径交替 | -**多边形模式** (`type="polygon"`): +**多边形模式** (`polystarType="polygon"`): - 只使用 `outerRadius` 和 `outerRoundness` - `innerRadius` 和 `innerRoundness` 被忽略 -**星形模式** (`type="star"`): +**星形模式** (`polystarType="star"`): - 外顶点位于 `outerRadius` 处 - 内顶点位于 `innerRadius` 处 - 顶点交替连接形成星形 @@ -1716,8 +1716,6 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: --- ---- - ## 附录 A. 节点层级与包含关系(Node Hierarchy) 本附录描述节点的分类和嵌套规则。 From 409c3704933216a375eceebca8b1c47add0e2e53 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 17:44:06 +0800 Subject: [PATCH 093/678] Refactor PAGX model hierarchy to use separate type methods for Resource and ColorSource. --- pagx/include/pagx/PAGXModel.h | 21 -- pagx/include/pagx/PAGXNode.h | 21 -- pagx/include/pagx/PAGXTypes.h | 22 -- pagx/include/pagx/model/BackgroundBlurStyle.h | 6 +- pagx/include/pagx/model/BlendFilter.h | 6 +- pagx/include/pagx/model/BlurFilter.h | 6 +- pagx/include/pagx/model/ColorMatrixFilter.h | 6 +- pagx/include/pagx/model/Composition.h | 6 +- pagx/include/pagx/model/ConicGradient.h | 6 +- pagx/include/pagx/model/DiamondGradient.h | 6 +- pagx/include/pagx/model/DropShadowFilter.h | 6 +- pagx/include/pagx/model/DropShadowStyle.h | 6 +- pagx/include/pagx/model/Element.h | 9 +- pagx/include/pagx/model/Ellipse.h | 6 +- pagx/include/pagx/model/Fill.h | 6 +- pagx/include/pagx/model/Group.h | 6 +- pagx/include/pagx/model/Image.h | 6 +- pagx/include/pagx/model/ImagePattern.h | 6 +- pagx/include/pagx/model/InnerShadowFilter.h | 6 +- pagx/include/pagx/model/InnerShadowStyle.h | 6 +- pagx/include/pagx/model/LayerFilter.h | 9 +- pagx/include/pagx/model/LayerStyle.h | 9 +- pagx/include/pagx/model/LinearGradient.h | 6 +- pagx/include/pagx/model/MergePath.h | 6 +- pagx/include/pagx/model/Model.h | 88 ------- pagx/include/pagx/model/NodeType.h | 91 ------- pagx/include/pagx/model/Path.h | 6 +- pagx/include/pagx/model/PathDataResource.h | 6 +- pagx/include/pagx/model/Polystar.h | 6 +- pagx/include/pagx/model/RadialGradient.h | 6 +- pagx/include/pagx/model/Rectangle.h | 6 +- pagx/include/pagx/model/Repeater.h | 6 +- pagx/include/pagx/model/Resource.h | 8 +- pagx/include/pagx/model/RoundCorner.h | 6 +- pagx/include/pagx/model/SolidColor.h | 6 +- pagx/include/pagx/model/Stroke.h | 45 +++- pagx/include/pagx/model/TextLayout.h | 6 +- pagx/include/pagx/model/TextModifier.h | 6 +- pagx/include/pagx/model/TextPath.h | 6 +- pagx/include/pagx/model/TextSpan.h | 6 +- pagx/include/pagx/model/TrimPath.h | 6 +- pagx/include/pagx/model/types/Enums.h | 54 ----- pagx/include/pagx/model/types/Types.h | 229 ------------------ pagx/src/PAGXDocument.cpp | 2 +- pagx/src/PAGXTypes.cpp | 77 ------ pagx/src/PAGXXMLWriter.cpp | 148 ++++++----- pagx/src/svg/PAGXSVGParser.cpp | 16 +- pagx/src/tgfx/LayerBuilder.cpp | 44 ++-- test/src/PAGXTest.cpp | 20 +- 49 files changed, 184 insertions(+), 915 deletions(-) delete mode 100644 pagx/include/pagx/PAGXModel.h delete mode 100644 pagx/include/pagx/PAGXNode.h delete mode 100644 pagx/include/pagx/PAGXTypes.h delete mode 100644 pagx/include/pagx/model/Model.h delete mode 100644 pagx/include/pagx/model/NodeType.h delete mode 100644 pagx/include/pagx/model/types/Enums.h delete mode 100644 pagx/include/pagx/model/types/Types.h diff --git a/pagx/include/pagx/PAGXModel.h b/pagx/include/pagx/PAGXModel.h deleted file mode 100644 index 4ecddd7909..0000000000 --- a/pagx/include/pagx/PAGXModel.h +++ /dev/null @@ -1,21 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/model/Model.h" diff --git a/pagx/include/pagx/PAGXNode.h b/pagx/include/pagx/PAGXNode.h deleted file mode 100644 index 4ecddd7909..0000000000 --- a/pagx/include/pagx/PAGXNode.h +++ /dev/null @@ -1,21 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/model/Model.h" diff --git a/pagx/include/pagx/PAGXTypes.h b/pagx/include/pagx/PAGXTypes.h deleted file mode 100644 index 54c4bb9de8..0000000000 --- a/pagx/include/pagx/PAGXTypes.h +++ /dev/null @@ -1,22 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "pagx/model/types/Enums.h" -#include "pagx/model/types/Types.h" diff --git a/pagx/include/pagx/model/BackgroundBlurStyle.h b/pagx/include/pagx/model/BackgroundBlurStyle.h index 1e134c1a35..ae0288d2b7 100644 --- a/pagx/include/pagx/model/BackgroundBlurStyle.h +++ b/pagx/include/pagx/model/BackgroundBlurStyle.h @@ -34,13 +34,9 @@ class BackgroundBlurStyle : public LayerStyle { TileMode tileMode = TileMode::Mirror; BlendMode blendMode = BlendMode::Normal; - LayerStyleType layerStyleType() const override { + LayerStyleType type() const override { return LayerStyleType::BackgroundBlurStyle; } - - NodeType type() const override { - return NodeType::BackgroundBlurStyle; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/BlendFilter.h b/pagx/include/pagx/model/BlendFilter.h index 154513ad5d..eb078137d9 100644 --- a/pagx/include/pagx/model/BlendFilter.h +++ b/pagx/include/pagx/model/BlendFilter.h @@ -32,11 +32,7 @@ class BlendFilter : public LayerFilter { Color color = {}; BlendMode blendMode = BlendMode::Normal; - NodeType type() const override { - return NodeType::BlendFilter; - } - - LayerFilterType layerFilterType() const override { + LayerFilterType type() const override { return LayerFilterType::BlendFilter; } }; diff --git a/pagx/include/pagx/model/BlurFilter.h b/pagx/include/pagx/model/BlurFilter.h index 1fd373c1cd..9940c58ee8 100644 --- a/pagx/include/pagx/model/BlurFilter.h +++ b/pagx/include/pagx/model/BlurFilter.h @@ -32,11 +32,7 @@ class BlurFilter : public LayerFilter { float blurrinessY = 0; TileMode tileMode = TileMode::Decal; - NodeType type() const override { - return NodeType::BlurFilter; - } - - LayerFilterType layerFilterType() const override { + LayerFilterType type() const override { return LayerFilterType::BlurFilter; } }; diff --git a/pagx/include/pagx/model/ColorMatrixFilter.h b/pagx/include/pagx/model/ColorMatrixFilter.h index 247cc3f16f..98ae503a74 100644 --- a/pagx/include/pagx/model/ColorMatrixFilter.h +++ b/pagx/include/pagx/model/ColorMatrixFilter.h @@ -30,11 +30,7 @@ class ColorMatrixFilter : public LayerFilter { public: std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; - NodeType type() const override { - return NodeType::ColorMatrixFilter; - } - - LayerFilterType layerFilterType() const override { + LayerFilterType type() const override { return LayerFilterType::ColorMatrixFilter; } }; diff --git a/pagx/include/pagx/model/Composition.h b/pagx/include/pagx/model/Composition.h index 41910fb4e3..98d4038345 100644 --- a/pagx/include/pagx/model/Composition.h +++ b/pagx/include/pagx/model/Composition.h @@ -37,11 +37,7 @@ class Composition : public Resource { float height = 0; std::vector> layers = {}; - NodeType type() const override { - return NodeType::Composition; - } - - ResourceType resourceType() const override { + ResourceType type() const override { return ResourceType::Composition; } diff --git a/pagx/include/pagx/model/ConicGradient.h b/pagx/include/pagx/model/ConicGradient.h index b35c5e81f8..fec15739d3 100644 --- a/pagx/include/pagx/model/ConicGradient.h +++ b/pagx/include/pagx/model/ConicGradient.h @@ -42,17 +42,13 @@ class ConicGradient : public ColorSource { return ColorSourceType::ConicGradient; } - ResourceType resourceType() const override { + ResourceType type() const override { return ResourceType::ConicGradient; } const std::string& resourceId() const override { return id; } - - NodeType type() const override { - return NodeType::ConicGradient; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/DiamondGradient.h b/pagx/include/pagx/model/DiamondGradient.h index c5a3df1270..75b8e84023 100644 --- a/pagx/include/pagx/model/DiamondGradient.h +++ b/pagx/include/pagx/model/DiamondGradient.h @@ -41,17 +41,13 @@ class DiamondGradient : public ColorSource { return ColorSourceType::DiamondGradient; } - ResourceType resourceType() const override { + ResourceType type() const override { return ResourceType::DiamondGradient; } const std::string& resourceId() const override { return id; } - - NodeType type() const override { - return NodeType::DiamondGradient; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/DropShadowFilter.h b/pagx/include/pagx/model/DropShadowFilter.h index b81e77f20a..8619dd31b5 100644 --- a/pagx/include/pagx/model/DropShadowFilter.h +++ b/pagx/include/pagx/model/DropShadowFilter.h @@ -35,11 +35,7 @@ class DropShadowFilter : public LayerFilter { Color color = {}; bool shadowOnly = false; - NodeType type() const override { - return NodeType::DropShadowFilter; - } - - LayerFilterType layerFilterType() const override { + LayerFilterType type() const override { return LayerFilterType::DropShadowFilter; } }; diff --git a/pagx/include/pagx/model/DropShadowStyle.h b/pagx/include/pagx/model/DropShadowStyle.h index 68e3f9486f..a6e515284d 100644 --- a/pagx/include/pagx/model/DropShadowStyle.h +++ b/pagx/include/pagx/model/DropShadowStyle.h @@ -37,13 +37,9 @@ class DropShadowStyle : public LayerStyle { bool showBehindLayer = true; BlendMode blendMode = BlendMode::Normal; - LayerStyleType layerStyleType() const override { + LayerStyleType type() const override { return LayerStyleType::DropShadowStyle; } - - NodeType type() const override { - return NodeType::DropShadowStyle; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Element.h b/pagx/include/pagx/model/Element.h index fd7f531e42..6f27ad15c6 100644 --- a/pagx/include/pagx/model/Element.h +++ b/pagx/include/pagx/model/Element.h @@ -18,8 +18,6 @@ #pragma once -#include "pagx/model/NodeType.h" - namespace pagx { /** @@ -106,12 +104,7 @@ class Element { /** * Returns the element type of this element. */ - virtual ElementType elementType() const = 0; - - /** - * Returns the unified node type of this element. - */ - virtual NodeType type() const = 0; + virtual ElementType type() const = 0; protected: Element() = default; diff --git a/pagx/include/pagx/model/Ellipse.h b/pagx/include/pagx/model/Ellipse.h index 7abd8025c5..6e15085ae4 100644 --- a/pagx/include/pagx/model/Ellipse.h +++ b/pagx/include/pagx/model/Ellipse.h @@ -43,13 +43,9 @@ class Ellipse : public Element { */ bool reversed = false; - ElementType elementType() const override { + ElementType type() const override { return ElementType::Ellipse; } - - NodeType type() const override { - return NodeType::Ellipse; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Fill.h b/pagx/include/pagx/model/Fill.h index b47f67569d..841839b94a 100644 --- a/pagx/include/pagx/model/Fill.h +++ b/pagx/include/pagx/model/Fill.h @@ -69,13 +69,9 @@ class Fill : public Element { */ Placement placement = Placement::Background; - ElementType elementType() const override { + ElementType type() const override { return ElementType::Fill; } - - NodeType type() const override { - return NodeType::Fill; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Group.h b/pagx/include/pagx/model/Group.h index 776654504d..005752646f 100644 --- a/pagx/include/pagx/model/Group.h +++ b/pagx/include/pagx/model/Group.h @@ -72,13 +72,9 @@ class Group : public Element { */ std::vector> elements = {}; - ElementType elementType() const override { + ElementType type() const override { return ElementType::Group; } - - NodeType type() const override { - return NodeType::Group; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Image.h b/pagx/include/pagx/model/Image.h index 07ab02f842..2212d6f284 100644 --- a/pagx/include/pagx/model/Image.h +++ b/pagx/include/pagx/model/Image.h @@ -31,11 +31,7 @@ class Image : public Resource { std::string id = {}; std::string source = {}; - NodeType type() const override { - return NodeType::Image; - } - - ResourceType resourceType() const override { + ResourceType type() const override { return ResourceType::Image; } diff --git a/pagx/include/pagx/model/ImagePattern.h b/pagx/include/pagx/model/ImagePattern.h index b90bc754a1..44c496b79f 100644 --- a/pagx/include/pagx/model/ImagePattern.h +++ b/pagx/include/pagx/model/ImagePattern.h @@ -42,17 +42,13 @@ class ImagePattern : public ColorSource { return ColorSourceType::ImagePattern; } - ResourceType resourceType() const override { + ResourceType type() const override { return ResourceType::ImagePattern; } const std::string& resourceId() const override { return id; } - - NodeType type() const override { - return NodeType::ImagePattern; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/InnerShadowFilter.h b/pagx/include/pagx/model/InnerShadowFilter.h index 210d61886a..c27de19407 100644 --- a/pagx/include/pagx/model/InnerShadowFilter.h +++ b/pagx/include/pagx/model/InnerShadowFilter.h @@ -35,11 +35,7 @@ class InnerShadowFilter : public LayerFilter { Color color = {}; bool shadowOnly = false; - NodeType type() const override { - return NodeType::InnerShadowFilter; - } - - LayerFilterType layerFilterType() const override { + LayerFilterType type() const override { return LayerFilterType::InnerShadowFilter; } }; diff --git a/pagx/include/pagx/model/InnerShadowStyle.h b/pagx/include/pagx/model/InnerShadowStyle.h index da233812dc..6885667587 100644 --- a/pagx/include/pagx/model/InnerShadowStyle.h +++ b/pagx/include/pagx/model/InnerShadowStyle.h @@ -36,13 +36,9 @@ class InnerShadowStyle : public LayerStyle { Color color = {}; BlendMode blendMode = BlendMode::Normal; - LayerStyleType layerStyleType() const override { + LayerStyleType type() const override { return LayerStyleType::InnerShadowStyle; } - - NodeType type() const override { - return NodeType::InnerShadowStyle; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/LayerFilter.h b/pagx/include/pagx/model/LayerFilter.h index 5156108544..4681bf5d8a 100644 --- a/pagx/include/pagx/model/LayerFilter.h +++ b/pagx/include/pagx/model/LayerFilter.h @@ -18,8 +18,6 @@ #pragma once -#include "pagx/model/NodeType.h" - namespace pagx { /** @@ -48,12 +46,7 @@ class LayerFilter { /** * Returns the layer filter type of this layer filter. */ - virtual LayerFilterType layerFilterType() const = 0; - - /** - * Returns the unified node type of this layer filter. - */ - virtual NodeType type() const = 0; + virtual LayerFilterType type() const = 0; protected: LayerFilter() = default; diff --git a/pagx/include/pagx/model/LayerStyle.h b/pagx/include/pagx/model/LayerStyle.h index 3c7fa41999..ccbf8e2cbf 100644 --- a/pagx/include/pagx/model/LayerStyle.h +++ b/pagx/include/pagx/model/LayerStyle.h @@ -18,8 +18,6 @@ #pragma once -#include "pagx/model/NodeType.h" - namespace pagx { /** @@ -46,12 +44,7 @@ class LayerStyle { /** * Returns the layer style type of this layer style. */ - virtual LayerStyleType layerStyleType() const = 0; - - /** - * Returns the unified node type of this layer style. - */ - virtual NodeType type() const = 0; + virtual LayerStyleType type() const = 0; protected: LayerStyle() = default; diff --git a/pagx/include/pagx/model/LinearGradient.h b/pagx/include/pagx/model/LinearGradient.h index 02707f03b2..919ecbec02 100644 --- a/pagx/include/pagx/model/LinearGradient.h +++ b/pagx/include/pagx/model/LinearGradient.h @@ -41,17 +41,13 @@ class LinearGradient : public ColorSource { return ColorSourceType::LinearGradient; } - ResourceType resourceType() const override { + ResourceType type() const override { return ResourceType::LinearGradient; } const std::string& resourceId() const override { return id; } - - NodeType type() const override { - return NodeType::LinearGradient; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/MergePath.h b/pagx/include/pagx/model/MergePath.h index 2a879b85ed..aa5ddb62ff 100644 --- a/pagx/include/pagx/model/MergePath.h +++ b/pagx/include/pagx/model/MergePath.h @@ -34,13 +34,9 @@ class MergePath : public Element { */ MergePathMode mode = MergePathMode::Append; - ElementType elementType() const override { + ElementType type() const override { return ElementType::MergePath; } - - NodeType type() const override { - return NodeType::MergePath; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Model.h b/pagx/include/pagx/model/Model.h deleted file mode 100644 index 345e27043b..0000000000 --- a/pagx/include/pagx/model/Model.h +++ /dev/null @@ -1,88 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -// Basic types and enums -#include "pagx/model/types/Enums.h" -#include "pagx/model/types/Types.h" - -// Unified node type -#include "pagx/model/NodeType.h" - -// Base classes -#include "pagx/model/ColorSource.h" -#include "pagx/model/Element.h" -#include "pagx/model/LayerFilter.h" -#include "pagx/model/LayerStyle.h" -#include "pagx/model/Resource.h" - -// Color sources -#include "pagx/model/ColorStop.h" -#include "pagx/model/ConicGradient.h" -#include "pagx/model/DiamondGradient.h" -#include "pagx/model/ImagePattern.h" -#include "pagx/model/LinearGradient.h" -#include "pagx/model/RadialGradient.h" -#include "pagx/model/SolidColor.h" - -// Vector elements - shapes -#include "pagx/model/Ellipse.h" -#include "pagx/model/Path.h" -#include "pagx/model/Polystar.h" -#include "pagx/model/Rectangle.h" -#include "pagx/model/TextSpan.h" - -// Vector elements - painters -#include "pagx/model/Fill.h" -#include "pagx/model/Stroke.h" - -// Vector elements - path modifiers -#include "pagx/model/MergePath.h" -#include "pagx/model/RoundCorner.h" -#include "pagx/model/TrimPath.h" - -// Vector elements - text modifiers -#include "pagx/model/RangeSelector.h" -#include "pagx/model/TextLayout.h" -#include "pagx/model/TextModifier.h" -#include "pagx/model/TextPath.h" - -// Vector elements - containers -#include "pagx/model/Group.h" -#include "pagx/model/Repeater.h" - -// Layer styles -#include "pagx/model/BackgroundBlurStyle.h" -#include "pagx/model/DropShadowStyle.h" -#include "pagx/model/InnerShadowStyle.h" - -// Layer filters -#include "pagx/model/BlendFilter.h" -#include "pagx/model/BlurFilter.h" -#include "pagx/model/ColorMatrixFilter.h" -#include "pagx/model/DropShadowFilter.h" -#include "pagx/model/InnerShadowFilter.h" - -// Resources -#include "pagx/model/Composition.h" -#include "pagx/model/Image.h" -#include "pagx/model/PathDataResource.h" - -// Layer -#include "pagx/model/Layer.h" diff --git a/pagx/include/pagx/model/NodeType.h b/pagx/include/pagx/model/NodeType.h deleted file mode 100644 index 25b1024dc6..0000000000 --- a/pagx/include/pagx/model/NodeType.h +++ /dev/null @@ -1,91 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -namespace pagx { - -/** - * NodeType enumerates all types of nodes in a PAGX document. - * This unified type system is used for type checking across different categories of nodes. - */ -enum class NodeType { - // Color sources - SolidColor, - LinearGradient, - RadialGradient, - ConicGradient, - DiamondGradient, - ImagePattern, - ColorStop, - - // Geometry elements - Rectangle, - Ellipse, - Polystar, - Path, - TextSpan, - - // Painters - Fill, - Stroke, - - // Shape modifiers - TrimPath, - RoundCorner, - MergePath, - - // Text modifiers - TextModifier, - TextPath, - TextLayout, - RangeSelector, - - // Repeater - Repeater, - - // Container - Group, - - // Layer styles - DropShadowStyle, - InnerShadowStyle, - BackgroundBlurStyle, - - // Layer filters - BlurFilter, - DropShadowFilter, - InnerShadowFilter, - BlendFilter, - ColorMatrixFilter, - - // Resources - Image, - PathData, - Composition, - - // Layer - Layer -}; - -/** - * Returns the string name of a node type. - */ -const char* NodeTypeName(NodeType type); - -} // namespace pagx diff --git a/pagx/include/pagx/model/Path.h b/pagx/include/pagx/model/Path.h index 7d68f7ab53..7839fa6e4e 100644 --- a/pagx/include/pagx/model/Path.h +++ b/pagx/include/pagx/model/Path.h @@ -39,13 +39,9 @@ class Path : public Element { */ bool reversed = false; - ElementType elementType() const override { + ElementType type() const override { return ElementType::Path; } - - NodeType type() const override { - return NodeType::Path; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/PathDataResource.h b/pagx/include/pagx/model/PathDataResource.h index 4b675bdf39..a53cc2ac21 100644 --- a/pagx/include/pagx/model/PathDataResource.h +++ b/pagx/include/pagx/model/PathDataResource.h @@ -31,11 +31,7 @@ class PathDataResource : public Resource { std::string id = {}; std::string data = {}; // SVG path data string - NodeType type() const override { - return NodeType::PathData; - } - - ResourceType resourceType() const override { + ResourceType type() const override { return ResourceType::PathData; } diff --git a/pagx/include/pagx/model/Polystar.h b/pagx/include/pagx/model/Polystar.h index 100ef0a1e8..01c70e2c36 100644 --- a/pagx/include/pagx/model/Polystar.h +++ b/pagx/include/pagx/model/Polystar.h @@ -76,13 +76,9 @@ class Polystar : public Element { */ bool reversed = false; - ElementType elementType() const override { + ElementType type() const override { return ElementType::Polystar; } - - NodeType type() const override { - return NodeType::Polystar; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/RadialGradient.h b/pagx/include/pagx/model/RadialGradient.h index 6e71881136..9bb3cd09bd 100644 --- a/pagx/include/pagx/model/RadialGradient.h +++ b/pagx/include/pagx/model/RadialGradient.h @@ -41,17 +41,13 @@ class RadialGradient : public ColorSource { return ColorSourceType::RadialGradient; } - ResourceType resourceType() const override { + ResourceType type() const override { return ResourceType::RadialGradient; } const std::string& resourceId() const override { return id; } - - NodeType type() const override { - return NodeType::RadialGradient; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Rectangle.h b/pagx/include/pagx/model/Rectangle.h index 463e25ac6a..f591dedbc1 100644 --- a/pagx/include/pagx/model/Rectangle.h +++ b/pagx/include/pagx/model/Rectangle.h @@ -48,13 +48,9 @@ class Rectangle : public Element { */ bool reversed = false; - ElementType elementType() const override { + ElementType type() const override { return ElementType::Rectangle; } - - NodeType type() const override { - return NodeType::Rectangle; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Repeater.h b/pagx/include/pagx/model/Repeater.h index a484ca73cb..798a627593 100644 --- a/pagx/include/pagx/model/Repeater.h +++ b/pagx/include/pagx/model/Repeater.h @@ -77,13 +77,9 @@ class Repeater : public Element { */ float endAlpha = 1; - ElementType elementType() const override { + ElementType type() const override { return ElementType::Repeater; } - - NodeType type() const override { - return NodeType::Repeater; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Resource.h b/pagx/include/pagx/model/Resource.h index f4223feb1f..a2de13c0c1 100644 --- a/pagx/include/pagx/model/Resource.h +++ b/pagx/include/pagx/model/Resource.h @@ -19,7 +19,6 @@ #pragma once #include -#include "pagx/model/NodeType.h" namespace pagx { @@ -81,12 +80,7 @@ class Resource { /** * Returns the resource type of this resource. */ - virtual ResourceType resourceType() const = 0; - - /** - * Returns the unified node type of this resource. - */ - virtual NodeType type() const = 0; + virtual ResourceType type() const = 0; /** * Returns the unique identifier of this resource. diff --git a/pagx/include/pagx/model/RoundCorner.h b/pagx/include/pagx/model/RoundCorner.h index 494bf28743..9aedc8ab8a 100644 --- a/pagx/include/pagx/model/RoundCorner.h +++ b/pagx/include/pagx/model/RoundCorner.h @@ -33,13 +33,9 @@ class RoundCorner : public Element { */ float radius = 10; - ElementType elementType() const override { + ElementType type() const override { return ElementType::RoundCorner; } - - NodeType type() const override { - return NodeType::RoundCorner; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/SolidColor.h b/pagx/include/pagx/model/SolidColor.h index d010784c6d..f83123abc9 100644 --- a/pagx/include/pagx/model/SolidColor.h +++ b/pagx/include/pagx/model/SolidColor.h @@ -36,17 +36,13 @@ class SolidColor : public ColorSource { return ColorSourceType::SolidColor; } - ResourceType resourceType() const override { + ResourceType type() const override { return ResourceType::SolidColor; } const std::string& resourceId() const override { return id; } - - NodeType type() const override { - return NodeType::SolidColor; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Stroke.h b/pagx/include/pagx/model/Stroke.h index 5750909d65..5e6a57a1b5 100644 --- a/pagx/include/pagx/model/Stroke.h +++ b/pagx/include/pagx/model/Stroke.h @@ -24,13 +24,46 @@ #include "pagx/model/ColorSource.h" #include "pagx/model/Element.h" #include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/LineCap.h" -#include "pagx/model/types/LineJoin.h" #include "pagx/model/types/Placement.h" -#include "pagx/model/types/StrokeAlign.h" namespace pagx { +/** + * Line cap styles for strokes. + */ +enum class LineCap { + Butt, + Round, + Square +}; + +std::string LineCapToString(LineCap cap); +LineCap LineCapFromString(const std::string& str); + +/** + * Line join styles for strokes. + */ +enum class LineJoin { + Miter, + Round, + Bevel +}; + +std::string LineJoinToString(LineJoin join); +LineJoin LineJoinFromString(const std::string& str); + +/** + * Stroke alignment relative to path. + */ +enum class StrokeAlign { + Center, + Inside, + Outside +}; + +std::string StrokeAlignToString(StrokeAlign align); +StrokeAlign StrokeAlignFromString(const std::string& str); + /** * Stroke represents a stroke painter that outlines shapes with a solid color, gradient, or * pattern. It supports various line cap styles, join styles, dash patterns, and stroke alignment. @@ -101,13 +134,9 @@ class Stroke : public Element { */ Placement placement = Placement::Background; - ElementType elementType() const override { + ElementType type() const override { return ElementType::Stroke; } - - NodeType type() const override { - return NodeType::Stroke; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/TextLayout.h b/pagx/include/pagx/model/TextLayout.h index 2dfdfb3578..d0c6fe3be7 100644 --- a/pagx/include/pagx/model/TextLayout.h +++ b/pagx/include/pagx/model/TextLayout.h @@ -67,13 +67,9 @@ class TextLayout : public Element { */ Overflow overflow = Overflow::Clip; - ElementType elementType() const override { + ElementType type() const override { return ElementType::TextLayout; } - - NodeType type() const override { - return NodeType::TextLayout; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/TextModifier.h b/pagx/include/pagx/model/TextModifier.h index 2e18161f14..d39c1c04d6 100644 --- a/pagx/include/pagx/model/TextModifier.h +++ b/pagx/include/pagx/model/TextModifier.h @@ -89,13 +89,9 @@ class TextModifier : public Element { */ std::vector rangeSelectors = {}; - ElementType elementType() const override { + ElementType type() const override { return ElementType::TextModifier; } - - NodeType type() const override { - return NodeType::TextModifier; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/TextPath.h b/pagx/include/pagx/model/TextPath.h index 4bea12846c..f8c20317d4 100644 --- a/pagx/include/pagx/model/TextPath.h +++ b/pagx/include/pagx/model/TextPath.h @@ -66,13 +66,9 @@ class TextPath : public Element { */ bool forceAlignment = false; - ElementType elementType() const override { + ElementType type() const override { return ElementType::TextPath; } - - NodeType type() const override { - return NodeType::TextPath; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/TextSpan.h b/pagx/include/pagx/model/TextSpan.h index c1e290fc5a..0488721f72 100644 --- a/pagx/include/pagx/model/TextSpan.h +++ b/pagx/include/pagx/model/TextSpan.h @@ -75,13 +75,9 @@ class TextSpan : public Element { */ std::string text = {}; - ElementType elementType() const override { + ElementType type() const override { return ElementType::TextSpan; } - - NodeType type() const override { - return NodeType::TextSpan; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/TrimPath.h b/pagx/include/pagx/model/TrimPath.h index 9f30bf4a66..d8eedaa5e7 100644 --- a/pagx/include/pagx/model/TrimPath.h +++ b/pagx/include/pagx/model/TrimPath.h @@ -54,13 +54,9 @@ class TrimPath : public Element { */ TrimType trimType = TrimType::Separate; - ElementType elementType() const override { + ElementType type() const override { return ElementType::TrimPath; } - - NodeType type() const override { - return NodeType::TrimPath; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/types/Enums.h b/pagx/include/pagx/model/types/Enums.h deleted file mode 100644 index acc3ffed7e..0000000000 --- a/pagx/include/pagx/model/types/Enums.h +++ /dev/null @@ -1,54 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -// Layer related -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/MaskType.h" - -// Painter related -#include "pagx/model/types/FillRule.h" -#include "pagx/model/types/LineCap.h" -#include "pagx/model/types/LineJoin.h" -#include "pagx/model/types/Placement.h" -#include "pagx/model/types/StrokeAlign.h" - -// Color source related -#include "pagx/model/types/SamplingMode.h" -#include "pagx/model/types/TileMode.h" - -// Geometry related -#include "pagx/model/types/PolystarType.h" - -// Path modifier related -#include "pagx/model/types/MergePathMode.h" -#include "pagx/model/types/TrimType.h" - -// Text modifier related -#include "pagx/model/types/FontStyle.h" -#include "pagx/model/types/Overflow.h" -#include "pagx/model/types/SelectorMode.h" -#include "pagx/model/types/SelectorShape.h" -#include "pagx/model/types/SelectorUnit.h" -#include "pagx/model/types/TextAlign.h" -#include "pagx/model/types/TextPathAlign.h" -#include "pagx/model/types/VerticalAlign.h" - -// Repeater related -#include "pagx/model/types/RepeaterOrder.h" diff --git a/pagx/include/pagx/model/types/Types.h b/pagx/include/pagx/model/types/Types.h deleted file mode 100644 index 3c17baaadf..0000000000 --- a/pagx/include/pagx/model/types/Types.h +++ /dev/null @@ -1,229 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include - -namespace pagx { - -/** - * A point with x and y coordinates. - */ -struct Point { - float x = 0; - float y = 0; - - bool operator==(const Point& other) const { - return x == other.x && y == other.y; - } - - bool operator!=(const Point& other) const { - return !(*this == other); - } -}; - -/** - * A size with width and height. - */ -struct Size { - float width = 0; - float height = 0; - - bool operator==(const Size& other) const { - return width == other.width && height == other.height; - } - - bool operator!=(const Size& other) const { - return !(*this == other); - } -}; - -/** - * A rectangle defined by position and size. - */ -struct Rect { - float x = 0; - float y = 0; - float width = 0; - float height = 0; - - /** - * Returns a Rect from left, top, right, bottom coordinates. - */ - static Rect MakeLTRB(float l, float t, float r, float b) { - return {l, t, r - l, b - t}; - } - - /** - * Returns a Rect from position and size. - */ - static Rect MakeXYWH(float x, float y, float w, float h) { - return {x, y, w, h}; - } - - float left() const { - return x; - } - - float top() const { - return y; - } - - float right() const { - return x + width; - } - - float bottom() const { - return y + height; - } - - bool isEmpty() const { - return width <= 0 || height <= 0; - } - - void setEmpty() { - x = y = width = height = 0; - } - - bool operator==(const Rect& other) const { - return x == other.x && y == other.y && width == other.width && height == other.height; - } - - bool operator!=(const Rect& other) const { - return !(*this == other); - } -}; - -/** - * An RGBA color with floating-point components in [0, 1]. - */ -struct Color { - float red = 0; - float green = 0; - float blue = 0; - float alpha = 1; - - /** - * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). - */ - static Color FromHex(uint32_t hex, bool hasAlpha = false); - - /** - * Returns a Color from RGBA components in [0, 1]. - */ - static Color FromRGBA(float r, float g, float b, float a = 1); - - /** - * Parses a color string. Supports: - * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" - * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" - * Returns black if parsing fails. - */ - static Color Parse(const std::string& str); - - /** - * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". - */ - std::string toHexString(bool includeAlpha = false) const; - - bool operator==(const Color& other) const { - return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; - } - - bool operator!=(const Color& other) const { - return !(*this == other); - } -}; - -/** - * A 2D affine transformation matrix. - * Matrix form: - * | a c tx | - * | b d ty | - * | 0 0 1 | - */ -struct Matrix { - float a = 1; // scaleX - float b = 0; // skewY - float c = 0; // skewX - float d = 1; // scaleY - float tx = 0; // transX - float ty = 0; // transY - - /** - * Returns the identity matrix. - */ - static Matrix Identity() { - return {}; - } - - /** - * Returns a translation matrix. - */ - static Matrix Translate(float x, float y); - - /** - * Returns a scale matrix. - */ - static Matrix Scale(float sx, float sy); - - /** - * Returns a rotation matrix (angle in degrees). - */ - static Matrix Rotate(float degrees); - - /** - * Parses a matrix string "a,b,c,d,tx,ty". - */ - static Matrix Parse(const std::string& str); - - /** - * Returns the matrix as a string "a,b,c,d,tx,ty". - */ - std::string toString() const; - - /** - * Returns true if this is the identity matrix. - */ - bool isIdentity() const { - return a == 1 && b == 0 && c == 0 && d == 1 && tx == 0 && ty == 0; - } - - /** - * Concatenates this matrix with another. - */ - Matrix operator*(const Matrix& other) const; - - /** - * Transforms a point by this matrix. - */ - Point mapPoint(const Point& point) const; - - bool operator==(const Matrix& other) const { - return a == other.a && b == other.b && c == other.c && d == other.d && tx == other.tx && - ty == other.ty; - } - - bool operator!=(const Matrix& other) const { - return !(*this == other); - } -}; - -} // namespace pagx diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp index bc57127416..cf2c484f8d 100644 --- a/pagx/src/PAGXDocument.cpp +++ b/pagx/src/PAGXDocument.cpp @@ -76,7 +76,7 @@ Layer* Document::findLayer(const std::string& id) const { } // Then search in Composition resources for (const auto& resource : resources) { - if (resource->resourceType() == ResourceType::Composition) { + if (resource->type() == ResourceType::Composition) { auto comp = static_cast(resource.get()); found = findLayerRecursive(comp->layers, id); if (found) { diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp index caaa486564..ab8a39121d 100644 --- a/pagx/src/PAGXTypes.cpp +++ b/pagx/src/PAGXTypes.cpp @@ -18,7 +18,6 @@ #include "pagx/model/types/Types.h" #include "pagx/model/types/Enums.h" -#include "pagx/model/NodeType.h" #include #include #include @@ -430,80 +429,4 @@ DEFINE_ENUM_CONVERSION(RepeaterOrder, #undef DEFINE_ENUM_CONVERSION -const char* NodeTypeName(NodeType type) { - switch (type) { - case NodeType::SolidColor: - return "SolidColor"; - case NodeType::LinearGradient: - return "LinearGradient"; - case NodeType::RadialGradient: - return "RadialGradient"; - case NodeType::ConicGradient: - return "ConicGradient"; - case NodeType::DiamondGradient: - return "DiamondGradient"; - case NodeType::ImagePattern: - return "ImagePattern"; - case NodeType::ColorStop: - return "ColorStop"; - case NodeType::Rectangle: - return "Rectangle"; - case NodeType::Ellipse: - return "Ellipse"; - case NodeType::Polystar: - return "Polystar"; - case NodeType::Path: - return "Path"; - case NodeType::TextSpan: - return "TextSpan"; - case NodeType::Fill: - return "Fill"; - case NodeType::Stroke: - return "Stroke"; - case NodeType::TrimPath: - return "TrimPath"; - case NodeType::RoundCorner: - return "RoundCorner"; - case NodeType::MergePath: - return "MergePath"; - case NodeType::TextModifier: - return "TextModifier"; - case NodeType::TextPath: - return "TextPath"; - case NodeType::TextLayout: - return "TextLayout"; - case NodeType::RangeSelector: - return "RangeSelector"; - case NodeType::Repeater: - return "Repeater"; - case NodeType::Group: - return "Group"; - case NodeType::DropShadowStyle: - return "DropShadowStyle"; - case NodeType::InnerShadowStyle: - return "InnerShadowStyle"; - case NodeType::BackgroundBlurStyle: - return "BackgroundBlurStyle"; - case NodeType::BlurFilter: - return "BlurFilter"; - case NodeType::DropShadowFilter: - return "DropShadowFilter"; - case NodeType::InnerShadowFilter: - return "InnerShadowFilter"; - case NodeType::BlendFilter: - return "BlendFilter"; - case NodeType::ColorMatrixFilter: - return "ColorMatrixFilter"; - case NodeType::Image: - return "Image"; - case NodeType::PathData: - return "PathData"; - case NodeType::Composition: - return "Composition"; - case NodeType::Layer: - return "Layer"; - } - return "Unknown"; -} - } // namespace pagx diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index c328a90a57..c7fabe1d28 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -193,13 +193,13 @@ static std::string colorSourceToKey(const ColorSource* node) { return ""; } std::ostringstream oss = {}; - switch (node->type()) { - case NodeType::SolidColor: { + switch (node->colorSourceType()) { + case ColorSourceType::SolidColor: { auto solid = static_cast(node); oss << "SolidColor:" << solid->color.toHexString(true); break; } - case NodeType::LinearGradient: { + case ColorSourceType::LinearGradient: { auto grad = static_cast(node); oss << "LinearGradient:" << grad->startPoint.x << "," << grad->startPoint.y << ":" << grad->endPoint.x << "," << grad->endPoint.y << ":" << grad->matrix.toString() << ":"; @@ -208,7 +208,7 @@ static std::string colorSourceToKey(const ColorSource* node) { } break; } - case NodeType::RadialGradient: { + case ColorSourceType::RadialGradient: { auto grad = static_cast(node); oss << "RadialGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->radius << ":" << grad->matrix.toString() << ":"; @@ -217,7 +217,7 @@ static std::string colorSourceToKey(const ColorSource* node) { } break; } - case NodeType::ConicGradient: { + case ColorSourceType::ConicGradient: { auto grad = static_cast(node); oss << "ConicGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->startAngle << ":" << grad->endAngle << ":" << grad->matrix.toString() << ":"; @@ -226,7 +226,7 @@ static std::string colorSourceToKey(const ColorSource* node) { } break; } - case NodeType::DiamondGradient: { + case ColorSourceType::DiamondGradient: { auto grad = static_cast(node); oss << "DiamondGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->halfDiagonal << ":" << grad->matrix.toString() << ":"; @@ -235,15 +235,13 @@ static std::string colorSourceToKey(const ColorSource* node) { } break; } - case NodeType::ImagePattern: { + case ColorSourceType::ImagePattern: { auto pattern = static_cast(node); oss << "ImagePattern:" << pattern->image << ":" << static_cast(pattern->tileModeX) << ":" << static_cast(pattern->tileModeY) << ":" << static_cast(pattern->sampling) << ":" << pattern->matrix.toString(); break; } - default: - break; } return oss.str(); } @@ -275,7 +273,7 @@ class ResourceContext { collectFromLayer(layer.get()); } for (const auto& resource : doc.resources) { - if (resource->type() == NodeType::Composition) { + if (resource->type() == ResourceType::Composition) { auto comp = static_cast(resource.get()); for (const auto& layer : comp->layers) { collectFromLayer(layer.get()); @@ -357,28 +355,28 @@ class ResourceContext { void collectFromVectorElement(const Element* element) { switch (element->type()) { - case NodeType::Path: { + case ElementType::Path: { auto path = static_cast(element); if (!path->data.isEmpty()) { getPathDataId(path->data.toSVGString()); } break; } - case NodeType::Fill: { + case ElementType::Fill: { auto fill = static_cast(element); if (fill->colorSource) { registerColorSource(fill->colorSource.get()); } break; } - case NodeType::Stroke: { + case ElementType::Stroke: { auto stroke = static_cast(element); if (stroke->colorSource) { registerColorSource(stroke->colorSource.get()); } break; } - case NodeType::Group: { + case ElementType::Group: { auto group = static_cast(element); for (const auto& child : group->elements) { collectFromVectorElement(child.get()); @@ -417,8 +415,8 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& stops } static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId) { - switch (node->type()) { - case NodeType::SolidColor: { + switch (node->colorSourceType()) { + case ColorSourceType::SolidColor: { auto solid = static_cast(node); xml.openElement("SolidColor"); if (writeId && !solid->id.empty()) { @@ -428,7 +426,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ xml.closeElementSelfClosing(); break; } - case NodeType::LinearGradient: { + case ColorSourceType::LinearGradient: { auto grad = static_cast(node); xml.openElement("LinearGradient"); if (writeId && !grad->id.empty()) { @@ -450,7 +448,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ } break; } - case NodeType::RadialGradient: { + case ColorSourceType::RadialGradient: { auto grad = static_cast(node); xml.openElement("RadialGradient"); if (writeId && !grad->id.empty()) { @@ -472,7 +470,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ } break; } - case NodeType::ConicGradient: { + case ColorSourceType::ConicGradient: { auto grad = static_cast(node); xml.openElement("ConicGradient"); if (writeId && !grad->id.empty()) { @@ -495,7 +493,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ } break; } - case NodeType::DiamondGradient: { + case ColorSourceType::DiamondGradient: { auto grad = static_cast(node); xml.openElement("DiamondGradient"); if (writeId && !grad->id.empty()) { @@ -517,7 +515,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ } break; } - case NodeType::ImagePattern: { + case ColorSourceType::ImagePattern: { auto pattern = static_cast(node); xml.openElement("ImagePattern"); if (writeId && !pattern->id.empty()) { @@ -539,16 +537,14 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ xml.closeElementSelfClosing(); break; } - default: - break; } } // Write ColorSource with assigned id (for Resources section) static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, const std::string& id) { - switch (node->type()) { - case NodeType::SolidColor: { + switch (node->colorSourceType()) { + case ColorSourceType::SolidColor: { auto solid = static_cast(node); xml.openElement("SolidColor"); xml.addAttribute("id", id); @@ -556,7 +552,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, xml.closeElementSelfClosing(); break; } - case NodeType::LinearGradient: { + case ColorSourceType::LinearGradient: { auto grad = static_cast(node); xml.openElement("LinearGradient"); xml.addAttribute("id", id); @@ -576,7 +572,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, } break; } - case NodeType::RadialGradient: { + case ColorSourceType::RadialGradient: { auto grad = static_cast(node); xml.openElement("RadialGradient"); xml.addAttribute("id", id); @@ -596,7 +592,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, } break; } - case NodeType::ConicGradient: { + case ColorSourceType::ConicGradient: { auto grad = static_cast(node); xml.openElement("ConicGradient"); xml.addAttribute("id", id); @@ -617,7 +613,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, } break; } - case NodeType::DiamondGradient: { + case ColorSourceType::DiamondGradient: { auto grad = static_cast(node); xml.openElement("DiamondGradient"); xml.addAttribute("id", id); @@ -637,7 +633,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, } break; } - case NodeType::ImagePattern: { + case ColorSourceType::ImagePattern: { auto pattern = static_cast(node); xml.openElement("ImagePattern"); xml.addAttribute("id", id); @@ -657,8 +653,6 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, xml.closeElementSelfClosing(); break; } - default: - break; } } @@ -669,7 +663,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, static void writeVectorElement(XMLBuilder& xml, const Element* node, const ResourceContext& ctx) { switch (node->type()) { - case NodeType::Rectangle: { + case ElementType::Rectangle: { auto rect = static_cast(node); xml.openElement("Rectangle"); if (rect->center.x != 0 || rect->center.y != 0) { @@ -683,7 +677,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElementSelfClosing(); break; } - case NodeType::Ellipse: { + case ElementType::Ellipse: { auto ellipse = static_cast(node); xml.openElement("Ellipse"); if (ellipse->center.x != 0 || ellipse->center.y != 0) { @@ -696,7 +690,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElementSelfClosing(); break; } - case NodeType::Polystar: { + case ElementType::Polystar: { auto polystar = static_cast(node); xml.openElement("Polystar"); if (polystar->center.x != 0 || polystar->center.y != 0) { @@ -713,7 +707,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElementSelfClosing(); break; } - case NodeType::Path: { + case ElementType::Path: { auto path = static_cast(node); xml.openElement("Path"); if (!path->data.isEmpty()) { @@ -731,7 +725,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElementSelfClosing(); break; } - case NodeType::TextSpan: { + case ElementType::TextSpan: { auto text = static_cast(node); xml.openElement("TextSpan"); xml.addAttribute("x", text->x); @@ -749,7 +743,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElement(); break; } - case NodeType::Fill: { + case ElementType::Fill: { auto fill = static_cast(node); xml.openElement("Fill"); // Check if ColorSource should be referenced or inlined @@ -783,7 +777,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, } break; } - case NodeType::Stroke: { + case ElementType::Stroke: { auto stroke = static_cast(node); xml.openElement("Stroke"); // Check if ColorSource should be referenced or inlined @@ -829,7 +823,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, } break; } - case NodeType::TrimPath: { + case ElementType::TrimPath: { auto trim = static_cast(node); xml.openElement("TrimPath"); xml.addAttribute("start", trim->start); @@ -841,14 +835,14 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElementSelfClosing(); break; } - case NodeType::RoundCorner: { + case ElementType::RoundCorner: { auto round = static_cast(node); xml.openElement("RoundCorner"); xml.addAttribute("radius", round->radius, 10.0f); xml.closeElementSelfClosing(); break; } - case NodeType::MergePath: { + case ElementType::MergePath: { auto merge = static_cast(node); xml.openElement("MergePath"); if (merge->mode != MergePathMode::Append) { @@ -857,7 +851,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElementSelfClosing(); break; } - case NodeType::TextModifier: { + case ElementType::TextModifier: { auto modifier = static_cast(node); xml.openElement("TextModifier"); if (modifier->anchorPoint.x != 0 || modifier->anchorPoint.y != 0) { @@ -907,7 +901,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, } break; } - case NodeType::TextPath: { + case ElementType::TextPath: { auto textPath = static_cast(node); xml.openElement("TextPath"); xml.addAttribute("path", textPath->path); @@ -922,7 +916,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElementSelfClosing(); break; } - case NodeType::TextLayout: { + case ElementType::TextLayout: { auto layout = static_cast(node); xml.openElement("TextLayout"); xml.addRequiredAttribute("width", layout->width); @@ -941,7 +935,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElementSelfClosing(); break; } - case NodeType::Repeater: { + case ElementType::Repeater: { auto repeater = static_cast(node); xml.openElement("Repeater"); xml.addAttribute("copies", repeater->copies, 3.0f); @@ -964,7 +958,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.closeElementSelfClosing(); break; } - case NodeType::Group: { + case ElementType::Group: { auto group = static_cast(node); xml.openElement("Group"); if (group->anchorPoint.x != 0 || group->anchorPoint.y != 0) { @@ -1002,7 +996,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { switch (node->type()) { - case NodeType::DropShadowStyle: { + case LayerStyleType::DropShadowStyle: { auto style = static_cast(node); xml.openElement("DropShadowStyle"); if (style->blendMode != BlendMode::Normal) { @@ -1017,7 +1011,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.closeElementSelfClosing(); break; } - case NodeType::InnerShadowStyle: { + case LayerStyleType::InnerShadowStyle: { auto style = static_cast(node); xml.openElement("InnerShadowStyle"); if (style->blendMode != BlendMode::Normal) { @@ -1031,7 +1025,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.closeElementSelfClosing(); break; } - case NodeType::BackgroundBlurStyle: { + case LayerStyleType::BackgroundBlurStyle: { auto style = static_cast(node); xml.openElement("BackgroundBlurStyle"); if (style->blendMode != BlendMode::Normal) { @@ -1056,7 +1050,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { switch (node->type()) { - case NodeType::BlurFilter: { + case LayerFilterType::BlurFilter: { auto filter = static_cast(node); xml.openElement("BlurFilter"); xml.addRequiredAttribute("blurrinessX", filter->blurrinessX); @@ -1067,7 +1061,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.closeElementSelfClosing(); break; } - case NodeType::DropShadowFilter: { + case LayerFilterType::DropShadowFilter: { auto filter = static_cast(node); xml.openElement("DropShadowFilter"); xml.addAttribute("offsetX", filter->offsetX); @@ -1079,7 +1073,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.closeElementSelfClosing(); break; } - case NodeType::InnerShadowFilter: { + case LayerFilterType::InnerShadowFilter: { auto filter = static_cast(node); xml.openElement("InnerShadowFilter"); xml.addAttribute("offsetX", filter->offsetX); @@ -1091,7 +1085,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.closeElementSelfClosing(); break; } - case NodeType::BlendFilter: { + case LayerFilterType::BlendFilter: { auto filter = static_cast(node); xml.openElement("BlendFilter"); xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); @@ -1101,7 +1095,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.closeElementSelfClosing(); break; } - case NodeType::ColorMatrixFilter: { + case LayerFilterType::ColorMatrixFilter: { auto filter = static_cast(node); xml.openElement("ColorMatrixFilter"); std::vector values(filter->matrix.begin(), filter->matrix.end()); @@ -1120,7 +1114,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceContext& ctx) { switch (node->type()) { - case NodeType::Image: { + case ResourceType::Image: { auto image = static_cast(node); xml.openElement("Image"); xml.addAttribute("id", image->id); @@ -1128,7 +1122,7 @@ static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceC xml.closeElementSelfClosing(); break; } - case NodeType::PathData: { + case ResourceType::PathData: { auto pathData = static_cast(node); xml.openElement("PathData"); xml.addAttribute("id", pathData->id); @@ -1136,7 +1130,7 @@ static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceC xml.closeElementSelfClosing(); break; } - case NodeType::Composition: { + case ResourceType::Composition: { auto comp = static_cast(node); xml.openElement("Composition"); xml.addAttribute("id", comp->id); @@ -1153,12 +1147,12 @@ static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceC } break; } - case NodeType::SolidColor: - case NodeType::LinearGradient: - case NodeType::RadialGradient: - case NodeType::ConicGradient: - case NodeType::DiamondGradient: - case NodeType::ImagePattern: + case ResourceType::SolidColor: + case ResourceType::LinearGradient: + case ResourceType::RadialGradient: + case ResourceType::ConicGradient: + case ResourceType::DiamondGradient: + case ResourceType::ImagePattern: writeColorSource(xml, static_cast(node), true); break; default: @@ -1267,7 +1261,7 @@ std::string PAGXXMLWriter::Write(const Document& doc) { for (const auto& layer : doc.layers) { std::function collectColorSources = [&](const Layer* layer) { for (const auto& element : layer->contents) { - if (element->type() == NodeType::Fill) { + if (element->type() == ElementType::Fill) { auto fill = static_cast(element.get()); if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { std::string key = colorSourceToKey(fill->colorSource.get()); @@ -1275,7 +1269,7 @@ std::string PAGXXMLWriter::Write(const Document& doc) { colorSourceByKey[key] = fill->colorSource.get(); } } - } else if (element->type() == NodeType::Stroke) { + } else if (element->type() == ElementType::Stroke) { auto stroke = static_cast(element.get()); if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { std::string key = colorSourceToKey(stroke->colorSource.get()); @@ -1283,11 +1277,11 @@ std::string PAGXXMLWriter::Write(const Document& doc) { colorSourceByKey[key] = stroke->colorSource.get(); } } - } else if (element->type() == NodeType::Group) { + } else if (element->type() == ElementType::Group) { auto group = static_cast(element.get()); std::function collectFromGroup = [&](const Group* g) { for (const auto& child : g->elements) { - if (child->type() == NodeType::Fill) { + if (child->type() == ElementType::Fill) { auto fill = static_cast(child.get()); if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { std::string key = colorSourceToKey(fill->colorSource.get()); @@ -1295,7 +1289,7 @@ std::string PAGXXMLWriter::Write(const Document& doc) { colorSourceByKey[key] = fill->colorSource.get(); } } - } else if (child->type() == NodeType::Stroke) { + } else if (child->type() == ElementType::Stroke) { auto stroke = static_cast(child.get()); if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { std::string key = colorSourceToKey(stroke->colorSource.get()); @@ -1303,7 +1297,7 @@ std::string PAGXXMLWriter::Write(const Document& doc) { colorSourceByKey[key] = stroke->colorSource.get(); } } - } else if (child->type() == NodeType::Group) { + } else if (child->type() == ElementType::Group) { collectFromGroup(static_cast(child.get())); } } @@ -1320,11 +1314,11 @@ std::string PAGXXMLWriter::Write(const Document& doc) { // Also collect from Compositions for (const auto& resource : doc.resources) { - if (resource->type() == NodeType::Composition) { + if (resource->type() == ResourceType::Composition) { auto comp = static_cast(resource.get()); std::function collectColorSources = [&](const Layer* layer) { for (const auto& element : layer->contents) { - if (element->type() == NodeType::Fill) { + if (element->type() == ElementType::Fill) { auto fill = static_cast(element.get()); if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { std::string key = colorSourceToKey(fill->colorSource.get()); @@ -1332,7 +1326,7 @@ std::string PAGXXMLWriter::Write(const Document& doc) { colorSourceByKey[key] = fill->colorSource.get(); } } - } else if (element->type() == NodeType::Stroke) { + } else if (element->type() == ElementType::Stroke) { auto stroke = static_cast(element.get()); if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { std::string key = colorSourceToKey(stroke->colorSource.get()); @@ -1340,11 +1334,11 @@ std::string PAGXXMLWriter::Write(const Document& doc) { colorSourceByKey[key] = stroke->colorSource.get(); } } - } else if (element->type() == NodeType::Group) { + } else if (element->type() == ElementType::Group) { auto group = static_cast(element.get()); std::function collectFromGroup = [&](const Group* g) { for (const auto& child : g->elements) { - if (child->type() == NodeType::Fill) { + if (child->type() == ElementType::Fill) { auto fill = static_cast(child.get()); if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { std::string key = colorSourceToKey(fill->colorSource.get()); @@ -1352,7 +1346,7 @@ std::string PAGXXMLWriter::Write(const Document& doc) { colorSourceByKey[key] = fill->colorSource.get(); } } - } else if (child->type() == NodeType::Stroke) { + } else if (child->type() == ElementType::Stroke) { auto stroke = static_cast(child.get()); if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { @@ -1361,7 +1355,7 @@ std::string PAGXXMLWriter::Write(const Document& doc) { colorSourceByKey[key] = stroke->colorSource.get(); } } - } else if (child->type() == NodeType::Group) { + } else if (child->type() == ElementType::Group) { collectFromGroup(static_cast(child.get())); } } diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 310c474e73..0376fed69d 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -1509,21 +1509,21 @@ static bool isSameGeometry(const Element* a, const Element* b) { } switch (a->type()) { - case NodeType::Rectangle: { + case ElementType::Rectangle: { auto rectA = static_cast(a); auto rectB = static_cast(b); return rectA->center.x == rectB->center.x && rectA->center.y == rectB->center.y && rectA->size.width == rectB->size.width && rectA->size.height == rectB->size.height && rectA->roundness == rectB->roundness; } - case NodeType::Ellipse: { + case ElementType::Ellipse: { auto ellipseA = static_cast(a); auto ellipseB = static_cast(b); return ellipseA->center.x == ellipseB->center.x && ellipseA->center.y == ellipseB->center.y && ellipseA->size.width == ellipseB->size.width && ellipseA->size.height == ellipseB->size.height; } - case NodeType::Path: { + case ElementType::Path: { auto pathA = static_cast(a); auto pathB = static_cast(b); return pathA->data.toSVGString() == pathB->data.toSVGString(); @@ -1550,10 +1550,10 @@ static bool isSimpleShapeLayer(const Layer* layer, const Element*& outGeometry, const auto* second = layer->contents[1].get(); // Check if first is geometry and second is painter. - bool firstIsGeometry = (first->type() == NodeType::Rectangle || - first->type() == NodeType::Ellipse || first->type() == NodeType::Path); + bool firstIsGeometry = (first->type() == ElementType::Rectangle || + first->type() == ElementType::Ellipse || first->type() == ElementType::Path); bool secondIsPainter = - (second->type() == NodeType::Fill || second->type() == NodeType::Stroke); + (second->type() == ElementType::Fill || second->type() == ElementType::Stroke); if (firstIsGeometry && secondIsPainter) { outGeometry = first; @@ -1584,8 +1584,8 @@ void SVGParserImpl::mergeAdjacentLayers(std::vector>& lay if (isSimpleShapeLayer(layers[i + 1].get(), geomB, painterB) && isSameGeometry(geomA, geomB)) { // Merge: one has Fill, the other has Stroke. - bool aHasFill = (painterA->type() == NodeType::Fill); - bool bHasFill = (painterB->type() == NodeType::Fill); + bool aHasFill = (painterA->type() == ElementType::Fill); + bool bHasFill = (painterB->type() == ElementType::Fill); if (aHasFill != bHasFill) { // Create merged layer. diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index b94c1577b8..817f1345ed 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -320,29 +320,29 @@ class LayerBuilderImpl { } switch (node->type()) { - case NodeType::Rectangle: + case ElementType::Rectangle: return convertRectangle(static_cast(node)); - case NodeType::Ellipse: + case ElementType::Ellipse: return convertEllipse(static_cast(node)); - case NodeType::Polystar: + case ElementType::Polystar: return convertPolystar(static_cast(node)); - case NodeType::Path: + case ElementType::Path: return convertPath(static_cast(node)); - case NodeType::TextSpan: + case ElementType::TextSpan: return convertTextSpan(static_cast(node)); - case NodeType::Fill: + case ElementType::Fill: return convertFill(static_cast(node)); - case NodeType::Stroke: + case ElementType::Stroke: return convertStroke(static_cast(node)); - case NodeType::TrimPath: + case ElementType::TrimPath: return convertTrimPath(static_cast(node)); - case NodeType::RoundCorner: + case ElementType::RoundCorner: return convertRoundCorner(static_cast(node)); - case NodeType::MergePath: + case ElementType::MergePath: return convertMergePath(static_cast(node)); - case NodeType::Repeater: + case ElementType::Repeater: return convertRepeater(static_cast(node)); - case NodeType::Group: + case ElementType::Group: return convertGroup(static_cast(node)); default: return nullptr; @@ -476,20 +476,20 @@ class LayerBuilderImpl { return nullptr; } - switch (node->type()) { - case NodeType::SolidColor: { + switch (node->colorSourceType()) { + case ColorSourceType::SolidColor: { auto solid = static_cast(node); return tgfx::SolidColor::Make(ToTGFX(solid->color)); } - case NodeType::LinearGradient: { + case ColorSourceType::LinearGradient: { auto grad = static_cast(node); return convertLinearGradient(grad); } - case NodeType::RadialGradient: { + case ColorSourceType::RadialGradient: { auto grad = static_cast(node); return convertRadialGradient(grad); } - case NodeType::ImagePattern: { + case ColorSourceType::ImagePattern: { auto pattern = static_cast(node); return convertImagePattern(pattern); } @@ -586,7 +586,7 @@ class LayerBuilderImpl { return nullptr; } for (const auto& resource : *_resources) { - if (resource->type() == NodeType::Image) { + if (resource->type() == ResourceType::Image) { auto imageNode = static_cast(resource.get()); if (imageNode->id == resourceId) { if (imageNode->source.find("data:") == 0) { @@ -683,12 +683,12 @@ class LayerBuilderImpl { } switch (node->type()) { - case NodeType::DropShadowStyle: { + case LayerStyleType::DropShadowStyle: { auto style = static_cast(node); return tgfx::DropShadowStyle::Make(style->offsetX, style->offsetY, style->blurrinessX, style->blurrinessY, ToTGFX(style->color)); } - case NodeType::InnerShadowStyle: { + case LayerStyleType::InnerShadowStyle: { auto style = static_cast(node); return tgfx::InnerShadowStyle::Make(style->offsetX, style->offsetY, style->blurrinessX, style->blurrinessY, ToTGFX(style->color)); @@ -704,11 +704,11 @@ class LayerBuilderImpl { } switch (node->type()) { - case NodeType::BlurFilter: { + case LayerFilterType::BlurFilter: { auto filter = static_cast(node); return tgfx::BlurFilter::Make(filter->blurrinessX, filter->blurrinessY); } - case NodeType::DropShadowFilter: { + case LayerFilterType::DropShadowFilter: { auto filter = static_cast(node); return tgfx::DropShadowFilter::Make(filter->offsetX, filter->offsetY, filter->blurrinessX, filter->blurrinessY, ToTGFX(filter->color)); diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 5327492670..1616d79e58 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -196,29 +196,29 @@ PAG_TEST(PAGXTest, PAGXNodeBasic) { rect->size.height = 80; rect->roundness = 10; - EXPECT_EQ(rect->type(), pagx::NodeType::Rectangle); - EXPECT_STREQ(pagx::NodeTypeName(rect->type()), "Rectangle"); + EXPECT_EQ(rect->type(), pagx::ElementType::Rectangle); + EXPECT_STREQ(pagx::ElementTypeName(rect->type()), "Rectangle"); EXPECT_FLOAT_EQ(rect->center.x, 50); EXPECT_FLOAT_EQ(rect->size.width, 100); // Test Path creation auto path = std::make_unique(); path->data = pagx::PathData::FromSVGString("M0 0 L100 100"); - EXPECT_EQ(path->type(), pagx::NodeType::Path); + EXPECT_EQ(path->type(), pagx::ElementType::Path); EXPECT_GT(path->data.verbs().size(), 0u); // Test Fill creation auto fill = std::make_unique(); fill->color = "#FF0000"; fill->alpha = 0.8f; - EXPECT_EQ(fill->type(), pagx::NodeType::Fill); + EXPECT_EQ(fill->type(), pagx::ElementType::Fill); EXPECT_EQ(fill->color, "#FF0000"); // Test Group with children auto group = std::make_unique(); group->elements.push_back(std::move(rect)); group->elements.push_back(std::move(fill)); - EXPECT_EQ(group->type(), pagx::NodeType::Group); + EXPECT_EQ(group->type(), pagx::ElementType::Group); EXPECT_EQ(group->elements.size(), 2u); } @@ -364,7 +364,7 @@ PAG_TEST(PAGXTest, ColorSources) { // Test SolidColor auto solid = std::make_unique(); solid->color = pagx::Color::FromRGBA(1.0f, 0.0f, 0.0f, 1.0f); - EXPECT_EQ(solid->type(), pagx::NodeType::SolidColor); + EXPECT_EQ(solid->colorSourceType(), pagx::ColorSourceType::SolidColor); EXPECT_FLOAT_EQ(solid->color.red, 1.0f); // Test LinearGradient @@ -385,7 +385,7 @@ PAG_TEST(PAGXTest, ColorSources) { linear->colorStops.push_back(stop1); linear->colorStops.push_back(stop2); - EXPECT_EQ(linear->type(), pagx::NodeType::LinearGradient); + EXPECT_EQ(linear->colorSourceType(), pagx::ColorSourceType::LinearGradient); EXPECT_EQ(linear->colorStops.size(), 2u); // Test RadialGradient @@ -395,7 +395,7 @@ PAG_TEST(PAGXTest, ColorSources) { radial->radius = 50; radial->colorStops = linear->colorStops; - EXPECT_EQ(radial->type(), pagx::NodeType::RadialGradient); + EXPECT_EQ(radial->colorSourceType(), pagx::ColorSourceType::RadialGradient); } /** @@ -424,8 +424,8 @@ PAG_TEST(PAGXTest, LayerStylesFilters) { EXPECT_EQ(layer->styles.size(), 1u); EXPECT_EQ(layer->filters.size(), 1u); - EXPECT_EQ(layer->styles[0]->type(), pagx::NodeType::DropShadowStyle); - EXPECT_EQ(layer->filters[0]->type(), pagx::NodeType::BlurFilter); + EXPECT_EQ(layer->styles[0]->type(), pagx::LayerStyleType::DropShadowStyle); + EXPECT_EQ(layer->filters[0]->type(), pagx::LayerFilterType::BlurFilter); } } // namespace pag From fe7ddcef672653affc8b863167767702766c316f Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 18:01:57 +0800 Subject: [PATCH 094/678] Consolidate enum types into model headers and simplify Resource and ColorSource hierarchies. --- pagx/include/pagx/model/BlendFilter.h | 2 +- pagx/include/pagx/model/ColorSource.h | 10 +- pagx/include/pagx/model/ColorStop.h | 2 +- pagx/include/pagx/model/ConicGradient.h | 13 +- pagx/include/pagx/model/DiamondGradient.h | 13 +- pagx/include/pagx/model/Document.h | 19 +- pagx/include/pagx/model/DropShadowFilter.h | 2 +- pagx/include/pagx/model/DropShadowStyle.h | 2 +- pagx/include/pagx/model/Ellipse.h | 3 +- pagx/include/pagx/model/Fill.h | 14 +- pagx/include/pagx/model/Group.h | 2 +- pagx/include/pagx/model/ImagePattern.h | 25 +- pagx/include/pagx/model/InnerShadowFilter.h | 2 +- pagx/include/pagx/model/InnerShadowStyle.h | 2 +- pagx/include/pagx/model/Layer.h | 16 +- pagx/include/pagx/model/LinearGradient.h | 13 +- pagx/include/pagx/model/MergePath.h | 16 +- pagx/include/pagx/model/PathData.h | 3 +- pagx/include/pagx/model/Polystar.h | 15 +- pagx/include/pagx/model/RadialGradient.h | 13 +- pagx/include/pagx/model/RangeSelector.h | 52 +++- pagx/include/pagx/model/Rectangle.h | 3 +- pagx/include/pagx/model/Repeater.h | 15 +- pagx/include/pagx/model/Resource.h | 28 +- pagx/include/pagx/model/SolidColor.h | 12 +- pagx/include/pagx/model/Stroke.h | 2 +- pagx/include/pagx/model/TextLayout.h | 41 ++- pagx/include/pagx/model/TextModifier.h | 9 +- pagx/include/pagx/model/TextPath.h | 13 +- pagx/include/pagx/model/TextSelector.h | 36 +++ pagx/include/pagx/model/TextSpan.h | 5 +- pagx/include/pagx/model/TrimPath.h | 13 +- pagx/include/pagx/model/types/Placement.h | 6 +- pagx/src/PAGXElement.cpp | 12 - pagx/src/PAGXTypes.cpp | 320 +++----------------- pagx/src/PAGXXMLParser.cpp | 33 +- pagx/src/PAGXXMLParser.h | 33 ++ pagx/src/PAGXXMLWriter.cpp | 97 +++--- pagx/src/svg/PAGXSVGParser.cpp | 2 +- pagx/src/svg/SVGParserInternal.h | 13 + pagx/src/tgfx/LayerBuilder.cpp | 31 +- test/src/PAGXTest.cpp | 17 +- 42 files changed, 486 insertions(+), 494 deletions(-) create mode 100644 pagx/include/pagx/model/TextSelector.h diff --git a/pagx/include/pagx/model/BlendFilter.h b/pagx/include/pagx/model/BlendFilter.h index eb078137d9..9335c684cd 100644 --- a/pagx/include/pagx/model/BlendFilter.h +++ b/pagx/include/pagx/model/BlendFilter.h @@ -20,7 +20,7 @@ #include "pagx/model/LayerFilter.h" #include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Color.h" namespace pagx { diff --git a/pagx/include/pagx/model/ColorSource.h b/pagx/include/pagx/model/ColorSource.h index 3567500729..ac0bff06f7 100644 --- a/pagx/include/pagx/model/ColorSource.h +++ b/pagx/include/pagx/model/ColorSource.h @@ -18,8 +18,6 @@ #pragma once -#include "pagx/model/Resource.h" - namespace pagx { /** @@ -41,14 +39,16 @@ const char* ColorSourceTypeName(ColorSourceType type); /** * Base class for color sources (SolidColor, gradients, ImagePattern). - * ColorSource can be used both inline in painters and as standalone resources. + * ColorSource can be used both inline in painters and as standalone resources in defs. */ -class ColorSource : public Resource { +class ColorSource { public: + virtual ~ColorSource() = default; + /** * Returns the color source type of this color source. */ - virtual ColorSourceType colorSourceType() const = 0; + virtual ColorSourceType type() const = 0; protected: ColorSource() = default; diff --git a/pagx/include/pagx/model/ColorStop.h b/pagx/include/pagx/model/ColorStop.h index e7b51fcf56..8e0fb8398e 100644 --- a/pagx/include/pagx/model/ColorStop.h +++ b/pagx/include/pagx/model/ColorStop.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Color.h" namespace pagx { diff --git a/pagx/include/pagx/model/ConicGradient.h b/pagx/include/pagx/model/ConicGradient.h index fec15739d3..3353317c62 100644 --- a/pagx/include/pagx/model/ConicGradient.h +++ b/pagx/include/pagx/model/ConicGradient.h @@ -22,7 +22,8 @@ #include #include "pagx/model/ColorSource.h" #include "pagx/model/ColorStop.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Matrix.h" +#include "pagx/model/types/Point.h" namespace pagx { @@ -38,17 +39,9 @@ class ConicGradient : public ColorSource { Matrix matrix = {}; std::vector colorStops = {}; - ColorSourceType colorSourceType() const override { + ColorSourceType type() const override { return ColorSourceType::ConicGradient; } - - ResourceType type() const override { - return ResourceType::ConicGradient; - } - - const std::string& resourceId() const override { - return id; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/DiamondGradient.h b/pagx/include/pagx/model/DiamondGradient.h index 75b8e84023..3eefe08300 100644 --- a/pagx/include/pagx/model/DiamondGradient.h +++ b/pagx/include/pagx/model/DiamondGradient.h @@ -22,7 +22,8 @@ #include #include "pagx/model/ColorSource.h" #include "pagx/model/ColorStop.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Matrix.h" +#include "pagx/model/types/Point.h" namespace pagx { @@ -37,17 +38,9 @@ class DiamondGradient : public ColorSource { Matrix matrix = {}; std::vector colorStops = {}; - ColorSourceType colorSourceType() const override { + ColorSourceType type() const override { return ColorSourceType::DiamondGradient; } - - ResourceType type() const override { - return ResourceType::DiamondGradient; - } - - const std::string& resourceId() const override { - return id; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Document.h b/pagx/include/pagx/model/Document.h index b303d731c9..47a7221ec0 100644 --- a/pagx/include/pagx/model/Document.h +++ b/pagx/include/pagx/model/Document.h @@ -22,7 +22,9 @@ #include #include #include -#include "pagx/model/Model.h" +#include "pagx/model/ColorSource.h" +#include "pagx/model/Layer.h" +#include "pagx/model/Resource.h" namespace pagx { @@ -51,11 +53,17 @@ class Document { float height = 0; /** - * Resources (images, gradients, compositions, etc.). + * Resources (images, paths, compositions). * These can be referenced by "#id" in the document. */ std::vector> resources = {}; + /** + * Color sources (gradients, solid colors, patterns). + * These can be referenced by "#id" in fills and strokes. + */ + std::vector> colorSources = {}; + /** * Top-level layers. */ @@ -100,6 +108,12 @@ class Document { */ Resource* findResource(const std::string& id) const; + /** + * Finds a color source by ID. + * Returns nullptr if not found. + */ + ColorSource* findColorSource(const std::string& id) const; + /** * Finds a layer by ID (searches recursively). * Returns nullptr if not found. @@ -111,6 +125,7 @@ class Document { Document() = default; mutable std::unordered_map resourceMap = {}; + mutable std::unordered_map colorSourceMap = {}; mutable bool resourceMapDirty = true; void rebuildResourceMap() const; diff --git a/pagx/include/pagx/model/DropShadowFilter.h b/pagx/include/pagx/model/DropShadowFilter.h index 8619dd31b5..fc97c755ae 100644 --- a/pagx/include/pagx/model/DropShadowFilter.h +++ b/pagx/include/pagx/model/DropShadowFilter.h @@ -19,7 +19,7 @@ #pragma once #include "pagx/model/LayerFilter.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Color.h" namespace pagx { diff --git a/pagx/include/pagx/model/DropShadowStyle.h b/pagx/include/pagx/model/DropShadowStyle.h index a6e515284d..b6c3426ccd 100644 --- a/pagx/include/pagx/model/DropShadowStyle.h +++ b/pagx/include/pagx/model/DropShadowStyle.h @@ -20,7 +20,7 @@ #include "pagx/model/LayerStyle.h" #include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Color.h" namespace pagx { diff --git a/pagx/include/pagx/model/Ellipse.h b/pagx/include/pagx/model/Ellipse.h index 6e15085ae4..d9b8fc31d4 100644 --- a/pagx/include/pagx/model/Ellipse.h +++ b/pagx/include/pagx/model/Ellipse.h @@ -19,7 +19,8 @@ #pragma once #include "pagx/model/Element.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Point.h" +#include "pagx/model/types/Size.h" namespace pagx { diff --git a/pagx/include/pagx/model/Fill.h b/pagx/include/pagx/model/Fill.h index 841839b94a..f3c257adb7 100644 --- a/pagx/include/pagx/model/Fill.h +++ b/pagx/include/pagx/model/Fill.h @@ -23,11 +23,21 @@ #include "pagx/model/ColorSource.h" #include "pagx/model/Element.h" #include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/FillRule.h" #include "pagx/model/types/Placement.h" namespace pagx { +/** + * Fill rules for paths. + */ +enum class FillRule { + Winding, + EvenOdd +}; + +std::string FillRuleToString(FillRule rule); +FillRule FillRuleFromString(const std::string& str); + /** * Fill represents a fill painter that fills shapes with a solid color, gradient, or pattern. The * color can be specified as a simple color string (e.g., "#FF0000"), a reference to a defined @@ -67,7 +77,7 @@ class Fill : public Element { * The placement of the fill relative to strokes (Background or Foreground). The default value is * Background. */ - Placement placement = Placement::Background; + LayerPlacement placement = LayerPlacement::Background; ElementType type() const override { return ElementType::Fill; diff --git a/pagx/include/pagx/model/Group.h b/pagx/include/pagx/model/Group.h index 005752646f..93fa6c7d46 100644 --- a/pagx/include/pagx/model/Group.h +++ b/pagx/include/pagx/model/Group.h @@ -21,7 +21,7 @@ #include #include #include "pagx/model/Element.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Point.h" namespace pagx { diff --git a/pagx/include/pagx/model/ImagePattern.h b/pagx/include/pagx/model/ImagePattern.h index 44c496b79f..e2cce77eb0 100644 --- a/pagx/include/pagx/model/ImagePattern.h +++ b/pagx/include/pagx/model/ImagePattern.h @@ -20,12 +20,23 @@ #include #include "pagx/model/ColorSource.h" -#include "pagx/model/types/SamplingMode.h" +#include "pagx/model/types/Matrix.h" #include "pagx/model/types/TileMode.h" -#include "pagx/model/types/Types.h" namespace pagx { +/** + * Sampling modes for images. + */ +enum class SamplingMode { + Nearest, + Linear, + Mipmap +}; + +std::string SamplingModeToString(SamplingMode mode); +SamplingMode SamplingModeFromString(const std::string& str); + /** * An image pattern. */ @@ -38,17 +49,9 @@ class ImagePattern : public ColorSource { SamplingMode sampling = SamplingMode::Linear; Matrix matrix = {}; - ColorSourceType colorSourceType() const override { + ColorSourceType type() const override { return ColorSourceType::ImagePattern; } - - ResourceType type() const override { - return ResourceType::ImagePattern; - } - - const std::string& resourceId() const override { - return id; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/InnerShadowFilter.h b/pagx/include/pagx/model/InnerShadowFilter.h index c27de19407..d119a01a23 100644 --- a/pagx/include/pagx/model/InnerShadowFilter.h +++ b/pagx/include/pagx/model/InnerShadowFilter.h @@ -19,7 +19,7 @@ #pragma once #include "pagx/model/LayerFilter.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Color.h" namespace pagx { diff --git a/pagx/include/pagx/model/InnerShadowStyle.h b/pagx/include/pagx/model/InnerShadowStyle.h index 6885667587..1cc3bb7624 100644 --- a/pagx/include/pagx/model/InnerShadowStyle.h +++ b/pagx/include/pagx/model/InnerShadowStyle.h @@ -20,7 +20,7 @@ #include "pagx/model/LayerStyle.h" #include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Color.h" namespace pagx { diff --git a/pagx/include/pagx/model/Layer.h b/pagx/include/pagx/model/Layer.h index e5d49cdbe2..e0e17de53a 100644 --- a/pagx/include/pagx/model/Layer.h +++ b/pagx/include/pagx/model/Layer.h @@ -26,11 +26,23 @@ #include "pagx/model/LayerFilter.h" #include "pagx/model/LayerStyle.h" #include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/MaskType.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Matrix.h" +#include "pagx/model/types/Rect.h" namespace pagx { +/** + * Mask types for layer masking. + */ +enum class MaskType { + Alpha, + Luminance, + Contour +}; + +std::string MaskTypeToString(MaskType type); +MaskType MaskTypeFromString(const std::string& str); + /** * Layer represents a layer node that can contain vector elements, layer styles, filters, and child * layers. It is the main building block for composing visual content in a PAGX document. diff --git a/pagx/include/pagx/model/LinearGradient.h b/pagx/include/pagx/model/LinearGradient.h index 919ecbec02..8d3915c012 100644 --- a/pagx/include/pagx/model/LinearGradient.h +++ b/pagx/include/pagx/model/LinearGradient.h @@ -22,7 +22,8 @@ #include #include "pagx/model/ColorSource.h" #include "pagx/model/ColorStop.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Matrix.h" +#include "pagx/model/types/Point.h" namespace pagx { @@ -37,17 +38,9 @@ class LinearGradient : public ColorSource { Matrix matrix = {}; std::vector colorStops = {}; - ColorSourceType colorSourceType() const override { + ColorSourceType type() const override { return ColorSourceType::LinearGradient; } - - ResourceType type() const override { - return ResourceType::LinearGradient; - } - - const std::string& resourceId() const override { - return id; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/MergePath.h b/pagx/include/pagx/model/MergePath.h index aa5ddb62ff..d048a5120e 100644 --- a/pagx/include/pagx/model/MergePath.h +++ b/pagx/include/pagx/model/MergePath.h @@ -18,11 +18,25 @@ #pragma once +#include #include "pagx/model/Element.h" -#include "pagx/model/types/MergePathMode.h" namespace pagx { +/** + * Path merge modes (boolean operations). + */ +enum class MergePathMode { + Append, + Union, + Intersect, + Xor, + Difference +}; + +std::string MergePathModeToString(MergePathMode mode); +MergePathMode MergePathModeFromString(const std::string& str); + /** * MergePath is a path modifier that merges multiple paths using boolean operations. It can append, * add, subtract, intersect, or exclude paths from each other. diff --git a/pagx/include/pagx/model/PathData.h b/pagx/include/pagx/model/PathData.h index d2d8395666..17079d22b5 100644 --- a/pagx/include/pagx/model/PathData.h +++ b/pagx/include/pagx/model/PathData.h @@ -20,7 +20,8 @@ #include #include -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Matrix.h" +#include "pagx/model/types/Rect.h" namespace pagx { diff --git a/pagx/include/pagx/model/Polystar.h b/pagx/include/pagx/model/Polystar.h index 01c70e2c36..fe98727d85 100644 --- a/pagx/include/pagx/model/Polystar.h +++ b/pagx/include/pagx/model/Polystar.h @@ -18,12 +18,23 @@ #pragma once +#include #include "pagx/model/Element.h" -#include "pagx/model/types/PolystarType.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Point.h" namespace pagx { +/** + * Polystar types. + */ +enum class PolystarType { + Polygon, + Star +}; + +std::string PolystarTypeToString(PolystarType type); +PolystarType PolystarTypeFromString(const std::string& str); + /** * Polystar represents a polygon or star shape with configurable points, radii, and roundness. */ diff --git a/pagx/include/pagx/model/RadialGradient.h b/pagx/include/pagx/model/RadialGradient.h index 9bb3cd09bd..4a43976111 100644 --- a/pagx/include/pagx/model/RadialGradient.h +++ b/pagx/include/pagx/model/RadialGradient.h @@ -22,7 +22,8 @@ #include #include "pagx/model/ColorSource.h" #include "pagx/model/ColorStop.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Matrix.h" +#include "pagx/model/types/Point.h" namespace pagx { @@ -37,17 +38,9 @@ class RadialGradient : public ColorSource { Matrix matrix = {}; std::vector colorStops = {}; - ColorSourceType colorSourceType() const override { + ColorSourceType type() const override { return ColorSourceType::RadialGradient; } - - ResourceType type() const override { - return ResourceType::RadialGradient; - } - - const std::string& resourceId() const override { - return id; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/RangeSelector.h b/pagx/include/pagx/model/RangeSelector.h index ce60587625..0e8184c4b8 100644 --- a/pagx/include/pagx/model/RangeSelector.h +++ b/pagx/include/pagx/model/RangeSelector.h @@ -18,16 +18,56 @@ #pragma once -#include "pagx/model/types/SelectorMode.h" -#include "pagx/model/types/SelectorShape.h" -#include "pagx/model/types/SelectorUnit.h" +#include +#include "pagx/model/TextSelector.h" namespace pagx { +/** + * Range selector unit. + */ +enum class SelectorUnit { + Index, + Percentage +}; + +std::string SelectorUnitToString(SelectorUnit unit); +SelectorUnit SelectorUnitFromString(const std::string& str); + +/** + * Range selector shape. + */ +enum class SelectorShape { + Square, + RampUp, + RampDown, + Triangle, + Round, + Smooth +}; + +std::string SelectorShapeToString(SelectorShape shape); +SelectorShape SelectorShapeFromString(const std::string& str); + +/** + * Range selector combination mode. + */ +enum class SelectorMode { + Add, + Subtract, + Intersect, + Min, + Max, + Difference +}; + +std::string SelectorModeToString(SelectorMode mode); +SelectorMode SelectorModeFromString(const std::string& str); + /** * Range selector for text modifier. */ -class RangeSelector { +class RangeSelector : public TextSelector { public: float start = 0; float end = 1; @@ -40,6 +80,10 @@ class RangeSelector { float weight = 1; bool randomizeOrder = false; int randomSeed = 0; + + TextSelectorType type() const override { + return TextSelectorType::RangeSelector; + } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Rectangle.h b/pagx/include/pagx/model/Rectangle.h index f591dedbc1..d70ecb5fb4 100644 --- a/pagx/include/pagx/model/Rectangle.h +++ b/pagx/include/pagx/model/Rectangle.h @@ -19,7 +19,8 @@ #pragma once #include "pagx/model/Element.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Point.h" +#include "pagx/model/types/Size.h" namespace pagx { diff --git a/pagx/include/pagx/model/Repeater.h b/pagx/include/pagx/model/Repeater.h index 798a627593..d174bab030 100644 --- a/pagx/include/pagx/model/Repeater.h +++ b/pagx/include/pagx/model/Repeater.h @@ -18,12 +18,23 @@ #pragma once +#include #include "pagx/model/Element.h" -#include "pagx/model/types/RepeaterOrder.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Point.h" namespace pagx { +/** + * Repeater stacking order. + */ +enum class RepeaterOrder { + BelowOriginal, + AboveOriginal +}; + +std::string RepeaterOrderToString(RepeaterOrder order); +RepeaterOrder RepeaterOrderFromString(const std::string& str); + /** * Repeater is a modifier that creates multiple copies of preceding elements with progressive * transformations. Each copy can have an incremental offset in position, rotation, scale, and diff --git a/pagx/include/pagx/model/Resource.h b/pagx/include/pagx/model/Resource.h index a2de13c0c1..8216aef3c0 100644 --- a/pagx/include/pagx/model/Resource.h +++ b/pagx/include/pagx/model/Resource.h @@ -37,31 +37,7 @@ enum class ResourceType { /** * A composition resource containing layers. */ - Composition, - /** - * A solid color resource. - */ - SolidColor, - /** - * A linear gradient resource. - */ - LinearGradient, - /** - * A radial gradient resource. - */ - RadialGradient, - /** - * A conic gradient resource. - */ - ConicGradient, - /** - * A diamond gradient resource. - */ - DiamondGradient, - /** - * An image pattern resource. - */ - ImagePattern + Composition }; /** @@ -71,7 +47,7 @@ const char* ResourceTypeName(ResourceType type); /** * Resource is the base class for all resources in a PAGX document. Resources are reusable items - * that can be referenced by ID (e.g., "#imageId", "#gradientId"). + * that can be referenced by ID (e.g., "#imageId"). */ class Resource { public: diff --git a/pagx/include/pagx/model/SolidColor.h b/pagx/include/pagx/model/SolidColor.h index f83123abc9..fb7aba8a40 100644 --- a/pagx/include/pagx/model/SolidColor.h +++ b/pagx/include/pagx/model/SolidColor.h @@ -20,7 +20,7 @@ #include #include "pagx/model/ColorSource.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/types/Color.h" namespace pagx { @@ -32,17 +32,9 @@ class SolidColor : public ColorSource { std::string id = {}; Color color = {}; - ColorSourceType colorSourceType() const override { + ColorSourceType type() const override { return ColorSourceType::SolidColor; } - - ResourceType type() const override { - return ResourceType::SolidColor; - } - - const std::string& resourceId() const override { - return id; - } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Stroke.h b/pagx/include/pagx/model/Stroke.h index 5e6a57a1b5..cee9cbaa45 100644 --- a/pagx/include/pagx/model/Stroke.h +++ b/pagx/include/pagx/model/Stroke.h @@ -132,7 +132,7 @@ class Stroke : public Element { * The placement of the stroke relative to fills (Background or Foreground). The default value is * Background. */ - Placement placement = Placement::Background; + LayerPlacement placement = LayerPlacement::Background; ElementType type() const override { return ElementType::Stroke; diff --git a/pagx/include/pagx/model/TextLayout.h b/pagx/include/pagx/model/TextLayout.h index d0c6fe3be7..494cae5c0a 100644 --- a/pagx/include/pagx/model/TextLayout.h +++ b/pagx/include/pagx/model/TextLayout.h @@ -18,13 +18,48 @@ #pragma once +#include #include "pagx/model/Element.h" -#include "pagx/model/types/Overflow.h" -#include "pagx/model/types/TextAlign.h" -#include "pagx/model/types/VerticalAlign.h" namespace pagx { +/** + * Text horizontal alignment. + */ +enum class TextAlign { + Left, + Center, + Right, + Justify +}; + +std::string TextAlignToString(TextAlign align); +TextAlign TextAlignFromString(const std::string& str); + +/** + * Text vertical alignment. + */ +enum class VerticalAlign { + Top, + Center, + Bottom +}; + +std::string VerticalAlignToString(VerticalAlign align); +VerticalAlign VerticalAlignFromString(const std::string& str); + +/** + * Text overflow handling. + */ +enum class Overflow { + Clip, + Visible, + Ellipsis +}; + +std::string OverflowToString(Overflow overflow); +Overflow OverflowFromString(const std::string& str); + /** * TextLayout is a text animator that controls text layout within a bounding box. It provides * options for text alignment, line height, indentation, and overflow handling. diff --git a/pagx/include/pagx/model/TextModifier.h b/pagx/include/pagx/model/TextModifier.h index d39c1c04d6..a6ad2e29ea 100644 --- a/pagx/include/pagx/model/TextModifier.h +++ b/pagx/include/pagx/model/TextModifier.h @@ -18,11 +18,12 @@ #pragma once +#include #include #include #include "pagx/model/Element.h" -#include "pagx/model/RangeSelector.h" -#include "pagx/model/types/Types.h" +#include "pagx/model/TextSelector.h" +#include "pagx/model/types/Point.h" namespace pagx { @@ -85,9 +86,9 @@ class TextModifier : public Element { float strokeWidth = -1; /** - * The range selectors that determine which characters are affected by this modifier. + * The selectors that determine which characters are affected by this modifier. */ - std::vector rangeSelectors = {}; + std::vector> selectors = {}; ElementType type() const override { return ElementType::TextModifier; diff --git a/pagx/include/pagx/model/TextPath.h b/pagx/include/pagx/model/TextPath.h index f8c20317d4..e1af9f90eb 100644 --- a/pagx/include/pagx/model/TextPath.h +++ b/pagx/include/pagx/model/TextPath.h @@ -20,10 +20,21 @@ #include #include "pagx/model/Element.h" -#include "pagx/model/types/TextPathAlign.h" namespace pagx { +/** + * Text path alignment. + */ +enum class TextPathAlign { + Start, + Center, + End +}; + +std::string TextPathAlignToString(TextPathAlign align); +TextPathAlign TextPathAlignFromString(const std::string& str); + /** * TextPath is a text animator that places text along a path. It allows text to follow the contour * of a referenced path shape. diff --git a/pagx/include/pagx/model/TextSelector.h b/pagx/include/pagx/model/TextSelector.h new file mode 100644 index 0000000000..27a3f6fb6e --- /dev/null +++ b/pagx/include/pagx/model/TextSelector.h @@ -0,0 +1,36 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +enum class TextSelectorType { + RangeSelector +}; + +class TextSelector { + public: + virtual ~TextSelector() = default; + virtual TextSelectorType type() const = 0; + + protected: + TextSelector() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/TextSpan.h b/pagx/include/pagx/model/TextSpan.h index 0488721f72..bd568352f9 100644 --- a/pagx/include/pagx/model/TextSpan.h +++ b/pagx/include/pagx/model/TextSpan.h @@ -20,7 +20,6 @@ #include #include "pagx/model/Element.h" -#include "pagx/model/types/FontStyle.h" namespace pagx { @@ -56,9 +55,9 @@ class TextSpan : public Element { int fontWeight = 400; /** - * The font style (Normal, Italic, or Oblique). The default value is Normal. + * The font style (e.g., "normal", "italic", "oblique"). The default value is "normal". */ - FontStyle fontStyle = FontStyle::Normal; + std::string fontStyle = "normal"; /** * The tracking value that adjusts spacing between characters. The default value is 0. diff --git a/pagx/include/pagx/model/TrimPath.h b/pagx/include/pagx/model/TrimPath.h index d8eedaa5e7..3da8c47fc3 100644 --- a/pagx/include/pagx/model/TrimPath.h +++ b/pagx/include/pagx/model/TrimPath.h @@ -18,11 +18,22 @@ #pragma once +#include #include "pagx/model/Element.h" -#include "pagx/model/types/TrimType.h" namespace pagx { +/** + * Trim path types. + */ +enum class TrimType { + Separate, + Continuous +}; + +std::string TrimTypeToString(TrimType type); +TrimType TrimTypeFromString(const std::string& str); + /** * TrimPath is a path modifier that trims paths to a specified range. It can be used to animate * path drawing or reveal effects by adjusting the start and end values. diff --git a/pagx/include/pagx/model/types/Placement.h b/pagx/include/pagx/model/types/Placement.h index 2cb3aaf32b..294c153d98 100644 --- a/pagx/include/pagx/model/types/Placement.h +++ b/pagx/include/pagx/model/types/Placement.h @@ -25,12 +25,12 @@ namespace pagx { /** * Placement of fill/stroke relative to child layers. */ -enum class Placement { +enum class LayerPlacement { Background, Foreground }; -std::string PlacementToString(Placement placement); -Placement PlacementFromString(const std::string& str); +std::string LayerPlacementToString(LayerPlacement placement); +LayerPlacement LayerPlacementFromString(const std::string& str); } // namespace pagx diff --git a/pagx/src/PAGXElement.cpp b/pagx/src/PAGXElement.cpp index c60c07ef1d..05ef236eb1 100644 --- a/pagx/src/PAGXElement.cpp +++ b/pagx/src/PAGXElement.cpp @@ -32,18 +32,6 @@ const char* ResourceTypeName(ResourceType type) { return "PathData"; case ResourceType::Composition: return "Composition"; - case ResourceType::SolidColor: - return "SolidColor"; - case ResourceType::LinearGradient: - return "LinearGradient"; - case ResourceType::RadialGradient: - return "RadialGradient"; - case ResourceType::ConicGradient: - return "ConicGradient"; - case ResourceType::DiamondGradient: - return "DiamondGradient"; - case ResourceType::ImagePattern: - return "ImagePattern"; default: return "Unknown"; } diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp index ab8a39121d..895840e064 100644 --- a/pagx/src/PAGXTypes.cpp +++ b/pagx/src/PAGXTypes.cpp @@ -16,292 +16,46 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "pagx/model/types/Types.h" -#include "pagx/model/types/Enums.h" -#include -#include -#include +#include #include +#include "pagx/model/Fill.h" +#include "pagx/model/ImagePattern.h" +#include "pagx/model/Layer.h" +#include "pagx/model/MergePath.h" +#include "pagx/model/Polystar.h" +#include "pagx/model/RangeSelector.h" +#include "pagx/model/Repeater.h" +#include "pagx/model/Stroke.h" +#include "pagx/model/TextLayout.h" +#include "pagx/model/TextPath.h" +#include "pagx/model/TrimPath.h" +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/Placement.h" +#include "pagx/model/types/TileMode.h" namespace pagx { -//============================================================================== -// Color implementation -//============================================================================== - -Color Color::FromHex(uint32_t hex, bool hasAlpha) { - Color color = {}; - if (hasAlpha) { - color.red = static_cast((hex >> 24) & 0xFF) / 255.0f; - color.green = static_cast((hex >> 16) & 0xFF) / 255.0f; - color.blue = static_cast((hex >> 8) & 0xFF) / 255.0f; - color.alpha = static_cast(hex & 0xFF) / 255.0f; - } else { - color.red = static_cast((hex >> 16) & 0xFF) / 255.0f; - color.green = static_cast((hex >> 8) & 0xFF) / 255.0f; - color.blue = static_cast(hex & 0xFF) / 255.0f; - color.alpha = 1.0f; - } - return color; -} - -Color Color::FromRGBA(float r, float g, float b, float a) { - return {r, g, b, a}; -} - -static int ParseHexDigit(char c) { - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'a' && c <= 'f') { - return c - 'a' + 10; - } - if (c >= 'A' && c <= 'F') { - return c - 'A' + 10; - } - return 0; -} - -Color Color::Parse(const std::string& str) { - if (str.empty()) { - return {}; - } - if (str[0] == '#') { - auto hex = str.substr(1); - if (hex.size() == 3) { - int r = ParseHexDigit(hex[0]); - int g = ParseHexDigit(hex[1]); - int b = ParseHexDigit(hex[2]); - return Color::FromRGBA(static_cast(r * 17) / 255.0f, - static_cast(g * 17) / 255.0f, - static_cast(b * 17) / 255.0f, 1.0f); - } - if (hex.size() == 6) { - int r = ParseHexDigit(hex[0]) * 16 + ParseHexDigit(hex[1]); - int g = ParseHexDigit(hex[2]) * 16 + ParseHexDigit(hex[3]); - int b = ParseHexDigit(hex[4]) * 16 + ParseHexDigit(hex[5]); - return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, - static_cast(b) / 255.0f, 1.0f); - } - if (hex.size() == 8) { - int r = ParseHexDigit(hex[0]) * 16 + ParseHexDigit(hex[1]); - int g = ParseHexDigit(hex[2]) * 16 + ParseHexDigit(hex[3]); - int b = ParseHexDigit(hex[4]) * 16 + ParseHexDigit(hex[5]); - int a = ParseHexDigit(hex[6]) * 16 + ParseHexDigit(hex[7]); - return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, - static_cast(b) / 255.0f, static_cast(a) / 255.0f); - } - } - if (str.substr(0, 4) == "rgb(" || str.substr(0, 5) == "rgba(") { - auto start = str.find('('); - auto end = str.find(')'); - if (start != std::string::npos && end != std::string::npos) { - auto values = str.substr(start + 1, end - start - 1); - std::istringstream iss(values); - std::string token = {}; - std::vector components = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - components.push_back(std::stof(trimmed)); - } - if (components.size() >= 3) { - float r = components[0] / 255.0f; - float g = components[1] / 255.0f; - float b = components[2] / 255.0f; - float a = components.size() >= 4 ? components[3] : 1.0f; - return Color::FromRGBA(r, g, b, a); - } - } - } - // CSS Color Level 4: color(colorspace r g b) or color(colorspace r g b / a) - // Supported colorspaces: display-p3, a98-rgb, rec2020, srgb - if (str.substr(0, 6) == "color(") { - auto start = str.find('('); - auto end = str.find(')'); - if (start != std::string::npos && end != std::string::npos) { - auto inner = str.substr(start + 1, end - start - 1); - // Trim whitespace - inner.erase(0, inner.find_first_not_of(" \t")); - inner.erase(inner.find_last_not_of(" \t") + 1); - - // Parse colorspace and values (space-separated) - std::istringstream iss(inner); - std::string colorspace = {}; - iss >> colorspace; - - std::vector components = {}; - std::string token = {}; - float alpha = 1.0f; - bool foundSlash = false; - - while (iss >> token) { - if (token == "/") { - foundSlash = true; - continue; - } - float value = std::stof(token); - if (foundSlash) { - alpha = value; - } else { - components.push_back(value); - } - } - - if (components.size() >= 3) { - float r = components[0]; - float g = components[1]; - float b = components[2]; - - // Convert from wide gamut colorspace to sRGB (approximate clipping). - // For display-p3, a98-rgb, rec2020: values are in 0-1 range. - // We do a simplified conversion by clamping to sRGB gamut. - // A proper implementation would use ICC profiles or matrix transforms. - if (colorspace == "display-p3") { - // Display P3 to sRGB approximate conversion matrix. - float sR = 1.2249f * r - 0.2247f * g - 0.0002f * b; - float sG = -0.0420f * r + 1.0419f * g + 0.0001f * b; - float sB = -0.0197f * r - 0.0786f * g + 1.0983f * b; - r = std::max(0.0f, std::min(1.0f, sR)); - g = std::max(0.0f, std::min(1.0f, sG)); - b = std::max(0.0f, std::min(1.0f, sB)); - } else if (colorspace == "a98-rgb") { - // Adobe RGB to sRGB approximate conversion. - float sR = 1.3982f * r - 0.3982f * g + 0.0f * b; - float sG = 0.0f * r + 1.0f * g + 0.0f * b; - float sB = 0.0f * r - 0.0429f * g + 1.0429f * b; - r = std::max(0.0f, std::min(1.0f, sR)); - g = std::max(0.0f, std::min(1.0f, sG)); - b = std::max(0.0f, std::min(1.0f, sB)); - } else if (colorspace == "rec2020") { - // Rec.2020 to sRGB approximate conversion. - float sR = 1.6605f * r - 0.5877f * g - 0.0728f * b; - float sG = -0.1246f * r + 1.1330f * g - 0.0084f * b; - float sB = -0.0182f * r - 0.1006f * g + 1.1188f * b; - r = std::max(0.0f, std::min(1.0f, sR)); - g = std::max(0.0f, std::min(1.0f, sG)); - b = std::max(0.0f, std::min(1.0f, sB)); - } - // For "srgb" or unknown colorspaces, use values directly. - - return Color::FromRGBA(r, g, b, alpha); - } - } - } - return {}; -} - -std::string Color::toHexString(bool includeAlpha) const { - auto toHex = [](float v) { - int i = static_cast(std::round(v * 255.0f)); - i = std::max(0, std::min(255, i)); - char buf[3] = {}; - snprintf(buf, sizeof(buf), "%02X", i); - return std::string(buf); - }; - std::string result = "#" + toHex(red) + toHex(green) + toHex(blue); - if (includeAlpha && alpha < 1.0f) { - result += toHex(alpha); - } - return result; -} - -//============================================================================== -// Matrix implementation -//============================================================================== - -Matrix Matrix::Translate(float x, float y) { - Matrix m = {}; - m.tx = x; - m.ty = y; - return m; -} - -Matrix Matrix::Scale(float sx, float sy) { - Matrix m = {}; - m.a = sx; - m.d = sy; - return m; -} - -Matrix Matrix::Rotate(float degrees) { - Matrix m = {}; - float radians = degrees * 3.14159265358979323846f / 180.0f; - float cosVal = std::cos(radians); - float sinVal = std::sin(radians); - m.a = cosVal; - m.b = sinVal; - m.c = -sinVal; - m.d = cosVal; - return m; -} - -Matrix Matrix::Parse(const std::string& str) { - Matrix m = {}; - std::istringstream iss(str); - std::string token = {}; - std::vector values = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - values.push_back(std::stof(trimmed)); - } - } - if (values.size() >= 6) { - m.a = values[0]; - m.b = values[1]; - m.c = values[2]; - m.d = values[3]; - m.tx = values[4]; - m.ty = values[5]; - } - return m; -} - -std::string Matrix::toString() const { - std::ostringstream oss = {}; - oss << a << "," << b << "," << c << "," << d << "," << tx << "," << ty; - return oss.str(); -} - -Matrix Matrix::operator*(const Matrix& other) const { - Matrix result = {}; - result.a = a * other.a + c * other.b; - result.b = b * other.a + d * other.b; - result.c = a * other.c + c * other.d; - result.d = b * other.c + d * other.d; - result.tx = a * other.tx + c * other.ty + tx; - result.ty = b * other.tx + d * other.ty + ty; - return result; -} - -Point Matrix::mapPoint(const Point& point) const { - return {a * point.x + c * point.y + tx, b * point.x + d * point.y + ty}; -} - //============================================================================== // Enum string conversions //============================================================================== -#define DEFINE_ENUM_CONVERSION(EnumType, ...) \ - static const std::unordered_map EnumType##ToStringMap = {__VA_ARGS__}; \ - static const std::unordered_map StringTo##EnumType##Map = [] { \ - std::unordered_map map = {}; \ - for (const auto& pair : EnumType##ToStringMap) { \ - map[pair.second] = pair.first; \ - } \ - return map; \ - }(); \ - std::string EnumType##ToString(EnumType value) { \ - auto it = EnumType##ToStringMap.find(value); \ - return it != EnumType##ToStringMap.end() ? it->second : ""; \ - } \ - EnumType EnumType##FromString(const std::string& str) { \ - auto it = StringTo##EnumType##Map.find(str); \ - return it != StringTo##EnumType##Map.end() ? it->second : EnumType##ToStringMap.begin()->first; \ +#define DEFINE_ENUM_CONVERSION(EnumType, ...) \ + static const std::unordered_map EnumType##ToStringMap = {__VA_ARGS__}; \ + static const std::unordered_map StringTo##EnumType##Map = [] { \ + std::unordered_map map = {}; \ + for (const auto& pair : EnumType##ToStringMap) { \ + map[pair.second] = pair.first; \ + } \ + return map; \ + }(); \ + std::string EnumType##ToString(EnumType value) { \ + auto it = EnumType##ToStringMap.find(value); \ + return it != EnumType##ToStringMap.end() ? it->second : ""; \ + } \ + EnumType EnumType##FromString(const std::string& str) { \ + auto it = StringTo##EnumType##Map.find(str); \ + return it != StringTo##EnumType##Map.end() ? it->second \ + : EnumType##ToStringMap.begin()->first; \ } DEFINE_ENUM_CONVERSION(BlendMode, @@ -343,9 +97,9 @@ DEFINE_ENUM_CONVERSION(StrokeAlign, {StrokeAlign::Inside, "inside"}, {StrokeAlign::Outside, "outside"}) -DEFINE_ENUM_CONVERSION(Placement, - {Placement::Background, "background"}, - {Placement::Foreground, "foreground"}) +DEFINE_ENUM_CONVERSION(LayerPlacement, + {LayerPlacement::Background, "background"}, + {LayerPlacement::Foreground, "foreground"}) DEFINE_ENUM_CONVERSION(TileMode, {TileMode::Clamp, "clamp"}, @@ -394,10 +148,6 @@ DEFINE_ENUM_CONVERSION(Overflow, {Overflow::Visible, "visible"}, {Overflow::Ellipsis, "ellipsis"}) -DEFINE_ENUM_CONVERSION(FontStyle, - {FontStyle::Normal, "normal"}, - {FontStyle::Italic, "italic"}) - DEFINE_ENUM_CONVERSION(TextPathAlign, {TextPathAlign::Start, "start"}, {TextPathAlign::Center, "center"}, diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index a4779c0d5e..c7ef1cb53c 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -293,9 +293,16 @@ void PAGXXMLParser::parseDocument(const XMLNode* root, Document* doc) { void PAGXXMLParser::parseResources(const XMLNode* node, Document* doc) { for (const auto& child : node->children) { + // Try to parse as a resource first auto resource = parseResource(child.get()); if (resource) { doc->resources.push_back(std::move(resource)); + continue; + } + // Try to parse as a color source + auto colorSource = parseColorSource(child.get()); + if (colorSource) { + doc->colorSources.push_back(std::move(colorSource)); } } } @@ -307,24 +314,6 @@ std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { if (node->tag == "PathData") { return parsePathData(node); } - if (node->tag == "SolidColor") { - return parseSolidColor(node); - } - if (node->tag == "LinearGradient") { - return parseLinearGradient(node); - } - if (node->tag == "RadialGradient") { - return parseRadialGradient(node); - } - if (node->tag == "ConicGradient") { - return parseConicGradient(node); - } - if (node->tag == "DiamondGradient") { - return parseDiamondGradient(node); - } - if (node->tag == "ImagePattern") { - return parseImagePattern(node); - } if (node->tag == "Composition") { return parseComposition(node); } @@ -575,7 +564,7 @@ std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) { textSpan->font = getAttribute(node, "font"); textSpan->fontSize = getFloatAttribute(node, "fontSize", 12); textSpan->fontWeight = getIntAttribute(node, "fontWeight", 400); - textSpan->fontStyle = FontStyleFromString(getAttribute(node, "fontStyle", "normal")); + textSpan->fontStyle = getAttribute(node, "fontStyle", "normal"); textSpan->tracking = getFloatAttribute(node, "tracking", 0); textSpan->baselineShift = getFloatAttribute(node, "baselineShift", 0); textSpan->text = node->text; @@ -592,7 +581,7 @@ std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { fill->alpha = getFloatAttribute(node, "alpha", 1); fill->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); fill->fillRule = FillRuleFromString(getAttribute(node, "fillRule", "winding")); - fill->placement = PlacementFromString(getAttribute(node, "placement", "background")); + fill->placement = LayerPlacementFromString(getAttribute(node, "placement", "background")); for (const auto& child : node->children) { auto colorSource = parseColorSource(child.get()); @@ -620,7 +609,7 @@ std::unique_ptr PAGXXMLParser::parseStroke(const XMLNode* node) { } stroke->dashOffset = getFloatAttribute(node, "dashOffset", 0); stroke->align = StrokeAlignFromString(getAttribute(node, "align", "center")); - stroke->placement = PlacementFromString(getAttribute(node, "placement", "background")); + stroke->placement = LayerPlacementFromString(getAttribute(node, "placement", "background")); for (const auto& child : node->children) { auto colorSource = parseColorSource(child.get()); @@ -676,7 +665,7 @@ std::unique_ptr PAGXXMLParser::parseTextModifier(const XMLNode* no for (const auto& child : node->children) { if (child->tag == "RangeSelector") { - modifier->rangeSelectors.push_back(parseRangeSelector(child.get())); + modifier->selectors.push_back(std::make_unique(parseRangeSelector(child.get()))); } } diff --git a/pagx/src/PAGXXMLParser.h b/pagx/src/PAGXXMLParser.h index 47fefc83a3..9ddc4733ab 100644 --- a/pagx/src/PAGXXMLParser.h +++ b/pagx/src/PAGXXMLParser.h @@ -22,7 +22,40 @@ #include #include #include +#include "pagx/model/BackgroundBlurStyle.h" +#include "pagx/model/BlendFilter.h" +#include "pagx/model/BlurFilter.h" +#include "pagx/model/ColorMatrixFilter.h" +#include "pagx/model/Composition.h" +#include "pagx/model/ConicGradient.h" +#include "pagx/model/DiamondGradient.h" #include "pagx/model/Document.h" +#include "pagx/model/DropShadowFilter.h" +#include "pagx/model/DropShadowStyle.h" +#include "pagx/model/Ellipse.h" +#include "pagx/model/Fill.h" +#include "pagx/model/Group.h" +#include "pagx/model/Image.h" +#include "pagx/model/ImagePattern.h" +#include "pagx/model/InnerShadowFilter.h" +#include "pagx/model/InnerShadowStyle.h" +#include "pagx/model/LinearGradient.h" +#include "pagx/model/MergePath.h" +#include "pagx/model/Path.h" +#include "pagx/model/PathDataResource.h" +#include "pagx/model/Polystar.h" +#include "pagx/model/RadialGradient.h" +#include "pagx/model/RangeSelector.h" +#include "pagx/model/Rectangle.h" +#include "pagx/model/Repeater.h" +#include "pagx/model/RoundCorner.h" +#include "pagx/model/SolidColor.h" +#include "pagx/model/Stroke.h" +#include "pagx/model/TextLayout.h" +#include "pagx/model/TextModifier.h" +#include "pagx/model/TextPath.h" +#include "pagx/model/TextSpan.h" +#include "pagx/model/TrimPath.h" namespace pagx { diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index c7fabe1d28..c0d059ff83 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -20,7 +20,40 @@ #include #include #include -#include "pagx/model/Model.h" +#include "pagx/model/BackgroundBlurStyle.h" +#include "pagx/model/BlendFilter.h" +#include "pagx/model/BlurFilter.h" +#include "pagx/model/ColorMatrixFilter.h" +#include "pagx/model/Composition.h" +#include "pagx/model/ConicGradient.h" +#include "pagx/model/DiamondGradient.h" +#include "pagx/model/Document.h" +#include "pagx/model/DropShadowFilter.h" +#include "pagx/model/DropShadowStyle.h" +#include "pagx/model/Ellipse.h" +#include "pagx/model/Fill.h" +#include "pagx/model/Group.h" +#include "pagx/model/Image.h" +#include "pagx/model/ImagePattern.h" +#include "pagx/model/InnerShadowFilter.h" +#include "pagx/model/InnerShadowStyle.h" +#include "pagx/model/LinearGradient.h" +#include "pagx/model/MergePath.h" +#include "pagx/model/Path.h" +#include "pagx/model/PathDataResource.h" +#include "pagx/model/Polystar.h" +#include "pagx/model/RadialGradient.h" +#include "pagx/model/RangeSelector.h" +#include "pagx/model/Rectangle.h" +#include "pagx/model/Repeater.h" +#include "pagx/model/RoundCorner.h" +#include "pagx/model/SolidColor.h" +#include "pagx/model/Stroke.h" +#include "pagx/model/TextLayout.h" +#include "pagx/model/TextModifier.h" +#include "pagx/model/TextPath.h" +#include "pagx/model/TextSpan.h" +#include "pagx/model/TrimPath.h" namespace pagx { @@ -193,7 +226,7 @@ static std::string colorSourceToKey(const ColorSource* node) { return ""; } std::ostringstream oss = {}; - switch (node->colorSourceType()) { + switch (node->type()) { case ColorSourceType::SolidColor: { auto solid = static_cast(node); oss << "SolidColor:" << solid->color.toHexString(true); @@ -415,7 +448,7 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& stops } static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId) { - switch (node->colorSourceType()) { + switch (node->type()) { case ColorSourceType::SolidColor: { auto solid = static_cast(node); xml.openElement("SolidColor"); @@ -543,7 +576,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ // Write ColorSource with assigned id (for Resources section) static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, const std::string& id) { - switch (node->colorSourceType()) { + switch (node->type()) { case ColorSourceType::SolidColor: { auto solid = static_cast(node); xml.openElement("SolidColor"); @@ -733,8 +766,8 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.addAttribute("font", text->font); xml.addAttribute("fontSize", text->fontSize, 12.0f); xml.addAttribute("fontWeight", text->fontWeight, 400); - if (text->fontStyle != FontStyle::Normal) { - xml.addAttribute("fontStyle", FontStyleToString(text->fontStyle)); + if (text->fontStyle != "normal" && !text->fontStyle.empty()) { + xml.addAttribute("fontStyle", text->fontStyle); } xml.addAttribute("tracking", text->tracking); xml.addAttribute("baselineShift", text->baselineShift); @@ -764,8 +797,8 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, if (fill->fillRule != FillRule::Winding) { xml.addAttribute("fillRule", FillRuleToString(fill->fillRule)); } - if (fill->placement != Placement::Background) { - xml.addAttribute("placement", PlacementToString(fill->placement)); + if (fill->placement != LayerPlacement::Background) { + xml.addAttribute("placement", LayerPlacementToString(fill->placement)); } // Inline ColorSource only if not extracted to Resources if (fill->colorSource && ctx.getColorSourceId(fill->colorSource.get()).empty()) { @@ -810,8 +843,8 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, if (stroke->align != StrokeAlign::Center) { xml.addAttribute("align", StrokeAlignToString(stroke->align)); } - if (stroke->placement != Placement::Background) { - xml.addAttribute("placement", PlacementToString(stroke->placement)); + if (stroke->placement != LayerPlacement::Background) { + xml.addAttribute("placement", LayerPlacementToString(stroke->placement)); } // Inline ColorSource only if not extracted to Resources if (stroke->colorSource && ctx.getColorSourceId(stroke->colorSource.get()).empty()) { @@ -872,29 +905,33 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, if (modifier->strokeWidth >= 0) { xml.addAttribute("strokeWidth", modifier->strokeWidth); } - if (modifier->rangeSelectors.empty()) { + if (modifier->selectors.empty()) { xml.closeElementSelfClosing(); } else { xml.closeElementStart(); - for (const auto& selector : modifier->rangeSelectors) { + for (const auto& selector : modifier->selectors) { + if (selector->type() != TextSelectorType::RangeSelector) { + continue; + } + auto rangeSelector = static_cast(selector.get()); xml.openElement("RangeSelector"); - xml.addAttribute("start", selector.start); - xml.addAttribute("end", selector.end, 1.0f); - xml.addAttribute("offset", selector.offset); - if (selector.unit != SelectorUnit::Percentage) { - xml.addAttribute("unit", SelectorUnitToString(selector.unit)); + xml.addAttribute("start", rangeSelector->start); + xml.addAttribute("end", rangeSelector->end, 1.0f); + xml.addAttribute("offset", rangeSelector->offset); + if (rangeSelector->unit != SelectorUnit::Percentage) { + xml.addAttribute("unit", SelectorUnitToString(rangeSelector->unit)); } - if (selector.shape != SelectorShape::Square) { - xml.addAttribute("shape", SelectorShapeToString(selector.shape)); + if (rangeSelector->shape != SelectorShape::Square) { + xml.addAttribute("shape", SelectorShapeToString(rangeSelector->shape)); } - xml.addAttribute("easeIn", selector.easeIn); - xml.addAttribute("easeOut", selector.easeOut); - if (selector.mode != SelectorMode::Add) { - xml.addAttribute("mode", SelectorModeToString(selector.mode)); + xml.addAttribute("easeIn", rangeSelector->easeIn); + xml.addAttribute("easeOut", rangeSelector->easeOut); + if (rangeSelector->mode != SelectorMode::Add) { + xml.addAttribute("mode", SelectorModeToString(rangeSelector->mode)); } - xml.addAttribute("weight", selector.weight, 1.0f); - xml.addAttribute("randomizeOrder", selector.randomizeOrder); - xml.addAttribute("randomSeed", selector.randomSeed); + xml.addAttribute("weight", rangeSelector->weight, 1.0f); + xml.addAttribute("randomizeOrder", rangeSelector->randomizeOrder); + xml.addAttribute("randomSeed", rangeSelector->randomSeed); xml.closeElementSelfClosing(); } xml.closeElement(); @@ -1147,14 +1184,6 @@ static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceC } break; } - case ResourceType::SolidColor: - case ResourceType::LinearGradient: - case ResourceType::RadialGradient: - case ResourceType::ConicGradient: - case ResourceType::DiamondGradient: - case ResourceType::ImagePattern: - writeColorSource(xml, static_cast(node), true); - break; default: break; } diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp index 0376fed69d..b9c44baa06 100644 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ b/pagx/src/svg/PAGXSVGParser.cpp @@ -21,7 +21,7 @@ #include #include #include -#include "pagx/PAGXModel.h" +#include "pagx/model/Document.h" #include "SVGParserInternal.h" #include "xml/XMLDOM.h" diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 4b05065ba6..4f0fdf0a5d 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -23,7 +23,20 @@ #include #include #include +#include "pagx/model/BlurFilter.h" #include "pagx/model/Document.h" +#include "pagx/model/Ellipse.h" +#include "pagx/model/Fill.h" +#include "pagx/model/Group.h" +#include "pagx/model/Image.h" +#include "pagx/model/ImagePattern.h" +#include "pagx/model/LinearGradient.h" +#include "pagx/model/Path.h" +#include "pagx/model/PathData.h" +#include "pagx/model/RadialGradient.h" +#include "pagx/model/Rectangle.h" +#include "pagx/model/Stroke.h" +#include "pagx/model/TextSpan.h" #include "pagx/PAGXSVGParser.h" #include "xml/XMLDOM.h" diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 817f1345ed..feca7a8463 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -20,7 +20,32 @@ #include #include #include -#include "pagx/model/Model.h" +#include "pagx/model/BlurFilter.h" +#include "pagx/model/Composition.h" +#include "pagx/model/ConicGradient.h" +#include "pagx/model/DiamondGradient.h" +#include "pagx/model/DropShadowFilter.h" +#include "pagx/model/DropShadowStyle.h" +#include "pagx/model/Ellipse.h" +#include "pagx/model/Fill.h" +#include "pagx/model/Group.h" +#include "pagx/model/Image.h" +#include "pagx/model/ImagePattern.h" +#include "pagx/model/InnerShadowFilter.h" +#include "pagx/model/InnerShadowStyle.h" +#include "pagx/model/Layer.h" +#include "pagx/model/LinearGradient.h" +#include "pagx/model/MergePath.h" +#include "pagx/model/Path.h" +#include "pagx/model/Polystar.h" +#include "pagx/model/RadialGradient.h" +#include "pagx/model/Rectangle.h" +#include "pagx/model/Repeater.h" +#include "pagx/model/RoundCorner.h" +#include "pagx/model/SolidColor.h" +#include "pagx/model/Stroke.h" +#include "pagx/model/TextSpan.h" +#include "pagx/model/TrimPath.h" #include "pagx/PAGXSVGParser.h" #include "tgfx/core/Data.h" #include "tgfx/core/Font.h" @@ -476,7 +501,7 @@ class LayerBuilderImpl { return nullptr; } - switch (node->colorSourceType()) { + switch (node->type()) { case ColorSourceType::SolidColor: { auto solid = static_cast(node); return tgfx::SolidColor::Make(ToTGFX(solid->color)); @@ -705,7 +730,7 @@ class LayerBuilderImpl { switch (node->type()) { case LayerFilterType::BlurFilter: { - auto filter = static_cast(node); + auto filter = static_cast(node); return tgfx::BlurFilter::Make(filter->blurrinessX, filter->blurrinessY); } case LayerFilterType::DropShadowFilter: { diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 1616d79e58..5929e3379b 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -19,8 +19,17 @@ #include #include "pagx/LayerBuilder.h" #include "pagx/model/Document.h" +#include "pagx/model/DropShadowStyle.h" +#include "pagx/model/BlurFilter.h" +#include "pagx/model/Ellipse.h" +#include "pagx/model/Fill.h" +#include "pagx/model/Group.h" +#include "pagx/model/LinearGradient.h" +#include "pagx/model/Path.h" +#include "pagx/model/RadialGradient.h" +#include "pagx/model/Rectangle.h" +#include "pagx/model/SolidColor.h" #include "pagx/PAGXSVGParser.h" -#include "pagx/PAGXModel.h" #include "pagx/model/PathData.h" #include "tgfx/core/Data.h" #include "tgfx/core/Stream.h" @@ -364,7 +373,7 @@ PAG_TEST(PAGXTest, ColorSources) { // Test SolidColor auto solid = std::make_unique(); solid->color = pagx::Color::FromRGBA(1.0f, 0.0f, 0.0f, 1.0f); - EXPECT_EQ(solid->colorSourceType(), pagx::ColorSourceType::SolidColor); + EXPECT_EQ(solid->type(), pagx::ColorSourceType::SolidColor); EXPECT_FLOAT_EQ(solid->color.red, 1.0f); // Test LinearGradient @@ -385,7 +394,7 @@ PAG_TEST(PAGXTest, ColorSources) { linear->colorStops.push_back(stop1); linear->colorStops.push_back(stop2); - EXPECT_EQ(linear->colorSourceType(), pagx::ColorSourceType::LinearGradient); + EXPECT_EQ(linear->type(), pagx::ColorSourceType::LinearGradient); EXPECT_EQ(linear->colorStops.size(), 2u); // Test RadialGradient @@ -395,7 +404,7 @@ PAG_TEST(PAGXTest, ColorSources) { radial->radius = 50; radial->colorStops = linear->colorStops; - EXPECT_EQ(radial->colorSourceType(), pagx::ColorSourceType::RadialGradient); + EXPECT_EQ(radial->type(), pagx::ColorSourceType::RadialGradient); } /** From 85eb6d0eec963cd04d8da4e1274d0f88b4f992aa Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 18:02:08 +0800 Subject: [PATCH 095/678] Remove redundant type header files that were consolidated into model headers. --- pagx/include/pagx/model/types/FillRule.h | 36 ----------------- pagx/include/pagx/model/types/FontStyle.h | 36 ----------------- pagx/include/pagx/model/types/LineCap.h | 37 ----------------- pagx/include/pagx/model/types/LineJoin.h | 37 ----------------- pagx/include/pagx/model/types/MaskType.h | 37 ----------------- pagx/include/pagx/model/types/MergePathMode.h | 39 ------------------ pagx/include/pagx/model/types/Overflow.h | 37 ----------------- pagx/include/pagx/model/types/PolystarType.h | 36 ----------------- pagx/include/pagx/model/types/RepeaterOrder.h | 36 ----------------- pagx/include/pagx/model/types/SamplingMode.h | 37 ----------------- pagx/include/pagx/model/types/SelectorMode.h | 40 ------------------- pagx/include/pagx/model/types/SelectorShape.h | 40 ------------------- pagx/include/pagx/model/types/SelectorUnit.h | 36 ----------------- pagx/include/pagx/model/types/StrokeAlign.h | 37 ----------------- pagx/include/pagx/model/types/TextAlign.h | 38 ------------------ pagx/include/pagx/model/types/TextPathAlign.h | 37 ----------------- pagx/include/pagx/model/types/TrimType.h | 36 ----------------- pagx/include/pagx/model/types/VerticalAlign.h | 37 ----------------- 18 files changed, 669 deletions(-) delete mode 100644 pagx/include/pagx/model/types/FillRule.h delete mode 100644 pagx/include/pagx/model/types/FontStyle.h delete mode 100644 pagx/include/pagx/model/types/LineCap.h delete mode 100644 pagx/include/pagx/model/types/LineJoin.h delete mode 100644 pagx/include/pagx/model/types/MaskType.h delete mode 100644 pagx/include/pagx/model/types/MergePathMode.h delete mode 100644 pagx/include/pagx/model/types/Overflow.h delete mode 100644 pagx/include/pagx/model/types/PolystarType.h delete mode 100644 pagx/include/pagx/model/types/RepeaterOrder.h delete mode 100644 pagx/include/pagx/model/types/SamplingMode.h delete mode 100644 pagx/include/pagx/model/types/SelectorMode.h delete mode 100644 pagx/include/pagx/model/types/SelectorShape.h delete mode 100644 pagx/include/pagx/model/types/SelectorUnit.h delete mode 100644 pagx/include/pagx/model/types/StrokeAlign.h delete mode 100644 pagx/include/pagx/model/types/TextAlign.h delete mode 100644 pagx/include/pagx/model/types/TextPathAlign.h delete mode 100644 pagx/include/pagx/model/types/TrimType.h delete mode 100644 pagx/include/pagx/model/types/VerticalAlign.h diff --git a/pagx/include/pagx/model/types/FillRule.h b/pagx/include/pagx/model/types/FillRule.h deleted file mode 100644 index 5ae1093101..0000000000 --- a/pagx/include/pagx/model/types/FillRule.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Fill rules for paths. - */ -enum class FillRule { - Winding, - EvenOdd -}; - -std::string FillRuleToString(FillRule rule); -FillRule FillRuleFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/FontStyle.h b/pagx/include/pagx/model/types/FontStyle.h deleted file mode 100644 index 59568b80ff..0000000000 --- a/pagx/include/pagx/model/types/FontStyle.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Font style. - */ -enum class FontStyle { - Normal, - Italic -}; - -std::string FontStyleToString(FontStyle style); -FontStyle FontStyleFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/LineCap.h b/pagx/include/pagx/model/types/LineCap.h deleted file mode 100644 index dfb779acca..0000000000 --- a/pagx/include/pagx/model/types/LineCap.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Line cap styles for strokes. - */ -enum class LineCap { - Butt, - Round, - Square -}; - -std::string LineCapToString(LineCap cap); -LineCap LineCapFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/LineJoin.h b/pagx/include/pagx/model/types/LineJoin.h deleted file mode 100644 index e6b12c6f57..0000000000 --- a/pagx/include/pagx/model/types/LineJoin.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Line join styles for strokes. - */ -enum class LineJoin { - Miter, - Round, - Bevel -}; - -std::string LineJoinToString(LineJoin join); -LineJoin LineJoinFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/MaskType.h b/pagx/include/pagx/model/types/MaskType.h deleted file mode 100644 index f274bd9a7c..0000000000 --- a/pagx/include/pagx/model/types/MaskType.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Mask types for layer masking. - */ -enum class MaskType { - Alpha, - Luminance, - Contour -}; - -std::string MaskTypeToString(MaskType type); -MaskType MaskTypeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/MergePathMode.h b/pagx/include/pagx/model/types/MergePathMode.h deleted file mode 100644 index 94a17879ec..0000000000 --- a/pagx/include/pagx/model/types/MergePathMode.h +++ /dev/null @@ -1,39 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Path merge modes (boolean operations). - */ -enum class MergePathMode { - Append, - Union, - Intersect, - Xor, - Difference -}; - -std::string MergePathModeToString(MergePathMode mode); -MergePathMode MergePathModeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/Overflow.h b/pagx/include/pagx/model/types/Overflow.h deleted file mode 100644 index 4a6868d063..0000000000 --- a/pagx/include/pagx/model/types/Overflow.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Text overflow handling. - */ -enum class Overflow { - Clip, - Visible, - Ellipsis -}; - -std::string OverflowToString(Overflow overflow); -Overflow OverflowFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/PolystarType.h b/pagx/include/pagx/model/types/PolystarType.h deleted file mode 100644 index 5834dc05c2..0000000000 --- a/pagx/include/pagx/model/types/PolystarType.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Polystar types. - */ -enum class PolystarType { - Polygon, - Star -}; - -std::string PolystarTypeToString(PolystarType type); -PolystarType PolystarTypeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/RepeaterOrder.h b/pagx/include/pagx/model/types/RepeaterOrder.h deleted file mode 100644 index e98b5a3439..0000000000 --- a/pagx/include/pagx/model/types/RepeaterOrder.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Repeater stacking order. - */ -enum class RepeaterOrder { - BelowOriginal, - AboveOriginal -}; - -std::string RepeaterOrderToString(RepeaterOrder order); -RepeaterOrder RepeaterOrderFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/SamplingMode.h b/pagx/include/pagx/model/types/SamplingMode.h deleted file mode 100644 index 270a5160e5..0000000000 --- a/pagx/include/pagx/model/types/SamplingMode.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Sampling modes for images. - */ -enum class SamplingMode { - Nearest, - Linear, - Mipmap -}; - -std::string SamplingModeToString(SamplingMode mode); -SamplingMode SamplingModeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/SelectorMode.h b/pagx/include/pagx/model/types/SelectorMode.h deleted file mode 100644 index c6794115ee..0000000000 --- a/pagx/include/pagx/model/types/SelectorMode.h +++ /dev/null @@ -1,40 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Range selector combination mode. - */ -enum class SelectorMode { - Add, - Subtract, - Intersect, - Min, - Max, - Difference -}; - -std::string SelectorModeToString(SelectorMode mode); -SelectorMode SelectorModeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/SelectorShape.h b/pagx/include/pagx/model/types/SelectorShape.h deleted file mode 100644 index e61239f4e8..0000000000 --- a/pagx/include/pagx/model/types/SelectorShape.h +++ /dev/null @@ -1,40 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Range selector shape. - */ -enum class SelectorShape { - Square, - RampUp, - RampDown, - Triangle, - Round, - Smooth -}; - -std::string SelectorShapeToString(SelectorShape shape); -SelectorShape SelectorShapeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/SelectorUnit.h b/pagx/include/pagx/model/types/SelectorUnit.h deleted file mode 100644 index 759da0682d..0000000000 --- a/pagx/include/pagx/model/types/SelectorUnit.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Range selector unit. - */ -enum class SelectorUnit { - Index, - Percentage -}; - -std::string SelectorUnitToString(SelectorUnit unit); -SelectorUnit SelectorUnitFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/StrokeAlign.h b/pagx/include/pagx/model/types/StrokeAlign.h deleted file mode 100644 index adccb2ed9f..0000000000 --- a/pagx/include/pagx/model/types/StrokeAlign.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Stroke alignment relative to path. - */ -enum class StrokeAlign { - Center, - Inside, - Outside -}; - -std::string StrokeAlignToString(StrokeAlign align); -StrokeAlign StrokeAlignFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/TextAlign.h b/pagx/include/pagx/model/types/TextAlign.h deleted file mode 100644 index 88ba8c64b2..0000000000 --- a/pagx/include/pagx/model/types/TextAlign.h +++ /dev/null @@ -1,38 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Text horizontal alignment. - */ -enum class TextAlign { - Left, - Center, - Right, - Justify -}; - -std::string TextAlignToString(TextAlign align); -TextAlign TextAlignFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/TextPathAlign.h b/pagx/include/pagx/model/types/TextPathAlign.h deleted file mode 100644 index 7957c8679b..0000000000 --- a/pagx/include/pagx/model/types/TextPathAlign.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Text path alignment. - */ -enum class TextPathAlign { - Start, - Center, - End -}; - -std::string TextPathAlignToString(TextPathAlign align); -TextPathAlign TextPathAlignFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/TrimType.h b/pagx/include/pagx/model/types/TrimType.h deleted file mode 100644 index 31b3aab19f..0000000000 --- a/pagx/include/pagx/model/types/TrimType.h +++ /dev/null @@ -1,36 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Trim path types. - */ -enum class TrimType { - Separate, - Continuous -}; - -std::string TrimTypeToString(TrimType type); -TrimType TrimTypeFromString(const std::string& str); - -} // namespace pagx diff --git a/pagx/include/pagx/model/types/VerticalAlign.h b/pagx/include/pagx/model/types/VerticalAlign.h deleted file mode 100644 index e1db9e2487..0000000000 --- a/pagx/include/pagx/model/types/VerticalAlign.h +++ /dev/null @@ -1,37 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * Text vertical alignment. - */ -enum class VerticalAlign { - Top, - Center, - Bottom -}; - -std::string VerticalAlignToString(VerticalAlign align); -VerticalAlign VerticalAlignFromString(const std::string& str); - -} // namespace pagx From b756a676fca4ae42fef75b91021c093ee8422e76 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 18:16:31 +0800 Subject: [PATCH 096/678] Fix Polystar attribute name: polystarType -> type --- pagx/docs/pagx_spec.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 5152462fac..d647ee42c2 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -927,13 +927,13 @@ boundingRect.bottom = center.y + size.height / 2 支持正多边形和星形两种模式。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `center` | point | 0,0 | 中心点 | -| `polystarType` | PolystarType | star | 类型(见下方) | +| `type` | PolystarType | star | 类型(见下方) | | `pointCount` | float | 5 | 顶点数(支持小数) | | `outerRadius` | float | 100 | 外半径 | | `innerRadius` | float | 50 | 内半径(仅星形) | @@ -949,11 +949,11 @@ boundingRect.bottom = center.y + size.height / 2 | `polygon` | 正多边形:只使用外半径 | | `star` | 星形:使用外半径和内半径交替 | -**多边形模式** (`polystarType="polygon"`): +**多边形模式** (`type="polygon"`): - 只使用 `outerRadius` 和 `outerRoundness` - `innerRadius` 和 `innerRoundness` 被忽略 -**星形模式** (`polystarType="star"`): +**星形模式** (`type="star"`): - 外顶点位于 `outerRadius` 处 - 内顶点位于 `innerRadius` 处 - 顶点交替连接形成星形 @@ -2182,7 +2182,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| | `center` | point | 0,0 | -| `polystarType` | PolystarType | star | +| `type` | PolystarType | star | | `pointCount` | float | 5 | | `outerRadius` | float | 100 | | `innerRadius` | float | 50 | From 9d9857aecd02d08b7fae97fd4b1acc76c2a2b031 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 18:24:38 +0800 Subject: [PATCH 097/678] Refactor: Add Node base class and rename SVGImporter - Add Node base class with nodeType and id properties for resources - Remove Resource base class, resources now inherit from Node directly - Rename PAGXSVGParser to SVGImporter for clearer naming - Update Document.resources to use Node type - Ensure all 8/9 PAGX tests pass (1 visual test fails due to missing baselines) --- pagx/include/pagx/SVGImporter.h | 72 + pagx/include/pagx/model/Composition.h | 27 +- pagx/include/pagx/model/Document.h | 12 +- pagx/include/pagx/model/Image.h | 19 +- pagx/include/pagx/model/Node.h | 72 + pagx/include/pagx/model/PathDataResource.h | 21 +- pagx/src/PAGXDocument.cpp | 17 +- pagx/src/PAGXElement.cpp | 10 +- pagx/src/PAGXXMLParser.cpp | 2 +- pagx/src/PAGXXMLParser.h | 2 +- pagx/src/PAGXXMLWriter.cpp | 16 +- pagx/src/svg/SVGImporter.cpp | 1718 ++++++++++++++++++++ pagx/src/svg/SVGParserInternal.h | 6 +- pagx/src/tgfx/LayerBuilder.cpp | 9 +- test/src/PAGXTest.cpp | 6 +- 15 files changed, 1941 insertions(+), 68 deletions(-) create mode 100644 pagx/include/pagx/SVGImporter.h create mode 100644 pagx/include/pagx/model/Node.h create mode 100644 pagx/src/svg/SVGImporter.cpp diff --git a/pagx/include/pagx/SVGImporter.h b/pagx/include/pagx/SVGImporter.h new file mode 100644 index 0000000000..111d97ae66 --- /dev/null +++ b/pagx/include/pagx/SVGImporter.h @@ -0,0 +1,72 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "pagx/model/Document.h" + +namespace pagx { + +/** + * SVGImporter converts SVG documents to PAGX Document. + * This importer is independent of tgfx and preserves complete SVG information. + */ +class SVGImporter { + public: + struct Options { + /** + * If true, unsupported SVG elements are preserved as Unknown nodes. + */ + bool preserveUnknownElements; + + /** + * If true, references are expanded to actual content. + */ + bool expandUseReferences; + + /** + * If true, nested transforms are flattened into single matrices. + */ + bool flattenTransforms; + + Options() : preserveUnknownElements(false), expandUseReferences(true), flattenTransforms(false) { + } + }; + + /** + * Parses an SVG file and creates a PAGX Document. + */ + static std::shared_ptr Parse(const std::string& filePath, + const Options& options = Options()); + + /** + * Parses SVG data and creates a PAGX Document. + */ + static std::shared_ptr Parse(const uint8_t* data, size_t length, + const Options& options = Options()); + + /** + * Parses an SVG string and creates a PAGX Document. + */ + static std::shared_ptr ParseString(const std::string& svgContent, + const Options& options = Options()); +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Composition.h b/pagx/include/pagx/model/Composition.h index 98d4038345..24a54f2840 100644 --- a/pagx/include/pagx/model/Composition.h +++ b/pagx/include/pagx/model/Composition.h @@ -21,28 +21,35 @@ #include #include #include -#include "pagx/model/Resource.h" +#include "pagx/model/Node.h" namespace pagx { class Layer; /** - * Composition resource. + * Composition represents a reusable composition resource that contains a set of layers. It can be + * referenced by a Layer's composition property to create instances. */ -class Composition : public Resource { +class Composition : public Node { public: - std::string id = {}; + /** + * The width of the composition in pixels. + */ float width = 0; + + /** + * The height of the composition in pixels. + */ float height = 0; - std::vector> layers = {}; - ResourceType type() const override { - return ResourceType::Composition; - } + /** + * The layers contained in this composition. + */ + std::vector> layers = {}; - const std::string& resourceId() const override { - return id; + NodeType nodeType() const override { + return NodeType::Composition; } }; diff --git a/pagx/include/pagx/model/Document.h b/pagx/include/pagx/model/Document.h index 47a7221ec0..b2aecf12a4 100644 --- a/pagx/include/pagx/model/Document.h +++ b/pagx/include/pagx/model/Document.h @@ -24,7 +24,7 @@ #include #include "pagx/model/ColorSource.h" #include "pagx/model/Layer.h" -#include "pagx/model/Resource.h" +#include "pagx/model/Node.h" namespace pagx { @@ -53,10 +53,10 @@ class Document { float height = 0; /** - * Resources (images, paths, compositions). + * Resources (images, compositions, etc.). * These can be referenced by "#id" in the document. */ - std::vector> resources = {}; + std::vector> resources = {}; /** * Color sources (gradients, solid colors, patterns). @@ -106,7 +106,7 @@ class Document { * Finds a resource by ID. * Returns nullptr if not found. */ - Resource* findResource(const std::string& id) const; + Node* findResource(const std::string& id) const; /** * Finds a color source by ID. @@ -124,13 +124,13 @@ class Document { friend class PAGXXMLParser; Document() = default; - mutable std::unordered_map resourceMap = {}; + mutable std::unordered_map resourceMap = {}; mutable std::unordered_map colorSourceMap = {}; mutable bool resourceMapDirty = true; void rebuildResourceMap() const; static Layer* findLayerRecursive(const std::vector>& layers, - const std::string& id); + const std::string& id); }; } // namespace pagx diff --git a/pagx/include/pagx/model/Image.h b/pagx/include/pagx/model/Image.h index 2212d6f284..e19c192509 100644 --- a/pagx/include/pagx/model/Image.h +++ b/pagx/include/pagx/model/Image.h @@ -19,24 +19,23 @@ #pragma once #include -#include "pagx/model/Resource.h" +#include "pagx/model/Node.h" namespace pagx { /** - * Image resource. + * Image represents an image resource that can be referenced by other nodes. The image source can + * be a file path, a URL, or a base64-encoded data URI. */ -class Image : public Resource { +class Image : public Node { public: - std::string id = {}; + /** + * The source of the image. Can be a file path, URL, or base64-encoded data URI. + */ std::string source = {}; - ResourceType type() const override { - return ResourceType::Image; - } - - const std::string& resourceId() const override { - return id; + NodeType nodeType() const override { + return NodeType::Image; } }; diff --git a/pagx/include/pagx/model/Node.h b/pagx/include/pagx/model/Node.h new file mode 100644 index 0000000000..5ed1fd8b32 --- /dev/null +++ b/pagx/include/pagx/model/Node.h @@ -0,0 +1,72 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +/** + * NodeType enumerates all types of nodes that can be stored in a PAGX document. + * This includes resources (Image, Composition) and other shared definitions. + */ +enum class NodeType { + /** + * An image resource. + */ + Image, + /** + * A reusable path data resource. + */ + PathData, + /** + * A composition resource containing layers. + */ + Composition +}; + +/** + * Returns the string name of a node type. + */ +const char* NodeTypeName(NodeType type); + +/** + * Node is the base class for all shareable nodes in a PAGX document. Nodes can be stored in the + * document's resources list and referenced by ID (e.g., "#imageId"). Each node has a unique + * identifier and a type. + */ +class Node { + public: + /** + * The unique identifier of this node. Used for referencing the node by ID (e.g., "#id"). + */ + std::string id = {}; + + virtual ~Node() = default; + + /** + * Returns the node type of this node. + */ + virtual NodeType nodeType() const = 0; + + protected: + Node() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/PathDataResource.h b/pagx/include/pagx/model/PathDataResource.h index a53cc2ac21..97c08e8471 100644 --- a/pagx/include/pagx/model/PathDataResource.h +++ b/pagx/include/pagx/model/PathDataResource.h @@ -19,24 +19,23 @@ #pragma once #include -#include "pagx/model/Resource.h" +#include "pagx/model/Node.h" namespace pagx { /** - * PathData resource - stores reusable path data. + * PathDataResource is a reusable path data resource that can be referenced by Path elements. It + * stores path data as an SVG path string for efficient serialization. */ -class PathDataResource : public Resource { +class PathDataResource : public Node { public: - std::string id = {}; - std::string data = {}; // SVG path data string + /** + * The SVG path data string (d attribute format). + */ + std::string data = {}; - ResourceType type() const override { - return ResourceType::PathData; - } - - const std::string& resourceId() const override { - return id; + NodeType nodeType() const override { + return NodeType::PathData; } }; diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp index cf2c484f8d..ea683b1f15 100644 --- a/pagx/src/PAGXDocument.cpp +++ b/pagx/src/PAGXDocument.cpp @@ -19,6 +19,7 @@ #include "pagx/model/Document.h" #include #include +#include "pagx/model/Composition.h" #include "PAGXXMLParser.h" #include "PAGXXMLWriter.h" @@ -60,7 +61,7 @@ std::string Document::toXML() const { return PAGXXMLWriter::Write(*this); } -Resource* Document::findResource(const std::string& id) const { +Node* Document::findResource(const std::string& id) const { if (resourceMapDirty) { rebuildResourceMap(); } @@ -68,6 +69,11 @@ Resource* Document::findResource(const std::string& id) const { return it != resourceMap.end() ? it->second : nullptr; } +ColorSource* Document::findColorSource(const std::string& id) const { + auto it = colorSourceMap.find(id); + return it != colorSourceMap.end() ? it->second : nullptr; +} + Layer* Document::findLayer(const std::string& id) const { // First search in top-level layers auto found = findLayerRecursive(layers, id); @@ -76,7 +82,7 @@ Layer* Document::findLayer(const std::string& id) const { } // Then search in Composition resources for (const auto& resource : resources) { - if (resource->type() == ResourceType::Composition) { + if (resource->nodeType() == NodeType::Composition) { auto comp = static_cast(resource.get()); found = findLayerRecursive(comp->layers, id); if (found) { @@ -90,16 +96,15 @@ Layer* Document::findLayer(const std::string& id) const { void Document::rebuildResourceMap() const { resourceMap.clear(); for (const auto& resource : resources) { - auto& id = resource->resourceId(); - if (!id.empty()) { - resourceMap[id] = resource.get(); + if (!resource->id.empty()) { + resourceMap[resource->id] = resource.get(); } } resourceMapDirty = false; } Layer* Document::findLayerRecursive(const std::vector>& layers, - const std::string& id) { + const std::string& id) { for (const auto& layer : layers) { if (layer->id == id) { return layer.get(); diff --git a/pagx/src/PAGXElement.cpp b/pagx/src/PAGXElement.cpp index 05ef236eb1..39cfbebd73 100644 --- a/pagx/src/PAGXElement.cpp +++ b/pagx/src/PAGXElement.cpp @@ -20,17 +20,17 @@ #include "pagx/model/Element.h" #include "pagx/model/LayerFilter.h" #include "pagx/model/LayerStyle.h" -#include "pagx/model/Resource.h" +#include "pagx/model/Node.h" namespace pagx { -const char* ResourceTypeName(ResourceType type) { +const char* NodeTypeName(NodeType type) { switch (type) { - case ResourceType::Image: + case NodeType::Image: return "Image"; - case ResourceType::PathData: + case NodeType::PathData: return "PathData"; - case ResourceType::Composition: + case NodeType::Composition: return "Composition"; default: return "Unknown"; diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index c7ef1cb53c..77bc7bce8c 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -307,7 +307,7 @@ void PAGXXMLParser::parseResources(const XMLNode* node, Document* doc) { } } -std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { +std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { if (node->tag == "Image") { return parseImage(node); } diff --git a/pagx/src/PAGXXMLParser.h b/pagx/src/PAGXXMLParser.h index 9ddc4733ab..8d101ea744 100644 --- a/pagx/src/PAGXXMLParser.h +++ b/pagx/src/PAGXXMLParser.h @@ -84,7 +84,7 @@ class PAGXXMLParser { static void parseDocument(const XMLNode* root, Document* doc); static void parseResources(const XMLNode* node, Document* doc); - static std::unique_ptr parseResource(const XMLNode* node); + static std::unique_ptr parseResource(const XMLNode* node); static std::unique_ptr parseLayer(const XMLNode* node); static void parseContents(const XMLNode* node, Layer* layer); static void parseStyles(const XMLNode* node, Layer* layer); diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index c0d059ff83..4f28483637 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -306,7 +306,7 @@ class ResourceContext { collectFromLayer(layer.get()); } for (const auto& resource : doc.resources) { - if (resource->type() == ResourceType::Composition) { + if (resource->nodeType() == NodeType::Composition) { auto comp = static_cast(resource.get()); for (const auto& layer : comp->layers) { collectFromLayer(layer.get()); @@ -431,7 +431,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, const ResourceContext& ctx); static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node); static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node); -static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceContext& ctx); +static void writeResource(XMLBuilder& xml, const Node* node, const ResourceContext& ctx); static void writeLayer(XMLBuilder& xml, const Layer* node, const ResourceContext& ctx); //============================================================================== @@ -1149,9 +1149,9 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { // Resource writing //============================================================================== -static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceContext& ctx) { - switch (node->type()) { - case ResourceType::Image: { +static void writeResource(XMLBuilder& xml, const Node* node, const ResourceContext& ctx) { + switch (node->nodeType()) { + case NodeType::Image: { auto image = static_cast(node); xml.openElement("Image"); xml.addAttribute("id", image->id); @@ -1159,7 +1159,7 @@ static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceC xml.closeElementSelfClosing(); break; } - case ResourceType::PathData: { + case NodeType::PathData: { auto pathData = static_cast(node); xml.openElement("PathData"); xml.addAttribute("id", pathData->id); @@ -1167,7 +1167,7 @@ static void writeResource(XMLBuilder& xml, const Resource* node, const ResourceC xml.closeElementSelfClosing(); break; } - case ResourceType::Composition: { + case NodeType::Composition: { auto comp = static_cast(node); xml.openElement("Composition"); xml.addAttribute("id", comp->id); @@ -1343,7 +1343,7 @@ std::string PAGXXMLWriter::Write(const Document& doc) { // Also collect from Compositions for (const auto& resource : doc.resources) { - if (resource->type() == ResourceType::Composition) { + if (resource->nodeType() == NodeType::Composition) { auto comp = static_cast(resource.get()); std::function collectColorSources = [&](const Layer* layer) { for (const auto& element : layer->contents) { diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp new file mode 100644 index 0000000000..d5b86d5c5c --- /dev/null +++ b/pagx/src/svg/SVGImporter.cpp @@ -0,0 +1,1718 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/SVGImporter.h" +#include +#include +#include +#include +#include "pagx/model/Document.h" +#include "SVGParserInternal.h" +#include "xml/XMLDOM.h" + +namespace pagx { + +std::shared_ptr SVGImporter::Parse(const std::string& filePath, + const Options& options) { + SVGParserImpl parser(options); + auto doc = parser.parseFile(filePath); + if (doc) { + auto lastSlash = filePath.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + doc->basePath = filePath.substr(0, lastSlash + 1); + } + } + return doc; +} + +std::shared_ptr SVGImporter::Parse(const uint8_t* data, size_t length, + const Options& options) { + SVGParserImpl parser(options); + return parser.parse(data, length); +} + +std::shared_ptr SVGImporter::ParseString(const std::string& svgContent, + const Options& options) { + return Parse(reinterpret_cast(svgContent.data()), svgContent.size(), options); +} + +// ============== SVGParserImpl ============== + +SVGParserImpl::SVGParserImpl(const SVGImporter::Options& options) : _options(options) { +} + +std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { + if (!data || length == 0) { + return nullptr; + } + + auto dom = DOM::Make(data, length); + if (!dom) { + return nullptr; + } + + return parseDOM(dom); +} + +std::shared_ptr SVGParserImpl::parseFile(const std::string& filePath) { + auto dom = DOM::MakeFromFile(filePath); + if (!dom) { + return nullptr; + } + + return parseDOM(dom); +} + +std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, + const std::string& name, + const std::string& defaultValue) const { + auto [found, value] = node->findAttribute(name); + return found ? value : defaultValue; +} + +std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& dom) { + auto root = dom->getRootNode(); + if (!root || root->name != "svg") { + return nullptr; + } + + // Parse viewBox and dimensions. + auto viewBox = parseViewBox(getAttribute(root, "viewBox")); + float width = parseLength(getAttribute(root, "width"), 0); + float height = parseLength(getAttribute(root, "height"), 0); + + if (viewBox.size() >= 4) { + _viewBoxWidth = viewBox[2]; + _viewBoxHeight = viewBox[3]; + if (width == 0) { + width = _viewBoxWidth; + } + if (height == 0) { + height = _viewBoxHeight; + } + } else { + _viewBoxWidth = width; + _viewBoxHeight = height; + } + + if (width <= 0 || height <= 0) { + return nullptr; + } + + _document = Document::Make(width, height); + + // Collect all IDs from the SVG to avoid conflicts when generating new IDs. + collectAllIds(root); + + // First pass: collect defs. + auto child = root->getFirstChild(); + while (child) { + if (child->name == "defs") { + parseDefs(child); + } + child = child->getNextSibling(); + } + + // Check if we need a viewBox transform. + bool needsViewBoxTransform = false; + Matrix viewBoxMatrix = Matrix::Identity(); + if (viewBox.size() >= 4) { + float viewBoxX = viewBox[0]; + float viewBoxY = viewBox[1]; + float viewBoxW = viewBox[2]; + float viewBoxH = viewBox[3]; + + if (viewBoxW > 0 && viewBoxH > 0 && + (viewBoxX != 0 || viewBoxY != 0 || viewBoxW != width || viewBoxH != height)) { + // Calculate uniform scale (meet behavior: fit inside viewport). + float scaleX = width / viewBoxW; + float scaleY = height / viewBoxH; + float scale = std::min(scaleX, scaleY); + + // Calculate translation to center content (xMidYMid). + float translateX = (width - viewBoxW * scale) / 2.0f - viewBoxX * scale; + float translateY = (height - viewBoxH * scale) / 2.0f - viewBoxY * scale; + + // Build the transform matrix: scale then translate. + viewBoxMatrix = Matrix::Translate(translateX, translateY) * Matrix::Scale(scale, scale); + needsViewBoxTransform = true; + } + } + + // Compute initial inherited style from the root element. + InheritedStyle rootStyle = {}; + rootStyle = computeInheritedStyle(root, rootStyle); + + // Collect converted layers. + std::vector> convertedLayers; + child = root->getFirstChild(); + while (child) { + if (child->name != "defs") { + auto layer = convertToLayer(child, rootStyle); + if (layer) { + convertedLayers.push_back(std::move(layer)); + } + } + child = child->getNextSibling(); + } + + // Add collected mask layers (invisible, used as mask references). + for (auto& maskLayer : _maskLayers) { + convertedLayers.insert(convertedLayers.begin(), std::move(maskLayer)); + } + _maskLayers.clear(); + + // Merge adjacent layers with the same geometry (optimize Fill + Stroke into one Layer). + mergeAdjacentLayers(convertedLayers); + + // If viewBox transform is needed, wrap in a root layer with the transform. + // Otherwise, add layers directly to document (no root wrapper). + if (needsViewBoxTransform) { + auto rootLayer = std::make_unique(); + rootLayer->matrix = viewBoxMatrix; + for (auto& layer : convertedLayers) { + rootLayer->children.push_back(std::move(layer)); + } + _document->layers.push_back(std::move(rootLayer)); + } else { + for (auto& layer : convertedLayers) { + _document->layers.push_back(std::move(layer)); + } + } + + return _document; +} + +InheritedStyle SVGParserImpl::computeInheritedStyle(const std::shared_ptr& element, + const InheritedStyle& parentStyle) { + InheritedStyle style = parentStyle; + + std::string fill = getAttribute(element, "fill"); + if (!fill.empty()) { + style.fill = fill; + } + + std::string stroke = getAttribute(element, "stroke"); + if (!stroke.empty()) { + style.stroke = stroke; + } + + std::string fillOpacity = getAttribute(element, "fill-opacity"); + if (!fillOpacity.empty()) { + style.fillOpacity = fillOpacity; + } + + std::string strokeOpacity = getAttribute(element, "stroke-opacity"); + if (!strokeOpacity.empty()) { + style.strokeOpacity = strokeOpacity; + } + + std::string fillRule = getAttribute(element, "fill-rule"); + if (!fillRule.empty()) { + style.fillRule = fillRule; + } + + return style; +} + +void SVGParserImpl::parseDefs(const std::shared_ptr& defsNode) { + auto child = defsNode->getFirstChild(); + while (child) { + std::string id = getAttribute(child, "id"); + if (!id.empty()) { + _defs[id] = child; + } + // Also handle nested defs. + if (child->name == "defs") { + parseDefs(child); + } + child = child->getNextSibling(); + } +} + +std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, + const InheritedStyle& parentStyle) { + const auto& tag = element->name; + + if (tag == "defs" || tag == "linearGradient" || tag == "radialGradient" || tag == "pattern" || + tag == "mask" || tag == "clipPath" || tag == "filter") { + return nullptr; + } + + // Compute inherited style for this element. + InheritedStyle inheritedStyle = computeInheritedStyle(element, parentStyle); + + auto layer = std::make_unique(); + + // Parse common layer attributes. + layer->id = getAttribute(element, "id"); + + // Parse data-* custom attributes. + parseCustomData(element, layer.get()); + + std::string transform = getAttribute(element, "transform"); + if (!transform.empty()) { + layer->matrix = parseTransform(transform); + } + + std::string opacity = getAttribute(element, "opacity"); + if (!opacity.empty()) { + layer->alpha = std::stof(opacity); + } + + std::string display = getAttribute(element, "display"); + if (display == "none") { + layer->visible = false; + } + + std::string visibility = getAttribute(element, "visibility"); + if (visibility == "hidden") { + layer->visible = false; + } + + // Handle mask attribute. + std::string maskAttr = getAttribute(element, "mask"); + if (!maskAttr.empty() && maskAttr != "none") { + std::string maskId = resolveUrl(maskAttr); + auto maskIt = _defs.find(maskId); + if (maskIt != _defs.end()) { + // Convert mask element to a mask layer. + auto maskLayer = convertMaskElement(maskIt->second, inheritedStyle); + if (maskLayer) { + layer->mask = "#" + maskLayer->id; + // SVG masks use luminance by default. + layer->maskType = MaskType::Luminance; + // Add mask layer as invisible layer to the document. + _maskLayers.push_back(std::move(maskLayer)); + } + } + } + + // Handle filter attribute. + std::string filterAttr = getAttribute(element, "filter"); + if (!filterAttr.empty() && filterAttr != "none") { + std::string filterId = resolveUrl(filterAttr); + auto filterIt = _defs.find(filterId); + if (filterIt != _defs.end()) { + convertFilterElement(filterIt->second, layer->filters); + } + } + + // Convert contents. + if (tag == "g" || tag == "svg") { + // Group: convert children as child layers. + auto child = element->getFirstChild(); + while (child) { + auto childLayer = convertToLayer(child, inheritedStyle); + if (childLayer) { + layer->children.push_back(std::move(childLayer)); + } + child = child->getNextSibling(); + } + } else { + // Shape element: convert to vector contents. + convertChildren(element, layer->contents, inheritedStyle); + } + + return layer; +} + +void SVGParserImpl::convertChildren(const std::shared_ptr& element, + std::vector>& contents, + const InheritedStyle& inheritedStyle) { + const auto& tag = element->name; + + // Handle text element specially - it returns a Group with TextSpan. + if (tag == "text") { + auto textGroup = convertText(element, inheritedStyle); + if (textGroup) { + contents.push_back(std::move(textGroup)); + } + return; + } + + auto shapeElement = convertElement(element); + if (shapeElement) { + contents.push_back(std::move(shapeElement)); + } + + addFillStroke(element, contents, inheritedStyle); +} + +std::unique_ptr SVGParserImpl::convertElement( + const std::shared_ptr& element) { + const auto& tag = element->name; + + if (tag == "rect") { + return convertRect(element); + } else if (tag == "circle") { + return convertCircle(element); + } else if (tag == "ellipse") { + return convertEllipse(element); + } else if (tag == "line") { + return convertLine(element); + } else if (tag == "polyline") { + return convertPolyline(element); + } else if (tag == "polygon") { + return convertPolygon(element); + } else if (tag == "path") { + return convertPath(element); + } else if (tag == "use") { + return convertUse(element); + } + + return nullptr; +} + +std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& element, + const InheritedStyle& parentStyle) { + // Compute inherited style for this group element. + InheritedStyle inheritedStyle = computeInheritedStyle(element, parentStyle); + + auto group = std::make_unique(); + + // group->name (removed) = getAttribute(element, "id"); + + std::string transform = getAttribute(element, "transform"); + if (!transform.empty()) { + // For Group, we need to decompose the matrix into position/rotation/scale. + // For simplicity, just store as position offset for translation. + Matrix m = parseTransform(transform); + group->position = {m.tx, m.ty}; + // Note: Full matrix decomposition would be more complex. + } + + std::string opacity = getAttribute(element, "opacity"); + if (!opacity.empty()) { + group->alpha = std::stof(opacity); + } + + auto child = element->getFirstChild(); + while (child) { + auto childElement = convertElement(child); + if (childElement) { + group->elements.push_back(std::move(childElement)); + } + addFillStroke(child, group->elements, inheritedStyle); + child = child->getNextSibling(); + } + + return group; +} + +std::unique_ptr SVGParserImpl::convertRect( + const std::shared_ptr& element) { + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); + float width = parseLength(getAttribute(element, "width"), _viewBoxWidth); + float height = parseLength(getAttribute(element, "height"), _viewBoxHeight); + float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); + float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); + + if (ry == 0) { + ry = rx; + } + + auto rect = std::make_unique(); + rect->center.x = x + width / 2; + rect->center.y = y + height / 2; + rect->size.width = width; + rect->size.height = height; + rect->roundness = std::max(rx, ry); + + return rect; +} + +std::unique_ptr SVGParserImpl::convertCircle( + const std::shared_ptr& element) { + float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); + float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); + float r = parseLength(getAttribute(element, "r"), _viewBoxWidth); + + auto ellipse = std::make_unique(); + ellipse->center.x = cx; + ellipse->center.y = cy; + ellipse->size.width = r * 2; + ellipse->size.height = r * 2; + + return ellipse; +} + +std::unique_ptr SVGParserImpl::convertEllipse( + const std::shared_ptr& element) { + float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); + float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); + float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); + float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); + + auto ellipse = std::make_unique(); + ellipse->center.x = cx; + ellipse->center.y = cy; + ellipse->size.width = rx * 2; + ellipse->size.height = ry * 2; + + return ellipse; +} + +std::unique_ptr SVGParserImpl::convertLine( + const std::shared_ptr& element) { + float x1 = parseLength(getAttribute(element, "x1"), _viewBoxWidth); + float y1 = parseLength(getAttribute(element, "y1"), _viewBoxHeight); + float x2 = parseLength(getAttribute(element, "x2"), _viewBoxWidth); + float y2 = parseLength(getAttribute(element, "y2"), _viewBoxHeight); + + auto path = std::make_unique(); + path->data.moveTo(x1, y1); + path->data.lineTo(x2, y2); + + return path; +} + +std::unique_ptr SVGParserImpl::convertPolyline( + const std::shared_ptr& element) { + auto path = std::make_unique(); + path->data = parsePoints(getAttribute(element, "points"), false); + return path; +} + +std::unique_ptr SVGParserImpl::convertPolygon( + const std::shared_ptr& element) { + auto path = std::make_unique(); + path->data = parsePoints(getAttribute(element, "points"), true); + return path; +} + +std::unique_ptr SVGParserImpl::convertPath( + const std::shared_ptr& element) { + auto path = std::make_unique(); + std::string d = getAttribute(element, "d"); + if (!d.empty()) { + path->data = PathData::FromSVGString(d); + } + return path; +} + +std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr& element, + const InheritedStyle& inheritedStyle) { + auto group = std::make_unique(); + + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); + + // Parse text-anchor attribute for x position adjustment. + // SVG text-anchor affects horizontal alignment: start (default), middle, end. + // Since PAGX TextSpan doesn't have text-anchor, we'll note this for future + // position adjustment after text shaping (requires knowing text width). + std::string anchor = getAttribute(element, "text-anchor"); + // Note: text-anchor adjustment would require knowing the text width after shaping. + // For now, we store the x position as-is. A full implementation would need to + // adjust x based on anchor after calculating the text bounds. + + // Get text content from child text nodes. + std::string textContent; + auto child = element->getFirstChild(); + while (child) { + if (child->type == DOMNodeType::Text) { + textContent += child->name; + } + child = child->getNextSibling(); + } + + if (!textContent.empty()) { + auto textSpan = std::make_unique(); + textSpan->x = x; + textSpan->y = y; + textSpan->text = textContent; + + std::string fontFamily = getAttribute(element, "font-family"); + if (!fontFamily.empty()) { + textSpan->font = fontFamily; + } + + std::string fontSize = getAttribute(element, "font-size"); + if (!fontSize.empty()) { + textSpan->fontSize = parseLength(fontSize, _viewBoxHeight); + } + + group->elements.push_back(std::move(textSpan)); + } + + addFillStroke(element, group->elements, inheritedStyle); + return group; +} + +std::unique_ptr SVGParserImpl::convertUse( + const std::shared_ptr& element) { + std::string href = getAttribute(element, "xlink:href"); + if (href.empty()) { + href = getAttribute(element, "href"); + } + + std::string refId = resolveUrl(href); + auto it = _defs.find(refId); + if (it == _defs.end()) { + return nullptr; + } + + if (_options.expandUseReferences) { + auto node = convertElement(it->second); + if (node) { + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); + if (x != 0 || y != 0) { + // Wrap in a group with translation. + auto group = std::make_unique(); + group->position = {x, y}; + group->elements.push_back(std::move(node)); + return group; + } + } + return node; + } + + // For non-expanded use references, just create an empty group for now. + auto group = std::make_unique(); + // group->name (removed) = "_useRef:" + refId; + return group; +} + +std::unique_ptr SVGParserImpl::convertLinearGradient( + const std::shared_ptr& element, const Rect& shapeBounds) { + auto gradient = std::make_unique(); + + gradient->id = getAttribute(element, "id"); + + // Check gradientUnits - determines how gradient coordinates are interpreted. + // Default is objectBoundingBox, meaning values are 0-1 ratios of the shape bounds. + std::string gradientUnits = getAttribute(element, "gradientUnits", "objectBoundingBox"); + bool useOBB = (gradientUnits == "objectBoundingBox"); + + // Parse gradient coordinates. + float x1 = parseLength(getAttribute(element, "x1", "0%"), 1.0f); + float y1 = parseLength(getAttribute(element, "y1", "0%"), 1.0f); + float x2 = parseLength(getAttribute(element, "x2", "100%"), 1.0f); + float y2 = parseLength(getAttribute(element, "y2", "0%"), 1.0f); + + // Parse gradientTransform. + std::string gradientTransform = getAttribute(element, "gradientTransform"); + Matrix transformMatrix = gradientTransform.empty() ? Matrix::Identity() + : parseTransform(gradientTransform); + + if (useOBB) { + // For objectBoundingBox, coordinates are normalized 0-1. + // Convert to actual coordinates based on shape bounds. + Point start = {shapeBounds.x + x1 * shapeBounds.width, shapeBounds.y + y1 * shapeBounds.height}; + Point end = {shapeBounds.x + x2 * shapeBounds.width, shapeBounds.y + y2 * shapeBounds.height}; + // Apply gradient transform after converting to actual coordinates. + start = transformMatrix.mapPoint(start); + end = transformMatrix.mapPoint(end); + gradient->startPoint = start; + gradient->endPoint = end; + } else { + // For userSpaceOnUse, coordinates are in user space. + // Apply gradient transform to user space points. + Point start = {x1, y1}; + Point end = {x2, y2}; + start = transformMatrix.mapPoint(start); + end = transformMatrix.mapPoint(end); + gradient->startPoint = start; + gradient->endPoint = end; + } + + // Parse stops. + auto child = element->getFirstChild(); + while (child) { + if (child->name == "stop") { + ColorStop stop; + stop.offset = parseLength(getAttribute(child, "offset", "0"), 1.0f); + stop.color = parseColor(getAttribute(child, "stop-color", "#000000")); + float opacity = parseLength(getAttribute(child, "stop-opacity", "1"), 1.0f); + stop.color.alpha = opacity; + gradient->colorStops.push_back(stop); + } + child = child->getNextSibling(); + } + + return gradient; +} + +std::unique_ptr SVGParserImpl::convertRadialGradient( + const std::shared_ptr& element, const Rect& shapeBounds) { + auto gradient = std::make_unique(); + + gradient->id = getAttribute(element, "id"); + + // Check gradientUnits - determines how gradient coordinates are interpreted. + std::string gradientUnits = getAttribute(element, "gradientUnits", "objectBoundingBox"); + bool useOBB = (gradientUnits == "objectBoundingBox"); + + // Parse gradient coordinates. + float cx = parseLength(getAttribute(element, "cx", "50%"), 1.0f); + float cy = parseLength(getAttribute(element, "cy", "50%"), 1.0f); + float r = parseLength(getAttribute(element, "r", "50%"), 1.0f); + + // Parse gradientTransform. + std::string gradientTransform = getAttribute(element, "gradientTransform"); + Matrix transformMatrix = gradientTransform.empty() ? Matrix::Identity() + : parseTransform(gradientTransform); + + if (useOBB) { + // For objectBoundingBox, convert normalized coordinates to actual coordinates. + Point center = {shapeBounds.x + cx * shapeBounds.width, + shapeBounds.y + cy * shapeBounds.height}; + // Radius is scaled by the average of width and height. + float actualRadius = r * (shapeBounds.width + shapeBounds.height) / 2.0f; + + // Apply gradientTransform after converting to actual coordinates. + center = transformMatrix.mapPoint(center); + // For radius, account for scaling in the transform. + float scaleX = std::sqrt(transformMatrix.a * transformMatrix.a + + transformMatrix.b * transformMatrix.b); + float scaleY = std::sqrt(transformMatrix.c * transformMatrix.c + + transformMatrix.d * transformMatrix.d); + actualRadius *= (scaleX + scaleY) / 2.0f; + + gradient->center = center; + gradient->radius = actualRadius; + + // Store the matrix for non-uniform scaling (rotation, skew, etc.). + if (!transformMatrix.isIdentity()) { + gradient->matrix = transformMatrix; + } + } else { + // For userSpaceOnUse, coordinates are in user space. + if (!gradientTransform.empty()) { + Point center = {cx, cy}; + center = transformMatrix.mapPoint(center); + gradient->center = center; + + float scaleX = std::sqrt(transformMatrix.a * transformMatrix.a + + transformMatrix.b * transformMatrix.b); + float scaleY = std::sqrt(transformMatrix.c * transformMatrix.c + + transformMatrix.d * transformMatrix.d); + gradient->radius = r * (scaleX + scaleY) / 2.0f; + + if (!transformMatrix.isIdentity()) { + gradient->matrix = transformMatrix; + } + } else { + gradient->center.x = cx; + gradient->center.y = cy; + gradient->radius = r; + } + } + + // Parse stops. + auto child = element->getFirstChild(); + while (child) { + if (child->name == "stop") { + ColorStop stop; + stop.offset = parseLength(getAttribute(child, "offset", "0"), 1.0f); + stop.color = parseColor(getAttribute(child, "stop-color", "#000000")); + float opacity = parseLength(getAttribute(child, "stop-opacity", "1"), 1.0f); + stop.color.alpha = opacity; + gradient->colorStops.push_back(stop); + } + child = child->getNextSibling(); + } + + return gradient; +} + +std::unique_ptr SVGParserImpl::convertPattern( + const std::shared_ptr& element, const Rect& shapeBounds) { + auto pattern = std::make_unique(); + + pattern->id = getAttribute(element, "id"); + + // SVG patterns use repeat by default. + pattern->tileModeX = TileMode::Repeat; + pattern->tileModeY = TileMode::Repeat; + + // Parse pattern dimensions from SVG attributes. + float patternWidth = parseLength(getAttribute(element, "width"), 1.0f); + float patternHeight = parseLength(getAttribute(element, "height"), 1.0f); + + // Check patternUnits - determines how pattern x/y/width/height are interpreted. + // Default is objectBoundingBox, meaning values are relative to the shape bounds. + std::string patternUnitsStr = getAttribute(element, "patternUnits", "objectBoundingBox"); + bool patternUnitsOBB = (patternUnitsStr == "objectBoundingBox"); + + // Check patternContentUnits - determines how pattern content coordinates are interpreted. + // Default is userSpaceOnUse, meaning content uses absolute coordinates. + std::string contentUnitsStr = getAttribute(element, "patternContentUnits", "userSpaceOnUse"); + bool contentUnitsOBB = (contentUnitsStr == "objectBoundingBox"); + + // Calculate the actual tile size in user space. + // When patternUnits is objectBoundingBox, pattern dimensions are 0-1 ratios of shape bounds. + float tileWidth = patternUnitsOBB ? patternWidth * shapeBounds.width : patternWidth; + float tileHeight = patternUnitsOBB ? patternHeight * shapeBounds.height : patternHeight; + + // Look for image reference inside the pattern. + auto child = element->getFirstChild(); + while (child) { + if (child->name == "use") { + std::string href = getAttribute(child, "xlink:href"); + if (href.empty()) { + href = getAttribute(child, "href"); + } + std::string imageId = resolveUrl(href); + + // Find the referenced image in defs. + auto imgIt = _defs.find(imageId); + if (imgIt != _defs.end() && imgIt->second->name == "image") { + std::string imageHref = getAttribute(imgIt->second, "xlink:href"); + if (imageHref.empty()) { + imageHref = getAttribute(imgIt->second, "href"); + } + + // Register the image resource and use the reference ID. + std::string resourceId = registerImageResource(imageHref); + pattern->image = "#" + resourceId; + + // Get image display dimensions from SVG (these are the dimensions in pattern content space). + float imageWidth = parseLength(getAttribute(imgIt->second, "width"), 1.0f); + float imageHeight = parseLength(getAttribute(imgIt->second, "height"), 1.0f); + + // Parse transform on the use element. + std::string useTransform = getAttribute(child, "transform"); + Matrix useMatrix = useTransform.empty() ? Matrix::Identity() : parseTransform(useTransform); + + // Calculate the image's actual size in user space (considering content units and transform). + float imageSizeInUserSpaceX = 0; + float imageSizeInUserSpaceY = 0; + if (contentUnitsOBB) { + // When patternContentUnits is objectBoundingBox, image dimensions are 0-1 ratios. + // Apply use transform (e.g., scale(0.005)) to map image to content space, + // then scale by shape bounds to get user space size. + imageSizeInUserSpaceX = imageWidth * useMatrix.a * shapeBounds.width; + imageSizeInUserSpaceY = imageHeight * useMatrix.d * shapeBounds.height; + } else { + // When patternContentUnits is userSpaceOnUse, image dimensions are in user space. + imageSizeInUserSpaceX = imageWidth * useMatrix.a; + imageSizeInUserSpaceY = imageHeight * useMatrix.d; + } + + // The ImagePattern shader tiles the original image pixels. + // We need to scale the image so it renders at the correct size within the tile. + // Since tgfx ImagePattern uses the image's original pixel dimensions as the base, + // the matrix should scale the image to match imageSizeInUserSpace. + // Note: imageWidth here is the SVG display size, which equals original pixel size + // when the image is embedded at 1:1 scale. + float scaleX = imageSizeInUserSpaceX / imageWidth; + float scaleY = imageSizeInUserSpaceY / imageHeight; + + // PAGX ImagePattern coordinates are relative to the geometry's local origin (0,0). + // SVG pattern with objectBoundingBox is relative to the shape's bounding box. + // We need to translate the pattern to align with the shape bounds. + // Matrix multiplication order: translate first, then scale (right to left). + pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y) * + Matrix::Scale(scaleX, scaleY); + } + } else if (child->name == "image") { + // Direct image element inside pattern. + std::string imageHref = getAttribute(child, "xlink:href"); + if (imageHref.empty()) { + imageHref = getAttribute(child, "href"); + } + + // Register the image resource and use the reference ID. + std::string resourceId = registerImageResource(imageHref); + pattern->image = "#" + resourceId; + + float imageWidth = parseLength(getAttribute(child, "width"), 1.0f); + float imageHeight = parseLength(getAttribute(child, "height"), 1.0f); + + if (contentUnitsOBB) { + // Image dimensions are 0-1 ratios, scale by shape bounds. + pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y) * + Matrix::Scale(shapeBounds.width, shapeBounds.height); + } else { + // Image dimensions are absolute, translate to shape bounds origin. + pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y); + } + } + child = child->getNextSibling(); + } + + return pattern; +} + +void SVGParserImpl::addFillStroke(const std::shared_ptr& element, + std::vector>& contents, + const InheritedStyle& inheritedStyle) { + // Get shape bounds for pattern calculations (computed once, used if needed). + Rect shapeBounds = getShapeBounds(element); + + // Determine effective fill value (element attribute overrides inherited). + std::string fill = getAttribute(element, "fill"); + if (fill.empty()) { + fill = inheritedStyle.fill; + } + + // Only add fill if we have an effective fill value that is not "none". + // If fill is empty and no inherited value, SVG default is black fill. + // But if inherited value is "none", we skip fill entirely. + if (fill != "none") { + if (fill.empty()) { + // No fill specified anywhere - use SVG default black. + auto fillNode = std::make_unique(); + fillNode->color = "#000000"; + contents.push_back(std::move(fillNode)); + } else if (fill.find("url(") == 0) { + auto fillNode = std::make_unique(); + std::string refId = resolveUrl(fill); + + // Try to inline the gradient or pattern. + // Don't set fillNode->color when using colorSource. + auto it = _defs.find(refId); + if (it != _defs.end()) { + if (it->second->name == "linearGradient") { + fillNode->colorSource = convertLinearGradient(it->second, shapeBounds); + } else if (it->second->name == "radialGradient") { + fillNode->colorSource = convertRadialGradient(it->second, shapeBounds); + } else if (it->second->name == "pattern") { + fillNode->colorSource = convertPattern(it->second, shapeBounds); + } + } + contents.push_back(std::move(fillNode)); + } else { + auto fillNode = std::make_unique(); + + // Determine effective fill-opacity. + std::string fillOpacity = getAttribute(element, "fill-opacity"); + if (fillOpacity.empty()) { + fillOpacity = inheritedStyle.fillOpacity; + } + if (!fillOpacity.empty()) { + fillNode->alpha = std::stof(fillOpacity); + } + + // Store the original color string for later parsing in LayerBuilder. + fillNode->color = fill; + + // Determine effective fill-rule. + std::string fillRule = getAttribute(element, "fill-rule"); + if (fillRule.empty()) { + fillRule = inheritedStyle.fillRule; + } + if (fillRule == "evenodd") { + fillNode->fillRule = FillRule::EvenOdd; + } + + contents.push_back(std::move(fillNode)); + } + } + + // Determine effective stroke value (element attribute overrides inherited). + std::string stroke = getAttribute(element, "stroke"); + if (stroke.empty()) { + stroke = inheritedStyle.stroke; + } + + if (!stroke.empty() && stroke != "none") { + auto strokeNode = std::make_unique(); + + if (stroke.find("url(") == 0) { + std::string refId = resolveUrl(stroke); + + // Don't set strokeNode->color when using colorSource. + auto it = _defs.find(refId); + if (it != _defs.end()) { + if (it->second->name == "linearGradient") { + strokeNode->colorSource = convertLinearGradient(it->second, shapeBounds); + } else if (it->second->name == "radialGradient") { + strokeNode->colorSource = convertRadialGradient(it->second, shapeBounds); + } else if (it->second->name == "pattern") { + strokeNode->colorSource = convertPattern(it->second, shapeBounds); + } + } + } else { + // Determine effective stroke-opacity. + std::string strokeOpacity = getAttribute(element, "stroke-opacity"); + if (strokeOpacity.empty()) { + strokeOpacity = inheritedStyle.strokeOpacity; + } + if (!strokeOpacity.empty()) { + strokeNode->alpha = std::stof(strokeOpacity); + } + + // Store the original color string for later parsing in LayerBuilder. + strokeNode->color = stroke; + } + + std::string strokeWidth = getAttribute(element, "stroke-width"); + if (!strokeWidth.empty()) { + strokeNode->width = parseLength(strokeWidth, _viewBoxWidth); + } + + std::string strokeLinecap = getAttribute(element, "stroke-linecap"); + if (!strokeLinecap.empty()) { + strokeNode->cap = LineCapFromString(strokeLinecap); + } + + std::string strokeLinejoin = getAttribute(element, "stroke-linejoin"); + if (!strokeLinejoin.empty()) { + strokeNode->join = LineJoinFromString(strokeLinejoin); + } + + std::string strokeMiterlimit = getAttribute(element, "stroke-miterlimit"); + if (!strokeMiterlimit.empty()) { + strokeNode->miterLimit = std::stof(strokeMiterlimit); + } + + std::string dashArray = getAttribute(element, "stroke-dasharray"); + if (!dashArray.empty() && dashArray != "none") { + // Parse dash array values, which may contain units (e.g., "2px,2px" or "2,2"). + // Use parseLength to handle both numeric values and values with units. + std::string token; + for (size_t i = 0; i <= dashArray.size(); i++) { + char c = (i < dashArray.size()) ? dashArray[i] : ','; + if (c == ',' || c == ' ' || c == '\t' || c == '\n' || c == '\r') { + if (!token.empty()) { + strokeNode->dashes.push_back(parseLength(token, _viewBoxWidth)); + token.clear(); + } + } else { + token += c; + } + } + } + + std::string dashOffset = getAttribute(element, "stroke-dashoffset"); + if (!dashOffset.empty()) { + strokeNode->dashOffset = parseLength(dashOffset, _viewBoxWidth); + } + + contents.push_back(std::move(strokeNode)); + } +} + +Rect SVGParserImpl::getShapeBounds(const std::shared_ptr& element) { + const auto& tag = element->name; + + if (tag == "rect") { + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); + float width = parseLength(getAttribute(element, "width"), _viewBoxWidth); + float height = parseLength(getAttribute(element, "height"), _viewBoxHeight); + return Rect::MakeXYWH(x, y, width, height); + } + + if (tag == "circle") { + float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); + float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); + float r = parseLength(getAttribute(element, "r"), _viewBoxWidth); + return Rect::MakeXYWH(cx - r, cy - r, r * 2, r * 2); + } + + if (tag == "ellipse") { + float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); + float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); + float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); + float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); + return Rect::MakeXYWH(cx - rx, cy - ry, rx * 2, ry * 2); + } + + if (tag == "line") { + float x1 = parseLength(getAttribute(element, "x1"), _viewBoxWidth); + float y1 = parseLength(getAttribute(element, "y1"), _viewBoxHeight); + float x2 = parseLength(getAttribute(element, "x2"), _viewBoxWidth); + float y2 = parseLength(getAttribute(element, "y2"), _viewBoxHeight); + float minX = std::min(x1, x2); + float minY = std::min(y1, y2); + float maxX = std::max(x1, x2); + float maxY = std::max(y1, y2); + return Rect::MakeXYWH(minX, minY, maxX - minX, maxY - minY); + } + + if (tag == "path") { + std::string d = getAttribute(element, "d"); + if (!d.empty()) { + auto pathData = PathData::FromSVGString(d); + return pathData.getBounds(); + } + } + + // For polyline and polygon, parse points and compute bounds. + if (tag == "polyline" || tag == "polygon") { + std::string pointsStr = getAttribute(element, "points"); + if (!pointsStr.empty()) { + auto pathData = parsePoints(pointsStr, tag == "polygon"); + return pathData.getBounds(); + } + } + + return Rect::MakeXYWH(0, 0, 0, 0); +} + +Matrix SVGParserImpl::parseTransform(const std::string& value) { + Matrix result = Matrix::Identity(); + if (value.empty()) { + return result; + } + + const char* ptr = value.c_str(); + const char* end = ptr + value.length(); + + auto skipWS = [&]() { + while (ptr < end && (std::isspace(*ptr) || *ptr == ',')) { + ++ptr; + } + }; + + auto readNumber = [&]() -> float { + skipWS(); + const char* start = ptr; + if (*ptr == '-' || *ptr == '+') { + ++ptr; + } + while (ptr < end && (std::isdigit(*ptr) || *ptr == '.')) { + ++ptr; + } + if (ptr < end && (*ptr == 'e' || *ptr == 'E')) { + ++ptr; + if (*ptr == '-' || *ptr == '+') { + ++ptr; + } + while (ptr < end && std::isdigit(*ptr)) { + ++ptr; + } + } + return std::stof(std::string(start, ptr)); + }; + + while (ptr < end) { + skipWS(); + if (ptr >= end) { + break; + } + + std::string func; + while (ptr < end && std::isalpha(*ptr)) { + func += *ptr++; + } + + skipWS(); + if (*ptr != '(') { + break; + } + ++ptr; + + Matrix m = Matrix::Identity(); + + if (func == "translate") { + float tx = readNumber(); + skipWS(); + float ty = 0; + if (ptr < end && *ptr != ')') { + ty = readNumber(); + } + m = Matrix::Translate(tx, ty); + } else if (func == "scale") { + float sx = readNumber(); + skipWS(); + float sy = sx; + if (ptr < end && *ptr != ')') { + sy = readNumber(); + } + m = Matrix::Scale(sx, sy); + } else if (func == "rotate") { + float angle = readNumber(); + skipWS(); + if (ptr < end && *ptr != ')') { + float cx = readNumber(); + float cy = readNumber(); + m = Matrix::Translate(cx, cy) * Matrix::Rotate(angle) * Matrix::Translate(-cx, -cy); + } else { + m = Matrix::Rotate(angle); + } + } else if (func == "skewX") { + float angle = readNumber(); + float radians = angle * 3.14159265358979323846f / 180.0f; + m.c = std::tan(radians); + } else if (func == "skewY") { + float angle = readNumber(); + float radians = angle * 3.14159265358979323846f / 180.0f; + m.b = std::tan(radians); + } else if (func == "matrix") { + m.a = readNumber(); + m.b = readNumber(); + m.c = readNumber(); + m.d = readNumber(); + m.tx = readNumber(); + m.ty = readNumber(); + } + + skipWS(); + if (*ptr == ')') { + ++ptr; + } + + result = result * m; + } + + return result; +} + +Color SVGParserImpl::parseColor(const std::string& value) { + if (value.empty() || value == "none") { + return {0, 0, 0, 0}; + } + + if (value[0] == '#') { + uint32_t hex = 0; + if (value.length() == 4) { + // #RGB -> #RRGGBB + char r = value[1]; + char g = value[2]; + char b = value[3]; + std::string expanded = std::string() + r + r + g + g + b + b; + hex = std::stoul(expanded, nullptr, 16); + return Color::FromHex(hex); + } else if (value.length() == 7) { + hex = std::stoul(value.substr(1), nullptr, 16); + return Color::FromHex(hex); + } else if (value.length() == 9) { + hex = std::stoul(value.substr(1), nullptr, 16); + return Color::FromHex(hex, true); + } + } + + if (value.find("rgb") == 0) { + size_t start = value.find('('); + size_t end = value.find(')'); + if (start != std::string::npos && end != std::string::npos) { + std::string inner = value.substr(start + 1, end - start - 1); + std::istringstream iss(inner); + float r = 0, g = 0, b = 0, a = 1.0f; + char comma = 0; + iss >> r >> comma >> g >> comma >> b; + if (value.find("rgba") == 0) { + iss >> comma >> a; + } + return Color::FromRGBA(r / 255.0f, g / 255.0f, b / 255.0f, a); + } + } + + // SVG/CSS named colors (CSS Color 3 + CSS Color 4 rebeccapurple). + // clang-format off + static const std::unordered_map namedColors = { + {"aliceblue", 0xF0F8FF}, + {"antiquewhite", 0xFAEBD7}, + {"aqua", 0x00FFFF}, + {"aquamarine", 0x7FFFD4}, + {"azure", 0xF0FFFF}, + {"beige", 0xF5F5DC}, + {"bisque", 0xFFE4C4}, + {"black", 0x000000}, + {"blanchedalmond", 0xFFEBCD}, + {"blue", 0x0000FF}, + {"blueviolet", 0x8A2BE2}, + {"brown", 0xA52A2A}, + {"burlywood", 0xDEB887}, + {"cadetblue", 0x5F9EA0}, + {"chartreuse", 0x7FFF00}, + {"chocolate", 0xD2691E}, + {"coral", 0xFF7F50}, + {"cornflowerblue", 0x6495ED}, + {"cornsilk", 0xFFF8DC}, + {"crimson", 0xDC143C}, + {"cyan", 0x00FFFF}, + {"darkblue", 0x00008B}, + {"darkcyan", 0x008B8B}, + {"darkgoldenrod", 0xB8860B}, + {"darkgray", 0xA9A9A9}, + {"darkgreen", 0x006400}, + {"darkgrey", 0xA9A9A9}, + {"darkkhaki", 0xBDB76B}, + {"darkmagenta", 0x8B008B}, + {"darkolivegreen", 0x556B2F}, + {"darkorange", 0xFF8C00}, + {"darkorchid", 0x9932CC}, + {"darkred", 0x8B0000}, + {"darksalmon", 0xE9967A}, + {"darkseagreen", 0x8FBC8F}, + {"darkslateblue", 0x483D8B}, + {"darkslategray", 0x2F4F4F}, + {"darkslategrey", 0x2F4F4F}, + {"darkturquoise", 0x00CED1}, + {"darkviolet", 0x9400D3}, + {"deeppink", 0xFF1493}, + {"deepskyblue", 0x00BFFF}, + {"dimgray", 0x696969}, + {"dimgrey", 0x696969}, + {"dodgerblue", 0x1E90FF}, + {"firebrick", 0xB22222}, + {"floralwhite", 0xFFFAF0}, + {"forestgreen", 0x228B22}, + {"fuchsia", 0xFF00FF}, + {"gainsboro", 0xDCDCDC}, + {"ghostwhite", 0xF8F8FF}, + {"gold", 0xFFD700}, + {"goldenrod", 0xDAA520}, + {"gray", 0x808080}, + {"green", 0x008000}, + {"greenyellow", 0xADFF2F}, + {"grey", 0x808080}, + {"honeydew", 0xF0FFF0}, + {"hotpink", 0xFF69B4}, + {"indianred", 0xCD5C5C}, + {"indigo", 0x4B0082}, + {"ivory", 0xFFFFF0}, + {"khaki", 0xF0E68C}, + {"lavender", 0xE6E6FA}, + {"lavenderblush", 0xFFF0F5}, + {"lawngreen", 0x7CFC00}, + {"lemonchiffon", 0xFFFACD}, + {"lightblue", 0xADD8E6}, + {"lightcoral", 0xF08080}, + {"lightcyan", 0xE0FFFF}, + {"lightgoldenrodyellow", 0xFAFAD2}, + {"lightgray", 0xD3D3D3}, + {"lightgreen", 0x90EE90}, + {"lightgrey", 0xD3D3D3}, + {"lightpink", 0xFFB6C1}, + {"lightsalmon", 0xFFA07A}, + {"lightseagreen", 0x20B2AA}, + {"lightskyblue", 0x87CEFA}, + {"lightslategray", 0x778899}, + {"lightslategrey", 0x778899}, + {"lightsteelblue", 0xB0C4DE}, + {"lightyellow", 0xFFFFE0}, + {"lime", 0x00FF00}, + {"limegreen", 0x32CD32}, + {"linen", 0xFAF0E6}, + {"magenta", 0xFF00FF}, + {"maroon", 0x800000}, + {"mediumaquamarine", 0x66CDAA}, + {"mediumblue", 0x0000CD}, + {"mediumorchid", 0xBA55D3}, + {"mediumpurple", 0x9370DB}, + {"mediumseagreen", 0x3CB371}, + {"mediumslateblue", 0x7B68EE}, + {"mediumspringgreen", 0x00FA9A}, + {"mediumturquoise", 0x48D1CC}, + {"mediumvioletred", 0xC71585}, + {"midnightblue", 0x191970}, + {"mintcream", 0xF5FFFA}, + {"mistyrose", 0xFFE4E1}, + {"moccasin", 0xFFE4B5}, + {"navajowhite", 0xFFDEAD}, + {"navy", 0x000080}, + {"oldlace", 0xFDF5E6}, + {"olive", 0x808000}, + {"olivedrab", 0x6B8E23}, + {"orange", 0xFFA500}, + {"orangered", 0xFF4500}, + {"orchid", 0xDA70D6}, + {"palegoldenrod", 0xEEE8AA}, + {"palegreen", 0x98FB98}, + {"paleturquoise", 0xAFEEEE}, + {"palevioletred", 0xDB7093}, + {"papayawhip", 0xFFEFD5}, + {"peachpuff", 0xFFDAB9}, + {"peru", 0xCD853F}, + {"pink", 0xFFC0CB}, + {"plum", 0xDDA0DD}, + {"powderblue", 0xB0E0E6}, + {"purple", 0x800080}, + {"red", 0xFF0000}, + {"rosybrown", 0xBC8F8F}, + {"royalblue", 0x4169E1}, + {"saddlebrown", 0x8B4513}, + {"salmon", 0xFA8072}, + {"sandybrown", 0xF4A460}, + {"seagreen", 0x2E8B57}, + {"seashell", 0xFFF5EE}, + {"sienna", 0xA0522D}, + {"silver", 0xC0C0C0}, + {"skyblue", 0x87CEEB}, + {"slateblue", 0x6A5ACD}, + {"slategray", 0x708090}, + {"slategrey", 0x708090}, + {"snow", 0xFFFAFA}, + {"springgreen", 0x00FF7F}, + {"steelblue", 0x4682B4}, + {"tan", 0xD2B48C}, + {"teal", 0x008080}, + {"thistle", 0xD8BFD8}, + {"tomato", 0xFF6347}, + {"transparent", 0x000000}, + {"turquoise", 0x40E0D0}, + {"violet", 0xEE82EE}, + {"wheat", 0xF5DEB3}, + {"white", 0xFFFFFF}, + {"whitesmoke", 0xF5F5F5}, + {"yellow", 0xFFFF00}, + {"yellowgreen", 0x9ACD32}, + // CSS Color 4 addition + {"rebeccapurple", 0x663399}, + }; + // clang-format on + + auto it = namedColors.find(value); + if (it != namedColors.end()) { + auto color = Color::FromHex(it->second); + if (value == "transparent") { + color.alpha = 0; + } + return color; + } + + return {0, 0, 0, 1}; +} + +float SVGParserImpl::parseLength(const std::string& value, float containerSize) { + if (value.empty()) { + return 0; + } + + size_t idx = 0; + float num = std::stof(value, &idx); + + std::string unit = value.substr(idx); + if (unit == "%") { + return num / 100.0f * containerSize; + } + if (unit == "px" || unit.empty()) { + return num; + } + if (unit == "pt") { + return num * 1.333333f; + } + if (unit == "em" || unit == "rem") { + return num * 16.0f; // Assume 16px base font. + } + if (unit == "in") { + return num * 96.0f; + } + if (unit == "cm") { + return num * 37.795275591f; + } + if (unit == "mm") { + return num * 3.7795275591f; + } + + return num; +} + +std::vector SVGParserImpl::parseViewBox(const std::string& value) { + std::vector result; + if (value.empty()) { + return result; + } + + std::istringstream iss(value); + float num = 0; + while (iss >> num) { + result.push_back(num); + } + + return result; +} + +PathData SVGParserImpl::parsePoints(const std::string& value, bool closed) { + PathData path; + if (value.empty()) { + return path; + } + + std::vector points; + std::istringstream iss(value); + float num = 0; + while (iss >> num) { + points.push_back(num); + char c = 0; + if (iss.peek() == ',' || iss.peek() == ' ') { + iss >> c; + } + } + + if (points.size() >= 2) { + path.moveTo(points[0], points[1]); + for (size_t i = 2; i + 1 < points.size(); i += 2) { + path.lineTo(points[i], points[i + 1]); + } + if (closed) { + path.close(); + } + } + + return path; +} + +std::string SVGParserImpl::resolveUrl(const std::string& url) { + if (url.empty()) { + return ""; + } + // Handle url(#id) format. + if (url.find("url(") == 0) { + size_t start = url.find('#'); + size_t end = url.find(')'); + if (start != std::string::npos && end != std::string::npos) { + return url.substr(start + 1, end - start - 1); + } + } + // Handle #id format. + if (url[0] == '#') { + return url.substr(1); + } + return url; +} + +std::string SVGParserImpl::registerImageResource(const std::string& imageSource) { + if (imageSource.empty()) { + return ""; + } + + // Check if this image source has already been registered. + auto it = _imageSourceToId.find(imageSource); + if (it != _imageSourceToId.end()) { + return it->second; + } + + // Generate a unique image ID that doesn't conflict with existing SVG IDs. + std::string imageId = generateUniqueId("image"); + + // Create and add the image resource to the document. + auto imageNode = std::make_unique(); + imageNode->id = imageId; + imageNode->source = imageSource; + _document->resources.push_back(std::move(imageNode)); + + // Cache the mapping. + _imageSourceToId[imageSource] = imageId; + + return imageId; +} + +// Helper function to check if two VectorElement nodes are the same geometry. +static bool isSameGeometry(const Element* a, const Element* b) { + if (!a || !b || a->type() != b->type()) { + return false; + } + + switch (a->type()) { + case ElementType::Rectangle: { + auto rectA = static_cast(a); + auto rectB = static_cast(b); + return rectA->center.x == rectB->center.x && rectA->center.y == rectB->center.y && + rectA->size.width == rectB->size.width && rectA->size.height == rectB->size.height && + rectA->roundness == rectB->roundness; + } + case ElementType::Ellipse: { + auto ellipseA = static_cast(a); + auto ellipseB = static_cast(b); + return ellipseA->center.x == ellipseB->center.x && ellipseA->center.y == ellipseB->center.y && + ellipseA->size.width == ellipseB->size.width && + ellipseA->size.height == ellipseB->size.height; + } + case ElementType::Path: { + auto pathA = static_cast(a); + auto pathB = static_cast(b); + return pathA->data.toSVGString() == pathB->data.toSVGString(); + } + default: + return false; + } +} + +// Check if a layer is a simple shape layer (contains exactly one geometry and one Fill or Stroke). +static bool isSimpleShapeLayer(const Layer* layer, const Element*& outGeometry, + const Element*& outPainter) { + if (!layer || layer->contents.size() != 2) { + return false; + } + if (!layer->children.empty() || !layer->filters.empty() || !layer->styles.empty()) { + return false; + } + if (!layer->matrix.isIdentity() || layer->alpha != 1.0f) { + return false; + } + + const auto* first = layer->contents[0].get(); + const auto* second = layer->contents[1].get(); + + // Check if first is geometry and second is painter. + bool firstIsGeometry = (first->type() == ElementType::Rectangle || + first->type() == ElementType::Ellipse || first->type() == ElementType::Path); + bool secondIsPainter = + (second->type() == ElementType::Fill || second->type() == ElementType::Stroke); + + if (firstIsGeometry && secondIsPainter) { + outGeometry = first; + outPainter = second; + return true; + } + return false; +} + +void SVGParserImpl::mergeAdjacentLayers(std::vector>& layers) { + if (layers.size() < 2) { + return; + } + + std::vector> merged; + size_t i = 0; + + while (i < layers.size()) { + const Element* geomA = nullptr; + const Element* painterA = nullptr; + + if (isSimpleShapeLayer(layers[i].get(), geomA, painterA)) { + // Check if the next layer has the same geometry. + if (i + 1 < layers.size()) { + const Element* geomB = nullptr; + const Element* painterB = nullptr; + + if (isSimpleShapeLayer(layers[i + 1].get(), geomB, painterB) && + isSameGeometry(geomA, geomB)) { + // Merge: one has Fill, the other has Stroke. + bool aHasFill = (painterA->type() == ElementType::Fill); + bool bHasFill = (painterB->type() == ElementType::Fill); + + if (aHasFill != bHasFill) { + // Create merged layer. + auto mergedLayer = std::make_unique(); + + // Move geometry from first layer. + mergedLayer->contents.push_back(std::move(layers[i]->contents[0])); + + // Add Fill first, then Stroke (standard order). + if (aHasFill) { + mergedLayer->contents.push_back(std::move(layers[i]->contents[1])); + mergedLayer->contents.push_back(std::move(layers[i + 1]->contents[1])); + } else { + mergedLayer->contents.push_back(std::move(layers[i + 1]->contents[1])); + mergedLayer->contents.push_back(std::move(layers[i]->contents[1])); + } + + merged.push_back(std::move(mergedLayer)); + i += 2; // Skip both layers. + continue; + } + } + } + } + + // No merge, keep the layer as is. + merged.push_back(std::move(layers[i])); + i++; + } + + layers = std::move(merged); +} + +std::unique_ptr SVGParserImpl::convertMaskElement( + const std::shared_ptr& maskElement, const InheritedStyle& parentStyle) { + auto maskLayer = std::make_unique(); + maskLayer->id = getAttribute(maskElement, "id"); + maskLayer->name = maskLayer->id; + maskLayer->visible = false; + + // Parse mask contents. + auto child = maskElement->getFirstChild(); + while (child) { + if (child->name == "rect" || child->name == "circle" || child->name == "ellipse" || + child->name == "path" || child->name == "polygon" || child->name == "polyline") { + InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); + convertChildren(child, maskLayer->contents, inheritedStyle); + } else if (child->name == "g") { + // Handle group inside mask. + InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); + auto groupChild = child->getFirstChild(); + while (groupChild) { + convertChildren(groupChild, maskLayer->contents, inheritedStyle); + groupChild = groupChild->getNextSibling(); + } + } + child = child->getNextSibling(); + } + + return maskLayer; +} + +void SVGParserImpl::convertFilterElement( + const std::shared_ptr& filterElement, + std::vector>& filters) { + // Parse filter children to find effect elements. + auto child = filterElement->getFirstChild(); + while (child) { + if (child->name == "feGaussianBlur") { + auto blurFilter = std::make_unique(); + std::string stdDeviation = getAttribute(child, "stdDeviation", "0"); + // stdDeviation can be one value (both X and Y) or two values (X Y). + std::istringstream iss(stdDeviation); + float devX = 0, devY = 0; + iss >> devX; + if (!(iss >> devY)) { + devY = devX; + } + blurFilter->blurrinessX = devX; + blurFilter->blurrinessY = devY; + filters.push_back(std::move(blurFilter)); + } + child = child->getNextSibling(); + } +} + +void SVGParserImpl::collectAllIds(const std::shared_ptr& node) { + if (!node) { + return; + } + + // Collect id from this node. + auto [found, id] = node->findAttribute("id"); + if (found && !id.empty()) { + _existingIds.insert(id); + } + + // Recursively collect from children. + auto child = node->getFirstChild(); + while (child) { + collectAllIds(child); + child = child->getNextSibling(); + } +} + +std::string SVGParserImpl::generateUniqueId(const std::string& prefix) { + std::string id; + do { + id = "_" + prefix + std::to_string(_nextGeneratedId++); + } while (_existingIds.count(id) > 0); + _existingIds.insert(id); + return id; +} + +void SVGParserImpl::parseCustomData(const std::shared_ptr& element, Layer* layer) { + if (!element || !layer) { + return; + } + + // Iterate through all attributes and find data-* ones. + for (const auto& attr : element->attributes) { + if (attr.name.length() > 5 && attr.name.substr(0, 5) == "data-") { + // Remove "data-" prefix and store in customData. + std::string key = attr.name.substr(5); + layer->customData[key] = attr.value; + } + } +} + +} // namespace pagx diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 4f0fdf0a5d..0e5d5e0fb5 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -37,7 +37,7 @@ #include "pagx/model/Rectangle.h" #include "pagx/model/Stroke.h" #include "pagx/model/TextSpan.h" -#include "pagx/PAGXSVGParser.h" +#include "pagx/SVGImporter.h" #include "xml/XMLDOM.h" namespace pagx { @@ -58,7 +58,7 @@ struct InheritedStyle { */ class SVGParserImpl { public: - explicit SVGParserImpl(const PAGXSVGParser::Options& options); + explicit SVGParserImpl(const SVGImporter::Options& options); std::shared_ptr parse(const uint8_t* data, size_t length); std::shared_ptr parseFile(const std::string& filePath); @@ -137,7 +137,7 @@ class SVGParserImpl { // Parse data-* attributes from element and add to layer's customData. void parseCustomData(const std::shared_ptr& element, Layer* layer); - PAGXSVGParser::Options _options = {}; + SVGImporter::Options _options = {}; std::shared_ptr _document = nullptr; std::unordered_map> _defs = {}; std::vector> _maskLayers = {}; diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index feca7a8463..301f75eacb 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -36,6 +36,7 @@ #include "pagx/model/Layer.h" #include "pagx/model/LinearGradient.h" #include "pagx/model/MergePath.h" +#include "pagx/model/Node.h" #include "pagx/model/Path.h" #include "pagx/model/Polystar.h" #include "pagx/model/RadialGradient.h" @@ -46,7 +47,7 @@ #include "pagx/model/Stroke.h" #include "pagx/model/TextSpan.h" #include "pagx/model/TrimPath.h" -#include "pagx/PAGXSVGParser.h" +#include "pagx/SVGImporter.h" #include "tgfx/core/Data.h" #include "tgfx/core/Font.h" #include "tgfx/core/Image.h" @@ -611,7 +612,7 @@ class LayerBuilderImpl { return nullptr; } for (const auto& resource : *_resources) { - if (resource->type() == ResourceType::Image) { + if (resource->nodeType() == NodeType::Image) { auto imageNode = static_cast(resource.get()); if (imageNode->id == resourceId) { if (imageNode->source.find("data:") == 0) { @@ -744,7 +745,7 @@ class LayerBuilderImpl { } LayerBuilder::Options _options = {}; - const std::vector>* _resources = nullptr; + const std::vector>* _resources = nullptr; std::shared_ptr _textShaper = nullptr; std::unordered_map> _layerById = {}; std::vector, std::string, tgfx::LayerMaskType>> @@ -784,7 +785,7 @@ PAGXContent LayerBuilder::FromData(const uint8_t* data, size_t length, const Opt } PAGXContent LayerBuilder::FromSVGFile(const std::string& filePath, const Options& options) { - auto document = PAGXSVGParser::Parse(filePath); + auto document = SVGImporter::Parse(filePath); if (!document) { return {}; } diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 5929e3379b..81c5d9af3c 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -29,7 +29,7 @@ #include "pagx/model/RadialGradient.h" #include "pagx/model/Rectangle.h" #include "pagx/model/SolidColor.h" -#include "pagx/PAGXSVGParser.h" +#include "pagx/SVGImporter.h" #include "pagx/model/PathData.h" #include "tgfx/core/Data.h" #include "tgfx/core/Stream.h" @@ -139,8 +139,8 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { } // Save PAGX file to output directory - pagx::PAGXSVGParser::Options parserOptions; - auto doc = pagx::PAGXSVGParser::Parse(svgPath, parserOptions); + pagx::SVGImporter::Options parserOptions; + auto doc = pagx::SVGImporter::Parse(svgPath, parserOptions); if (doc) { std::string xml = doc->toXML(); auto pagxData = Data::MakeWithCopy(xml.data(), xml.size()); From 89f14391969644f29143df0fa3845943a1802d2e Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 18:24:52 +0800 Subject: [PATCH 098/678] Remove deprecated Resource class and PAGXSVGParser files - Delete Resource.h (replaced by Node base class) - Delete PAGXSVGParser.h and PAGXSVGParser.cpp (replaced by SVGImporter) --- pagx/include/pagx/PAGXSVGParser.h | 72 -- pagx/include/pagx/model/Resource.h | 70 -- pagx/src/svg/PAGXSVGParser.cpp | 1718 ---------------------------- 3 files changed, 1860 deletions(-) delete mode 100644 pagx/include/pagx/PAGXSVGParser.h delete mode 100644 pagx/include/pagx/model/Resource.h delete mode 100644 pagx/src/svg/PAGXSVGParser.cpp diff --git a/pagx/include/pagx/PAGXSVGParser.h b/pagx/include/pagx/PAGXSVGParser.h deleted file mode 100644 index 072ef87a17..0000000000 --- a/pagx/include/pagx/PAGXSVGParser.h +++ /dev/null @@ -1,72 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include -#include -#include "pagx/model/Document.h" - -namespace pagx { - -/** - * PAGXSVGParser converts SVG documents to PAGXDocument. - * This parser is independent of tgfx and preserves complete SVG information. - */ -class PAGXSVGParser { - public: - struct Options { - /** - * If true, unsupported SVG elements are preserved as Unknown nodes. - */ - bool preserveUnknownElements; - - /** - * If true, references are expanded to actual content. - */ - bool expandUseReferences; - - /** - * If true, nested transforms are flattened into single matrices. - */ - bool flattenTransforms; - - Options() : preserveUnknownElements(false), expandUseReferences(true), flattenTransforms(false) { - } - }; - - /** - * Parses an SVG file and creates a PAGXDocument. - */ - static std::shared_ptr Parse(const std::string& filePath, - const Options& options = Options()); - - /** - * Parses SVG data and creates a PAGXDocument. - */ - static std::shared_ptr Parse(const uint8_t* data, size_t length, - const Options& options = Options()); - - /** - * Parses an SVG string and creates a PAGXDocument. - */ - static std::shared_ptr ParseString(const std::string& svgContent, - const Options& options = Options()); -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/Resource.h b/pagx/include/pagx/model/Resource.h deleted file mode 100644 index 8216aef3c0..0000000000 --- a/pagx/include/pagx/model/Resource.h +++ /dev/null @@ -1,70 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * ResourceType enumerates all types of resources that can be stored in a PAGX document. - */ -enum class ResourceType { - /** - * An image resource. - */ - Image, - /** - * A reusable path data resource. - */ - PathData, - /** - * A composition resource containing layers. - */ - Composition -}; - -/** - * Returns the string name of a resource type. - */ -const char* ResourceTypeName(ResourceType type); - -/** - * Resource is the base class for all resources in a PAGX document. Resources are reusable items - * that can be referenced by ID (e.g., "#imageId"). - */ -class Resource { - public: - virtual ~Resource() = default; - - /** - * Returns the resource type of this resource. - */ - virtual ResourceType type() const = 0; - - /** - * Returns the unique identifier of this resource. - */ - virtual const std::string& resourceId() const = 0; - - protected: - Resource() = default; -}; - -} // namespace pagx diff --git a/pagx/src/svg/PAGXSVGParser.cpp b/pagx/src/svg/PAGXSVGParser.cpp deleted file mode 100644 index b9c44baa06..0000000000 --- a/pagx/src/svg/PAGXSVGParser.cpp +++ /dev/null @@ -1,1718 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "pagx/PAGXSVGParser.h" -#include -#include -#include -#include -#include "pagx/model/Document.h" -#include "SVGParserInternal.h" -#include "xml/XMLDOM.h" - -namespace pagx { - -std::shared_ptr PAGXSVGParser::Parse(const std::string& filePath, - const Options& options) { - SVGParserImpl parser(options); - auto doc = parser.parseFile(filePath); - if (doc) { - auto lastSlash = filePath.find_last_of("/\\"); - if (lastSlash != std::string::npos) { - doc->basePath = filePath.substr(0, lastSlash + 1); - } - } - return doc; -} - -std::shared_ptr PAGXSVGParser::Parse(const uint8_t* data, size_t length, - const Options& options) { - SVGParserImpl parser(options); - return parser.parse(data, length); -} - -std::shared_ptr PAGXSVGParser::ParseString(const std::string& svgContent, - const Options& options) { - return Parse(reinterpret_cast(svgContent.data()), svgContent.size(), options); -} - -// ============== SVGParserImpl ============== - -SVGParserImpl::SVGParserImpl(const PAGXSVGParser::Options& options) : _options(options) { -} - -std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { - if (!data || length == 0) { - return nullptr; - } - - auto dom = DOM::Make(data, length); - if (!dom) { - return nullptr; - } - - return parseDOM(dom); -} - -std::shared_ptr SVGParserImpl::parseFile(const std::string& filePath) { - auto dom = DOM::MakeFromFile(filePath); - if (!dom) { - return nullptr; - } - - return parseDOM(dom); -} - -std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, - const std::string& name, - const std::string& defaultValue) const { - auto [found, value] = node->findAttribute(name); - return found ? value : defaultValue; -} - -std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& dom) { - auto root = dom->getRootNode(); - if (!root || root->name != "svg") { - return nullptr; - } - - // Parse viewBox and dimensions. - auto viewBox = parseViewBox(getAttribute(root, "viewBox")); - float width = parseLength(getAttribute(root, "width"), 0); - float height = parseLength(getAttribute(root, "height"), 0); - - if (viewBox.size() >= 4) { - _viewBoxWidth = viewBox[2]; - _viewBoxHeight = viewBox[3]; - if (width == 0) { - width = _viewBoxWidth; - } - if (height == 0) { - height = _viewBoxHeight; - } - } else { - _viewBoxWidth = width; - _viewBoxHeight = height; - } - - if (width <= 0 || height <= 0) { - return nullptr; - } - - _document = Document::Make(width, height); - - // Collect all IDs from the SVG to avoid conflicts when generating new IDs. - collectAllIds(root); - - // First pass: collect defs. - auto child = root->getFirstChild(); - while (child) { - if (child->name == "defs") { - parseDefs(child); - } - child = child->getNextSibling(); - } - - // Check if we need a viewBox transform. - bool needsViewBoxTransform = false; - Matrix viewBoxMatrix = Matrix::Identity(); - if (viewBox.size() >= 4) { - float viewBoxX = viewBox[0]; - float viewBoxY = viewBox[1]; - float viewBoxW = viewBox[2]; - float viewBoxH = viewBox[3]; - - if (viewBoxW > 0 && viewBoxH > 0 && - (viewBoxX != 0 || viewBoxY != 0 || viewBoxW != width || viewBoxH != height)) { - // Calculate uniform scale (meet behavior: fit inside viewport). - float scaleX = width / viewBoxW; - float scaleY = height / viewBoxH; - float scale = std::min(scaleX, scaleY); - - // Calculate translation to center content (xMidYMid). - float translateX = (width - viewBoxW * scale) / 2.0f - viewBoxX * scale; - float translateY = (height - viewBoxH * scale) / 2.0f - viewBoxY * scale; - - // Build the transform matrix: scale then translate. - viewBoxMatrix = Matrix::Translate(translateX, translateY) * Matrix::Scale(scale, scale); - needsViewBoxTransform = true; - } - } - - // Compute initial inherited style from the root element. - InheritedStyle rootStyle = {}; - rootStyle = computeInheritedStyle(root, rootStyle); - - // Collect converted layers. - std::vector> convertedLayers; - child = root->getFirstChild(); - while (child) { - if (child->name != "defs") { - auto layer = convertToLayer(child, rootStyle); - if (layer) { - convertedLayers.push_back(std::move(layer)); - } - } - child = child->getNextSibling(); - } - - // Add collected mask layers (invisible, used as mask references). - for (auto& maskLayer : _maskLayers) { - convertedLayers.insert(convertedLayers.begin(), std::move(maskLayer)); - } - _maskLayers.clear(); - - // Merge adjacent layers with the same geometry (optimize Fill + Stroke into one Layer). - mergeAdjacentLayers(convertedLayers); - - // If viewBox transform is needed, wrap in a root layer with the transform. - // Otherwise, add layers directly to document (no root wrapper). - if (needsViewBoxTransform) { - auto rootLayer = std::make_unique(); - rootLayer->matrix = viewBoxMatrix; - for (auto& layer : convertedLayers) { - rootLayer->children.push_back(std::move(layer)); - } - _document->layers.push_back(std::move(rootLayer)); - } else { - for (auto& layer : convertedLayers) { - _document->layers.push_back(std::move(layer)); - } - } - - return _document; -} - -InheritedStyle SVGParserImpl::computeInheritedStyle(const std::shared_ptr& element, - const InheritedStyle& parentStyle) { - InheritedStyle style = parentStyle; - - std::string fill = getAttribute(element, "fill"); - if (!fill.empty()) { - style.fill = fill; - } - - std::string stroke = getAttribute(element, "stroke"); - if (!stroke.empty()) { - style.stroke = stroke; - } - - std::string fillOpacity = getAttribute(element, "fill-opacity"); - if (!fillOpacity.empty()) { - style.fillOpacity = fillOpacity; - } - - std::string strokeOpacity = getAttribute(element, "stroke-opacity"); - if (!strokeOpacity.empty()) { - style.strokeOpacity = strokeOpacity; - } - - std::string fillRule = getAttribute(element, "fill-rule"); - if (!fillRule.empty()) { - style.fillRule = fillRule; - } - - return style; -} - -void SVGParserImpl::parseDefs(const std::shared_ptr& defsNode) { - auto child = defsNode->getFirstChild(); - while (child) { - std::string id = getAttribute(child, "id"); - if (!id.empty()) { - _defs[id] = child; - } - // Also handle nested defs. - if (child->name == "defs") { - parseDefs(child); - } - child = child->getNextSibling(); - } -} - -std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, - const InheritedStyle& parentStyle) { - const auto& tag = element->name; - - if (tag == "defs" || tag == "linearGradient" || tag == "radialGradient" || tag == "pattern" || - tag == "mask" || tag == "clipPath" || tag == "filter") { - return nullptr; - } - - // Compute inherited style for this element. - InheritedStyle inheritedStyle = computeInheritedStyle(element, parentStyle); - - auto layer = std::make_unique(); - - // Parse common layer attributes. - layer->id = getAttribute(element, "id"); - - // Parse data-* custom attributes. - parseCustomData(element, layer.get()); - - std::string transform = getAttribute(element, "transform"); - if (!transform.empty()) { - layer->matrix = parseTransform(transform); - } - - std::string opacity = getAttribute(element, "opacity"); - if (!opacity.empty()) { - layer->alpha = std::stof(opacity); - } - - std::string display = getAttribute(element, "display"); - if (display == "none") { - layer->visible = false; - } - - std::string visibility = getAttribute(element, "visibility"); - if (visibility == "hidden") { - layer->visible = false; - } - - // Handle mask attribute. - std::string maskAttr = getAttribute(element, "mask"); - if (!maskAttr.empty() && maskAttr != "none") { - std::string maskId = resolveUrl(maskAttr); - auto maskIt = _defs.find(maskId); - if (maskIt != _defs.end()) { - // Convert mask element to a mask layer. - auto maskLayer = convertMaskElement(maskIt->second, inheritedStyle); - if (maskLayer) { - layer->mask = "#" + maskLayer->id; - // SVG masks use luminance by default. - layer->maskType = MaskType::Luminance; - // Add mask layer as invisible layer to the document. - _maskLayers.push_back(std::move(maskLayer)); - } - } - } - - // Handle filter attribute. - std::string filterAttr = getAttribute(element, "filter"); - if (!filterAttr.empty() && filterAttr != "none") { - std::string filterId = resolveUrl(filterAttr); - auto filterIt = _defs.find(filterId); - if (filterIt != _defs.end()) { - convertFilterElement(filterIt->second, layer->filters); - } - } - - // Convert contents. - if (tag == "g" || tag == "svg") { - // Group: convert children as child layers. - auto child = element->getFirstChild(); - while (child) { - auto childLayer = convertToLayer(child, inheritedStyle); - if (childLayer) { - layer->children.push_back(std::move(childLayer)); - } - child = child->getNextSibling(); - } - } else { - // Shape element: convert to vector contents. - convertChildren(element, layer->contents, inheritedStyle); - } - - return layer; -} - -void SVGParserImpl::convertChildren(const std::shared_ptr& element, - std::vector>& contents, - const InheritedStyle& inheritedStyle) { - const auto& tag = element->name; - - // Handle text element specially - it returns a Group with TextSpan. - if (tag == "text") { - auto textGroup = convertText(element, inheritedStyle); - if (textGroup) { - contents.push_back(std::move(textGroup)); - } - return; - } - - auto shapeElement = convertElement(element); - if (shapeElement) { - contents.push_back(std::move(shapeElement)); - } - - addFillStroke(element, contents, inheritedStyle); -} - -std::unique_ptr SVGParserImpl::convertElement( - const std::shared_ptr& element) { - const auto& tag = element->name; - - if (tag == "rect") { - return convertRect(element); - } else if (tag == "circle") { - return convertCircle(element); - } else if (tag == "ellipse") { - return convertEllipse(element); - } else if (tag == "line") { - return convertLine(element); - } else if (tag == "polyline") { - return convertPolyline(element); - } else if (tag == "polygon") { - return convertPolygon(element); - } else if (tag == "path") { - return convertPath(element); - } else if (tag == "use") { - return convertUse(element); - } - - return nullptr; -} - -std::unique_ptr SVGParserImpl::convertG(const std::shared_ptr& element, - const InheritedStyle& parentStyle) { - // Compute inherited style for this group element. - InheritedStyle inheritedStyle = computeInheritedStyle(element, parentStyle); - - auto group = std::make_unique(); - - // group->name (removed) = getAttribute(element, "id"); - - std::string transform = getAttribute(element, "transform"); - if (!transform.empty()) { - // For Group, we need to decompose the matrix into position/rotation/scale. - // For simplicity, just store as position offset for translation. - Matrix m = parseTransform(transform); - group->position = {m.tx, m.ty}; - // Note: Full matrix decomposition would be more complex. - } - - std::string opacity = getAttribute(element, "opacity"); - if (!opacity.empty()) { - group->alpha = std::stof(opacity); - } - - auto child = element->getFirstChild(); - while (child) { - auto childElement = convertElement(child); - if (childElement) { - group->elements.push_back(std::move(childElement)); - } - addFillStroke(child, group->elements, inheritedStyle); - child = child->getNextSibling(); - } - - return group; -} - -std::unique_ptr SVGParserImpl::convertRect( - const std::shared_ptr& element) { - float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); - float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); - float width = parseLength(getAttribute(element, "width"), _viewBoxWidth); - float height = parseLength(getAttribute(element, "height"), _viewBoxHeight); - float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); - float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); - - if (ry == 0) { - ry = rx; - } - - auto rect = std::make_unique(); - rect->center.x = x + width / 2; - rect->center.y = y + height / 2; - rect->size.width = width; - rect->size.height = height; - rect->roundness = std::max(rx, ry); - - return rect; -} - -std::unique_ptr SVGParserImpl::convertCircle( - const std::shared_ptr& element) { - float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); - float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); - float r = parseLength(getAttribute(element, "r"), _viewBoxWidth); - - auto ellipse = std::make_unique(); - ellipse->center.x = cx; - ellipse->center.y = cy; - ellipse->size.width = r * 2; - ellipse->size.height = r * 2; - - return ellipse; -} - -std::unique_ptr SVGParserImpl::convertEllipse( - const std::shared_ptr& element) { - float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); - float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); - float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); - float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); - - auto ellipse = std::make_unique(); - ellipse->center.x = cx; - ellipse->center.y = cy; - ellipse->size.width = rx * 2; - ellipse->size.height = ry * 2; - - return ellipse; -} - -std::unique_ptr SVGParserImpl::convertLine( - const std::shared_ptr& element) { - float x1 = parseLength(getAttribute(element, "x1"), _viewBoxWidth); - float y1 = parseLength(getAttribute(element, "y1"), _viewBoxHeight); - float x2 = parseLength(getAttribute(element, "x2"), _viewBoxWidth); - float y2 = parseLength(getAttribute(element, "y2"), _viewBoxHeight); - - auto path = std::make_unique(); - path->data.moveTo(x1, y1); - path->data.lineTo(x2, y2); - - return path; -} - -std::unique_ptr SVGParserImpl::convertPolyline( - const std::shared_ptr& element) { - auto path = std::make_unique(); - path->data = parsePoints(getAttribute(element, "points"), false); - return path; -} - -std::unique_ptr SVGParserImpl::convertPolygon( - const std::shared_ptr& element) { - auto path = std::make_unique(); - path->data = parsePoints(getAttribute(element, "points"), true); - return path; -} - -std::unique_ptr SVGParserImpl::convertPath( - const std::shared_ptr& element) { - auto path = std::make_unique(); - std::string d = getAttribute(element, "d"); - if (!d.empty()) { - path->data = PathData::FromSVGString(d); - } - return path; -} - -std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr& element, - const InheritedStyle& inheritedStyle) { - auto group = std::make_unique(); - - float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); - float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); - - // Parse text-anchor attribute for x position adjustment. - // SVG text-anchor affects horizontal alignment: start (default), middle, end. - // Since PAGX TextSpan doesn't have text-anchor, we'll note this for future - // position adjustment after text shaping (requires knowing text width). - std::string anchor = getAttribute(element, "text-anchor"); - // Note: text-anchor adjustment would require knowing the text width after shaping. - // For now, we store the x position as-is. A full implementation would need to - // adjust x based on anchor after calculating the text bounds. - - // Get text content from child text nodes. - std::string textContent; - auto child = element->getFirstChild(); - while (child) { - if (child->type == DOMNodeType::Text) { - textContent += child->name; - } - child = child->getNextSibling(); - } - - if (!textContent.empty()) { - auto textSpan = std::make_unique(); - textSpan->x = x; - textSpan->y = y; - textSpan->text = textContent; - - std::string fontFamily = getAttribute(element, "font-family"); - if (!fontFamily.empty()) { - textSpan->font = fontFamily; - } - - std::string fontSize = getAttribute(element, "font-size"); - if (!fontSize.empty()) { - textSpan->fontSize = parseLength(fontSize, _viewBoxHeight); - } - - group->elements.push_back(std::move(textSpan)); - } - - addFillStroke(element, group->elements, inheritedStyle); - return group; -} - -std::unique_ptr SVGParserImpl::convertUse( - const std::shared_ptr& element) { - std::string href = getAttribute(element, "xlink:href"); - if (href.empty()) { - href = getAttribute(element, "href"); - } - - std::string refId = resolveUrl(href); - auto it = _defs.find(refId); - if (it == _defs.end()) { - return nullptr; - } - - if (_options.expandUseReferences) { - auto node = convertElement(it->second); - if (node) { - float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); - float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); - if (x != 0 || y != 0) { - // Wrap in a group with translation. - auto group = std::make_unique(); - group->position = {x, y}; - group->elements.push_back(std::move(node)); - return group; - } - } - return node; - } - - // For non-expanded use references, just create an empty group for now. - auto group = std::make_unique(); - // group->name (removed) = "_useRef:" + refId; - return group; -} - -std::unique_ptr SVGParserImpl::convertLinearGradient( - const std::shared_ptr& element, const Rect& shapeBounds) { - auto gradient = std::make_unique(); - - gradient->id = getAttribute(element, "id"); - - // Check gradientUnits - determines how gradient coordinates are interpreted. - // Default is objectBoundingBox, meaning values are 0-1 ratios of the shape bounds. - std::string gradientUnits = getAttribute(element, "gradientUnits", "objectBoundingBox"); - bool useOBB = (gradientUnits == "objectBoundingBox"); - - // Parse gradient coordinates. - float x1 = parseLength(getAttribute(element, "x1", "0%"), 1.0f); - float y1 = parseLength(getAttribute(element, "y1", "0%"), 1.0f); - float x2 = parseLength(getAttribute(element, "x2", "100%"), 1.0f); - float y2 = parseLength(getAttribute(element, "y2", "0%"), 1.0f); - - // Parse gradientTransform. - std::string gradientTransform = getAttribute(element, "gradientTransform"); - Matrix transformMatrix = gradientTransform.empty() ? Matrix::Identity() - : parseTransform(gradientTransform); - - if (useOBB) { - // For objectBoundingBox, coordinates are normalized 0-1. - // Convert to actual coordinates based on shape bounds. - Point start = {shapeBounds.x + x1 * shapeBounds.width, shapeBounds.y + y1 * shapeBounds.height}; - Point end = {shapeBounds.x + x2 * shapeBounds.width, shapeBounds.y + y2 * shapeBounds.height}; - // Apply gradient transform after converting to actual coordinates. - start = transformMatrix.mapPoint(start); - end = transformMatrix.mapPoint(end); - gradient->startPoint = start; - gradient->endPoint = end; - } else { - // For userSpaceOnUse, coordinates are in user space. - // Apply gradient transform to user space points. - Point start = {x1, y1}; - Point end = {x2, y2}; - start = transformMatrix.mapPoint(start); - end = transformMatrix.mapPoint(end); - gradient->startPoint = start; - gradient->endPoint = end; - } - - // Parse stops. - auto child = element->getFirstChild(); - while (child) { - if (child->name == "stop") { - ColorStop stop; - stop.offset = parseLength(getAttribute(child, "offset", "0"), 1.0f); - stop.color = parseColor(getAttribute(child, "stop-color", "#000000")); - float opacity = parseLength(getAttribute(child, "stop-opacity", "1"), 1.0f); - stop.color.alpha = opacity; - gradient->colorStops.push_back(stop); - } - child = child->getNextSibling(); - } - - return gradient; -} - -std::unique_ptr SVGParserImpl::convertRadialGradient( - const std::shared_ptr& element, const Rect& shapeBounds) { - auto gradient = std::make_unique(); - - gradient->id = getAttribute(element, "id"); - - // Check gradientUnits - determines how gradient coordinates are interpreted. - std::string gradientUnits = getAttribute(element, "gradientUnits", "objectBoundingBox"); - bool useOBB = (gradientUnits == "objectBoundingBox"); - - // Parse gradient coordinates. - float cx = parseLength(getAttribute(element, "cx", "50%"), 1.0f); - float cy = parseLength(getAttribute(element, "cy", "50%"), 1.0f); - float r = parseLength(getAttribute(element, "r", "50%"), 1.0f); - - // Parse gradientTransform. - std::string gradientTransform = getAttribute(element, "gradientTransform"); - Matrix transformMatrix = gradientTransform.empty() ? Matrix::Identity() - : parseTransform(gradientTransform); - - if (useOBB) { - // For objectBoundingBox, convert normalized coordinates to actual coordinates. - Point center = {shapeBounds.x + cx * shapeBounds.width, - shapeBounds.y + cy * shapeBounds.height}; - // Radius is scaled by the average of width and height. - float actualRadius = r * (shapeBounds.width + shapeBounds.height) / 2.0f; - - // Apply gradientTransform after converting to actual coordinates. - center = transformMatrix.mapPoint(center); - // For radius, account for scaling in the transform. - float scaleX = std::sqrt(transformMatrix.a * transformMatrix.a + - transformMatrix.b * transformMatrix.b); - float scaleY = std::sqrt(transformMatrix.c * transformMatrix.c + - transformMatrix.d * transformMatrix.d); - actualRadius *= (scaleX + scaleY) / 2.0f; - - gradient->center = center; - gradient->radius = actualRadius; - - // Store the matrix for non-uniform scaling (rotation, skew, etc.). - if (!transformMatrix.isIdentity()) { - gradient->matrix = transformMatrix; - } - } else { - // For userSpaceOnUse, coordinates are in user space. - if (!gradientTransform.empty()) { - Point center = {cx, cy}; - center = transformMatrix.mapPoint(center); - gradient->center = center; - - float scaleX = std::sqrt(transformMatrix.a * transformMatrix.a + - transformMatrix.b * transformMatrix.b); - float scaleY = std::sqrt(transformMatrix.c * transformMatrix.c + - transformMatrix.d * transformMatrix.d); - gradient->radius = r * (scaleX + scaleY) / 2.0f; - - if (!transformMatrix.isIdentity()) { - gradient->matrix = transformMatrix; - } - } else { - gradient->center.x = cx; - gradient->center.y = cy; - gradient->radius = r; - } - } - - // Parse stops. - auto child = element->getFirstChild(); - while (child) { - if (child->name == "stop") { - ColorStop stop; - stop.offset = parseLength(getAttribute(child, "offset", "0"), 1.0f); - stop.color = parseColor(getAttribute(child, "stop-color", "#000000")); - float opacity = parseLength(getAttribute(child, "stop-opacity", "1"), 1.0f); - stop.color.alpha = opacity; - gradient->colorStops.push_back(stop); - } - child = child->getNextSibling(); - } - - return gradient; -} - -std::unique_ptr SVGParserImpl::convertPattern( - const std::shared_ptr& element, const Rect& shapeBounds) { - auto pattern = std::make_unique(); - - pattern->id = getAttribute(element, "id"); - - // SVG patterns use repeat by default. - pattern->tileModeX = TileMode::Repeat; - pattern->tileModeY = TileMode::Repeat; - - // Parse pattern dimensions from SVG attributes. - float patternWidth = parseLength(getAttribute(element, "width"), 1.0f); - float patternHeight = parseLength(getAttribute(element, "height"), 1.0f); - - // Check patternUnits - determines how pattern x/y/width/height are interpreted. - // Default is objectBoundingBox, meaning values are relative to the shape bounds. - std::string patternUnitsStr = getAttribute(element, "patternUnits", "objectBoundingBox"); - bool patternUnitsOBB = (patternUnitsStr == "objectBoundingBox"); - - // Check patternContentUnits - determines how pattern content coordinates are interpreted. - // Default is userSpaceOnUse, meaning content uses absolute coordinates. - std::string contentUnitsStr = getAttribute(element, "patternContentUnits", "userSpaceOnUse"); - bool contentUnitsOBB = (contentUnitsStr == "objectBoundingBox"); - - // Calculate the actual tile size in user space. - // When patternUnits is objectBoundingBox, pattern dimensions are 0-1 ratios of shape bounds. - float tileWidth = patternUnitsOBB ? patternWidth * shapeBounds.width : patternWidth; - float tileHeight = patternUnitsOBB ? patternHeight * shapeBounds.height : patternHeight; - - // Look for image reference inside the pattern. - auto child = element->getFirstChild(); - while (child) { - if (child->name == "use") { - std::string href = getAttribute(child, "xlink:href"); - if (href.empty()) { - href = getAttribute(child, "href"); - } - std::string imageId = resolveUrl(href); - - // Find the referenced image in defs. - auto imgIt = _defs.find(imageId); - if (imgIt != _defs.end() && imgIt->second->name == "image") { - std::string imageHref = getAttribute(imgIt->second, "xlink:href"); - if (imageHref.empty()) { - imageHref = getAttribute(imgIt->second, "href"); - } - - // Register the image resource and use the reference ID. - std::string resourceId = registerImageResource(imageHref); - pattern->image = "#" + resourceId; - - // Get image display dimensions from SVG (these are the dimensions in pattern content space). - float imageWidth = parseLength(getAttribute(imgIt->second, "width"), 1.0f); - float imageHeight = parseLength(getAttribute(imgIt->second, "height"), 1.0f); - - // Parse transform on the use element. - std::string useTransform = getAttribute(child, "transform"); - Matrix useMatrix = useTransform.empty() ? Matrix::Identity() : parseTransform(useTransform); - - // Calculate the image's actual size in user space (considering content units and transform). - float imageSizeInUserSpaceX = 0; - float imageSizeInUserSpaceY = 0; - if (contentUnitsOBB) { - // When patternContentUnits is objectBoundingBox, image dimensions are 0-1 ratios. - // Apply use transform (e.g., scale(0.005)) to map image to content space, - // then scale by shape bounds to get user space size. - imageSizeInUserSpaceX = imageWidth * useMatrix.a * shapeBounds.width; - imageSizeInUserSpaceY = imageHeight * useMatrix.d * shapeBounds.height; - } else { - // When patternContentUnits is userSpaceOnUse, image dimensions are in user space. - imageSizeInUserSpaceX = imageWidth * useMatrix.a; - imageSizeInUserSpaceY = imageHeight * useMatrix.d; - } - - // The ImagePattern shader tiles the original image pixels. - // We need to scale the image so it renders at the correct size within the tile. - // Since tgfx ImagePattern uses the image's original pixel dimensions as the base, - // the matrix should scale the image to match imageSizeInUserSpace. - // Note: imageWidth here is the SVG display size, which equals original pixel size - // when the image is embedded at 1:1 scale. - float scaleX = imageSizeInUserSpaceX / imageWidth; - float scaleY = imageSizeInUserSpaceY / imageHeight; - - // PAGX ImagePattern coordinates are relative to the geometry's local origin (0,0). - // SVG pattern with objectBoundingBox is relative to the shape's bounding box. - // We need to translate the pattern to align with the shape bounds. - // Matrix multiplication order: translate first, then scale (right to left). - pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y) * - Matrix::Scale(scaleX, scaleY); - } - } else if (child->name == "image") { - // Direct image element inside pattern. - std::string imageHref = getAttribute(child, "xlink:href"); - if (imageHref.empty()) { - imageHref = getAttribute(child, "href"); - } - - // Register the image resource and use the reference ID. - std::string resourceId = registerImageResource(imageHref); - pattern->image = "#" + resourceId; - - float imageWidth = parseLength(getAttribute(child, "width"), 1.0f); - float imageHeight = parseLength(getAttribute(child, "height"), 1.0f); - - if (contentUnitsOBB) { - // Image dimensions are 0-1 ratios, scale by shape bounds. - pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y) * - Matrix::Scale(shapeBounds.width, shapeBounds.height); - } else { - // Image dimensions are absolute, translate to shape bounds origin. - pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y); - } - } - child = child->getNextSibling(); - } - - return pattern; -} - -void SVGParserImpl::addFillStroke(const std::shared_ptr& element, - std::vector>& contents, - const InheritedStyle& inheritedStyle) { - // Get shape bounds for pattern calculations (computed once, used if needed). - Rect shapeBounds = getShapeBounds(element); - - // Determine effective fill value (element attribute overrides inherited). - std::string fill = getAttribute(element, "fill"); - if (fill.empty()) { - fill = inheritedStyle.fill; - } - - // Only add fill if we have an effective fill value that is not "none". - // If fill is empty and no inherited value, SVG default is black fill. - // But if inherited value is "none", we skip fill entirely. - if (fill != "none") { - if (fill.empty()) { - // No fill specified anywhere - use SVG default black. - auto fillNode = std::make_unique(); - fillNode->color = "#000000"; - contents.push_back(std::move(fillNode)); - } else if (fill.find("url(") == 0) { - auto fillNode = std::make_unique(); - std::string refId = resolveUrl(fill); - - // Try to inline the gradient or pattern. - // Don't set fillNode->color when using colorSource. - auto it = _defs.find(refId); - if (it != _defs.end()) { - if (it->second->name == "linearGradient") { - fillNode->colorSource = convertLinearGradient(it->second, shapeBounds); - } else if (it->second->name == "radialGradient") { - fillNode->colorSource = convertRadialGradient(it->second, shapeBounds); - } else if (it->second->name == "pattern") { - fillNode->colorSource = convertPattern(it->second, shapeBounds); - } - } - contents.push_back(std::move(fillNode)); - } else { - auto fillNode = std::make_unique(); - - // Determine effective fill-opacity. - std::string fillOpacity = getAttribute(element, "fill-opacity"); - if (fillOpacity.empty()) { - fillOpacity = inheritedStyle.fillOpacity; - } - if (!fillOpacity.empty()) { - fillNode->alpha = std::stof(fillOpacity); - } - - // Store the original color string for later parsing in LayerBuilder. - fillNode->color = fill; - - // Determine effective fill-rule. - std::string fillRule = getAttribute(element, "fill-rule"); - if (fillRule.empty()) { - fillRule = inheritedStyle.fillRule; - } - if (fillRule == "evenodd") { - fillNode->fillRule = FillRule::EvenOdd; - } - - contents.push_back(std::move(fillNode)); - } - } - - // Determine effective stroke value (element attribute overrides inherited). - std::string stroke = getAttribute(element, "stroke"); - if (stroke.empty()) { - stroke = inheritedStyle.stroke; - } - - if (!stroke.empty() && stroke != "none") { - auto strokeNode = std::make_unique(); - - if (stroke.find("url(") == 0) { - std::string refId = resolveUrl(stroke); - - // Don't set strokeNode->color when using colorSource. - auto it = _defs.find(refId); - if (it != _defs.end()) { - if (it->second->name == "linearGradient") { - strokeNode->colorSource = convertLinearGradient(it->second, shapeBounds); - } else if (it->second->name == "radialGradient") { - strokeNode->colorSource = convertRadialGradient(it->second, shapeBounds); - } else if (it->second->name == "pattern") { - strokeNode->colorSource = convertPattern(it->second, shapeBounds); - } - } - } else { - // Determine effective stroke-opacity. - std::string strokeOpacity = getAttribute(element, "stroke-opacity"); - if (strokeOpacity.empty()) { - strokeOpacity = inheritedStyle.strokeOpacity; - } - if (!strokeOpacity.empty()) { - strokeNode->alpha = std::stof(strokeOpacity); - } - - // Store the original color string for later parsing in LayerBuilder. - strokeNode->color = stroke; - } - - std::string strokeWidth = getAttribute(element, "stroke-width"); - if (!strokeWidth.empty()) { - strokeNode->width = parseLength(strokeWidth, _viewBoxWidth); - } - - std::string strokeLinecap = getAttribute(element, "stroke-linecap"); - if (!strokeLinecap.empty()) { - strokeNode->cap = LineCapFromString(strokeLinecap); - } - - std::string strokeLinejoin = getAttribute(element, "stroke-linejoin"); - if (!strokeLinejoin.empty()) { - strokeNode->join = LineJoinFromString(strokeLinejoin); - } - - std::string strokeMiterlimit = getAttribute(element, "stroke-miterlimit"); - if (!strokeMiterlimit.empty()) { - strokeNode->miterLimit = std::stof(strokeMiterlimit); - } - - std::string dashArray = getAttribute(element, "stroke-dasharray"); - if (!dashArray.empty() && dashArray != "none") { - // Parse dash array values, which may contain units (e.g., "2px,2px" or "2,2"). - // Use parseLength to handle both numeric values and values with units. - std::string token; - for (size_t i = 0; i <= dashArray.size(); i++) { - char c = (i < dashArray.size()) ? dashArray[i] : ','; - if (c == ',' || c == ' ' || c == '\t' || c == '\n' || c == '\r') { - if (!token.empty()) { - strokeNode->dashes.push_back(parseLength(token, _viewBoxWidth)); - token.clear(); - } - } else { - token += c; - } - } - } - - std::string dashOffset = getAttribute(element, "stroke-dashoffset"); - if (!dashOffset.empty()) { - strokeNode->dashOffset = parseLength(dashOffset, _viewBoxWidth); - } - - contents.push_back(std::move(strokeNode)); - } -} - -Rect SVGParserImpl::getShapeBounds(const std::shared_ptr& element) { - const auto& tag = element->name; - - if (tag == "rect") { - float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); - float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); - float width = parseLength(getAttribute(element, "width"), _viewBoxWidth); - float height = parseLength(getAttribute(element, "height"), _viewBoxHeight); - return Rect::MakeXYWH(x, y, width, height); - } - - if (tag == "circle") { - float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); - float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); - float r = parseLength(getAttribute(element, "r"), _viewBoxWidth); - return Rect::MakeXYWH(cx - r, cy - r, r * 2, r * 2); - } - - if (tag == "ellipse") { - float cx = parseLength(getAttribute(element, "cx"), _viewBoxWidth); - float cy = parseLength(getAttribute(element, "cy"), _viewBoxHeight); - float rx = parseLength(getAttribute(element, "rx"), _viewBoxWidth); - float ry = parseLength(getAttribute(element, "ry"), _viewBoxHeight); - return Rect::MakeXYWH(cx - rx, cy - ry, rx * 2, ry * 2); - } - - if (tag == "line") { - float x1 = parseLength(getAttribute(element, "x1"), _viewBoxWidth); - float y1 = parseLength(getAttribute(element, "y1"), _viewBoxHeight); - float x2 = parseLength(getAttribute(element, "x2"), _viewBoxWidth); - float y2 = parseLength(getAttribute(element, "y2"), _viewBoxHeight); - float minX = std::min(x1, x2); - float minY = std::min(y1, y2); - float maxX = std::max(x1, x2); - float maxY = std::max(y1, y2); - return Rect::MakeXYWH(minX, minY, maxX - minX, maxY - minY); - } - - if (tag == "path") { - std::string d = getAttribute(element, "d"); - if (!d.empty()) { - auto pathData = PathData::FromSVGString(d); - return pathData.getBounds(); - } - } - - // For polyline and polygon, parse points and compute bounds. - if (tag == "polyline" || tag == "polygon") { - std::string pointsStr = getAttribute(element, "points"); - if (!pointsStr.empty()) { - auto pathData = parsePoints(pointsStr, tag == "polygon"); - return pathData.getBounds(); - } - } - - return Rect::MakeXYWH(0, 0, 0, 0); -} - -Matrix SVGParserImpl::parseTransform(const std::string& value) { - Matrix result = Matrix::Identity(); - if (value.empty()) { - return result; - } - - const char* ptr = value.c_str(); - const char* end = ptr + value.length(); - - auto skipWS = [&]() { - while (ptr < end && (std::isspace(*ptr) || *ptr == ',')) { - ++ptr; - } - }; - - auto readNumber = [&]() -> float { - skipWS(); - const char* start = ptr; - if (*ptr == '-' || *ptr == '+') { - ++ptr; - } - while (ptr < end && (std::isdigit(*ptr) || *ptr == '.')) { - ++ptr; - } - if (ptr < end && (*ptr == 'e' || *ptr == 'E')) { - ++ptr; - if (*ptr == '-' || *ptr == '+') { - ++ptr; - } - while (ptr < end && std::isdigit(*ptr)) { - ++ptr; - } - } - return std::stof(std::string(start, ptr)); - }; - - while (ptr < end) { - skipWS(); - if (ptr >= end) { - break; - } - - std::string func; - while (ptr < end && std::isalpha(*ptr)) { - func += *ptr++; - } - - skipWS(); - if (*ptr != '(') { - break; - } - ++ptr; - - Matrix m = Matrix::Identity(); - - if (func == "translate") { - float tx = readNumber(); - skipWS(); - float ty = 0; - if (ptr < end && *ptr != ')') { - ty = readNumber(); - } - m = Matrix::Translate(tx, ty); - } else if (func == "scale") { - float sx = readNumber(); - skipWS(); - float sy = sx; - if (ptr < end && *ptr != ')') { - sy = readNumber(); - } - m = Matrix::Scale(sx, sy); - } else if (func == "rotate") { - float angle = readNumber(); - skipWS(); - if (ptr < end && *ptr != ')') { - float cx = readNumber(); - float cy = readNumber(); - m = Matrix::Translate(cx, cy) * Matrix::Rotate(angle) * Matrix::Translate(-cx, -cy); - } else { - m = Matrix::Rotate(angle); - } - } else if (func == "skewX") { - float angle = readNumber(); - float radians = angle * 3.14159265358979323846f / 180.0f; - m.c = std::tan(radians); - } else if (func == "skewY") { - float angle = readNumber(); - float radians = angle * 3.14159265358979323846f / 180.0f; - m.b = std::tan(radians); - } else if (func == "matrix") { - m.a = readNumber(); - m.b = readNumber(); - m.c = readNumber(); - m.d = readNumber(); - m.tx = readNumber(); - m.ty = readNumber(); - } - - skipWS(); - if (*ptr == ')') { - ++ptr; - } - - result = result * m; - } - - return result; -} - -Color SVGParserImpl::parseColor(const std::string& value) { - if (value.empty() || value == "none") { - return {0, 0, 0, 0}; - } - - if (value[0] == '#') { - uint32_t hex = 0; - if (value.length() == 4) { - // #RGB -> #RRGGBB - char r = value[1]; - char g = value[2]; - char b = value[3]; - std::string expanded = std::string() + r + r + g + g + b + b; - hex = std::stoul(expanded, nullptr, 16); - return Color::FromHex(hex); - } else if (value.length() == 7) { - hex = std::stoul(value.substr(1), nullptr, 16); - return Color::FromHex(hex); - } else if (value.length() == 9) { - hex = std::stoul(value.substr(1), nullptr, 16); - return Color::FromHex(hex, true); - } - } - - if (value.find("rgb") == 0) { - size_t start = value.find('('); - size_t end = value.find(')'); - if (start != std::string::npos && end != std::string::npos) { - std::string inner = value.substr(start + 1, end - start - 1); - std::istringstream iss(inner); - float r = 0, g = 0, b = 0, a = 1.0f; - char comma = 0; - iss >> r >> comma >> g >> comma >> b; - if (value.find("rgba") == 0) { - iss >> comma >> a; - } - return Color::FromRGBA(r / 255.0f, g / 255.0f, b / 255.0f, a); - } - } - - // SVG/CSS named colors (CSS Color 3 + CSS Color 4 rebeccapurple). - // clang-format off - static const std::unordered_map namedColors = { - {"aliceblue", 0xF0F8FF}, - {"antiquewhite", 0xFAEBD7}, - {"aqua", 0x00FFFF}, - {"aquamarine", 0x7FFFD4}, - {"azure", 0xF0FFFF}, - {"beige", 0xF5F5DC}, - {"bisque", 0xFFE4C4}, - {"black", 0x000000}, - {"blanchedalmond", 0xFFEBCD}, - {"blue", 0x0000FF}, - {"blueviolet", 0x8A2BE2}, - {"brown", 0xA52A2A}, - {"burlywood", 0xDEB887}, - {"cadetblue", 0x5F9EA0}, - {"chartreuse", 0x7FFF00}, - {"chocolate", 0xD2691E}, - {"coral", 0xFF7F50}, - {"cornflowerblue", 0x6495ED}, - {"cornsilk", 0xFFF8DC}, - {"crimson", 0xDC143C}, - {"cyan", 0x00FFFF}, - {"darkblue", 0x00008B}, - {"darkcyan", 0x008B8B}, - {"darkgoldenrod", 0xB8860B}, - {"darkgray", 0xA9A9A9}, - {"darkgreen", 0x006400}, - {"darkgrey", 0xA9A9A9}, - {"darkkhaki", 0xBDB76B}, - {"darkmagenta", 0x8B008B}, - {"darkolivegreen", 0x556B2F}, - {"darkorange", 0xFF8C00}, - {"darkorchid", 0x9932CC}, - {"darkred", 0x8B0000}, - {"darksalmon", 0xE9967A}, - {"darkseagreen", 0x8FBC8F}, - {"darkslateblue", 0x483D8B}, - {"darkslategray", 0x2F4F4F}, - {"darkslategrey", 0x2F4F4F}, - {"darkturquoise", 0x00CED1}, - {"darkviolet", 0x9400D3}, - {"deeppink", 0xFF1493}, - {"deepskyblue", 0x00BFFF}, - {"dimgray", 0x696969}, - {"dimgrey", 0x696969}, - {"dodgerblue", 0x1E90FF}, - {"firebrick", 0xB22222}, - {"floralwhite", 0xFFFAF0}, - {"forestgreen", 0x228B22}, - {"fuchsia", 0xFF00FF}, - {"gainsboro", 0xDCDCDC}, - {"ghostwhite", 0xF8F8FF}, - {"gold", 0xFFD700}, - {"goldenrod", 0xDAA520}, - {"gray", 0x808080}, - {"green", 0x008000}, - {"greenyellow", 0xADFF2F}, - {"grey", 0x808080}, - {"honeydew", 0xF0FFF0}, - {"hotpink", 0xFF69B4}, - {"indianred", 0xCD5C5C}, - {"indigo", 0x4B0082}, - {"ivory", 0xFFFFF0}, - {"khaki", 0xF0E68C}, - {"lavender", 0xE6E6FA}, - {"lavenderblush", 0xFFF0F5}, - {"lawngreen", 0x7CFC00}, - {"lemonchiffon", 0xFFFACD}, - {"lightblue", 0xADD8E6}, - {"lightcoral", 0xF08080}, - {"lightcyan", 0xE0FFFF}, - {"lightgoldenrodyellow", 0xFAFAD2}, - {"lightgray", 0xD3D3D3}, - {"lightgreen", 0x90EE90}, - {"lightgrey", 0xD3D3D3}, - {"lightpink", 0xFFB6C1}, - {"lightsalmon", 0xFFA07A}, - {"lightseagreen", 0x20B2AA}, - {"lightskyblue", 0x87CEFA}, - {"lightslategray", 0x778899}, - {"lightslategrey", 0x778899}, - {"lightsteelblue", 0xB0C4DE}, - {"lightyellow", 0xFFFFE0}, - {"lime", 0x00FF00}, - {"limegreen", 0x32CD32}, - {"linen", 0xFAF0E6}, - {"magenta", 0xFF00FF}, - {"maroon", 0x800000}, - {"mediumaquamarine", 0x66CDAA}, - {"mediumblue", 0x0000CD}, - {"mediumorchid", 0xBA55D3}, - {"mediumpurple", 0x9370DB}, - {"mediumseagreen", 0x3CB371}, - {"mediumslateblue", 0x7B68EE}, - {"mediumspringgreen", 0x00FA9A}, - {"mediumturquoise", 0x48D1CC}, - {"mediumvioletred", 0xC71585}, - {"midnightblue", 0x191970}, - {"mintcream", 0xF5FFFA}, - {"mistyrose", 0xFFE4E1}, - {"moccasin", 0xFFE4B5}, - {"navajowhite", 0xFFDEAD}, - {"navy", 0x000080}, - {"oldlace", 0xFDF5E6}, - {"olive", 0x808000}, - {"olivedrab", 0x6B8E23}, - {"orange", 0xFFA500}, - {"orangered", 0xFF4500}, - {"orchid", 0xDA70D6}, - {"palegoldenrod", 0xEEE8AA}, - {"palegreen", 0x98FB98}, - {"paleturquoise", 0xAFEEEE}, - {"palevioletred", 0xDB7093}, - {"papayawhip", 0xFFEFD5}, - {"peachpuff", 0xFFDAB9}, - {"peru", 0xCD853F}, - {"pink", 0xFFC0CB}, - {"plum", 0xDDA0DD}, - {"powderblue", 0xB0E0E6}, - {"purple", 0x800080}, - {"red", 0xFF0000}, - {"rosybrown", 0xBC8F8F}, - {"royalblue", 0x4169E1}, - {"saddlebrown", 0x8B4513}, - {"salmon", 0xFA8072}, - {"sandybrown", 0xF4A460}, - {"seagreen", 0x2E8B57}, - {"seashell", 0xFFF5EE}, - {"sienna", 0xA0522D}, - {"silver", 0xC0C0C0}, - {"skyblue", 0x87CEEB}, - {"slateblue", 0x6A5ACD}, - {"slategray", 0x708090}, - {"slategrey", 0x708090}, - {"snow", 0xFFFAFA}, - {"springgreen", 0x00FF7F}, - {"steelblue", 0x4682B4}, - {"tan", 0xD2B48C}, - {"teal", 0x008080}, - {"thistle", 0xD8BFD8}, - {"tomato", 0xFF6347}, - {"transparent", 0x000000}, - {"turquoise", 0x40E0D0}, - {"violet", 0xEE82EE}, - {"wheat", 0xF5DEB3}, - {"white", 0xFFFFFF}, - {"whitesmoke", 0xF5F5F5}, - {"yellow", 0xFFFF00}, - {"yellowgreen", 0x9ACD32}, - // CSS Color 4 addition - {"rebeccapurple", 0x663399}, - }; - // clang-format on - - auto it = namedColors.find(value); - if (it != namedColors.end()) { - auto color = Color::FromHex(it->second); - if (value == "transparent") { - color.alpha = 0; - } - return color; - } - - return {0, 0, 0, 1}; -} - -float SVGParserImpl::parseLength(const std::string& value, float containerSize) { - if (value.empty()) { - return 0; - } - - size_t idx = 0; - float num = std::stof(value, &idx); - - std::string unit = value.substr(idx); - if (unit == "%") { - return num / 100.0f * containerSize; - } - if (unit == "px" || unit.empty()) { - return num; - } - if (unit == "pt") { - return num * 1.333333f; - } - if (unit == "em" || unit == "rem") { - return num * 16.0f; // Assume 16px base font. - } - if (unit == "in") { - return num * 96.0f; - } - if (unit == "cm") { - return num * 37.795275591f; - } - if (unit == "mm") { - return num * 3.7795275591f; - } - - return num; -} - -std::vector SVGParserImpl::parseViewBox(const std::string& value) { - std::vector result; - if (value.empty()) { - return result; - } - - std::istringstream iss(value); - float num = 0; - while (iss >> num) { - result.push_back(num); - } - - return result; -} - -PathData SVGParserImpl::parsePoints(const std::string& value, bool closed) { - PathData path; - if (value.empty()) { - return path; - } - - std::vector points; - std::istringstream iss(value); - float num = 0; - while (iss >> num) { - points.push_back(num); - char c = 0; - if (iss.peek() == ',' || iss.peek() == ' ') { - iss >> c; - } - } - - if (points.size() >= 2) { - path.moveTo(points[0], points[1]); - for (size_t i = 2; i + 1 < points.size(); i += 2) { - path.lineTo(points[i], points[i + 1]); - } - if (closed) { - path.close(); - } - } - - return path; -} - -std::string SVGParserImpl::resolveUrl(const std::string& url) { - if (url.empty()) { - return ""; - } - // Handle url(#id) format. - if (url.find("url(") == 0) { - size_t start = url.find('#'); - size_t end = url.find(')'); - if (start != std::string::npos && end != std::string::npos) { - return url.substr(start + 1, end - start - 1); - } - } - // Handle #id format. - if (url[0] == '#') { - return url.substr(1); - } - return url; -} - -std::string SVGParserImpl::registerImageResource(const std::string& imageSource) { - if (imageSource.empty()) { - return ""; - } - - // Check if this image source has already been registered. - auto it = _imageSourceToId.find(imageSource); - if (it != _imageSourceToId.end()) { - return it->second; - } - - // Generate a unique image ID that doesn't conflict with existing SVG IDs. - std::string imageId = generateUniqueId("image"); - - // Create and add the image resource to the document. - auto imageNode = std::make_unique(); - imageNode->id = imageId; - imageNode->source = imageSource; - _document->resources.push_back(std::move(imageNode)); - - // Cache the mapping. - _imageSourceToId[imageSource] = imageId; - - return imageId; -} - -// Helper function to check if two VectorElement nodes are the same geometry. -static bool isSameGeometry(const Element* a, const Element* b) { - if (!a || !b || a->type() != b->type()) { - return false; - } - - switch (a->type()) { - case ElementType::Rectangle: { - auto rectA = static_cast(a); - auto rectB = static_cast(b); - return rectA->center.x == rectB->center.x && rectA->center.y == rectB->center.y && - rectA->size.width == rectB->size.width && rectA->size.height == rectB->size.height && - rectA->roundness == rectB->roundness; - } - case ElementType::Ellipse: { - auto ellipseA = static_cast(a); - auto ellipseB = static_cast(b); - return ellipseA->center.x == ellipseB->center.x && ellipseA->center.y == ellipseB->center.y && - ellipseA->size.width == ellipseB->size.width && - ellipseA->size.height == ellipseB->size.height; - } - case ElementType::Path: { - auto pathA = static_cast(a); - auto pathB = static_cast(b); - return pathA->data.toSVGString() == pathB->data.toSVGString(); - } - default: - return false; - } -} - -// Check if a layer is a simple shape layer (contains exactly one geometry and one Fill or Stroke). -static bool isSimpleShapeLayer(const Layer* layer, const Element*& outGeometry, - const Element*& outPainter) { - if (!layer || layer->contents.size() != 2) { - return false; - } - if (!layer->children.empty() || !layer->filters.empty() || !layer->styles.empty()) { - return false; - } - if (!layer->matrix.isIdentity() || layer->alpha != 1.0f) { - return false; - } - - const auto* first = layer->contents[0].get(); - const auto* second = layer->contents[1].get(); - - // Check if first is geometry and second is painter. - bool firstIsGeometry = (first->type() == ElementType::Rectangle || - first->type() == ElementType::Ellipse || first->type() == ElementType::Path); - bool secondIsPainter = - (second->type() == ElementType::Fill || second->type() == ElementType::Stroke); - - if (firstIsGeometry && secondIsPainter) { - outGeometry = first; - outPainter = second; - return true; - } - return false; -} - -void SVGParserImpl::mergeAdjacentLayers(std::vector>& layers) { - if (layers.size() < 2) { - return; - } - - std::vector> merged; - size_t i = 0; - - while (i < layers.size()) { - const Element* geomA = nullptr; - const Element* painterA = nullptr; - - if (isSimpleShapeLayer(layers[i].get(), geomA, painterA)) { - // Check if the next layer has the same geometry. - if (i + 1 < layers.size()) { - const Element* geomB = nullptr; - const Element* painterB = nullptr; - - if (isSimpleShapeLayer(layers[i + 1].get(), geomB, painterB) && - isSameGeometry(geomA, geomB)) { - // Merge: one has Fill, the other has Stroke. - bool aHasFill = (painterA->type() == ElementType::Fill); - bool bHasFill = (painterB->type() == ElementType::Fill); - - if (aHasFill != bHasFill) { - // Create merged layer. - auto mergedLayer = std::make_unique(); - - // Move geometry from first layer. - mergedLayer->contents.push_back(std::move(layers[i]->contents[0])); - - // Add Fill first, then Stroke (standard order). - if (aHasFill) { - mergedLayer->contents.push_back(std::move(layers[i]->contents[1])); - mergedLayer->contents.push_back(std::move(layers[i + 1]->contents[1])); - } else { - mergedLayer->contents.push_back(std::move(layers[i + 1]->contents[1])); - mergedLayer->contents.push_back(std::move(layers[i]->contents[1])); - } - - merged.push_back(std::move(mergedLayer)); - i += 2; // Skip both layers. - continue; - } - } - } - } - - // No merge, keep the layer as is. - merged.push_back(std::move(layers[i])); - i++; - } - - layers = std::move(merged); -} - -std::unique_ptr SVGParserImpl::convertMaskElement( - const std::shared_ptr& maskElement, const InheritedStyle& parentStyle) { - auto maskLayer = std::make_unique(); - maskLayer->id = getAttribute(maskElement, "id"); - maskLayer->name = maskLayer->id; - maskLayer->visible = false; - - // Parse mask contents. - auto child = maskElement->getFirstChild(); - while (child) { - if (child->name == "rect" || child->name == "circle" || child->name == "ellipse" || - child->name == "path" || child->name == "polygon" || child->name == "polyline") { - InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); - convertChildren(child, maskLayer->contents, inheritedStyle); - } else if (child->name == "g") { - // Handle group inside mask. - InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); - auto groupChild = child->getFirstChild(); - while (groupChild) { - convertChildren(groupChild, maskLayer->contents, inheritedStyle); - groupChild = groupChild->getNextSibling(); - } - } - child = child->getNextSibling(); - } - - return maskLayer; -} - -void SVGParserImpl::convertFilterElement( - const std::shared_ptr& filterElement, - std::vector>& filters) { - // Parse filter children to find effect elements. - auto child = filterElement->getFirstChild(); - while (child) { - if (child->name == "feGaussianBlur") { - auto blurFilter = std::make_unique(); - std::string stdDeviation = getAttribute(child, "stdDeviation", "0"); - // stdDeviation can be one value (both X and Y) or two values (X Y). - std::istringstream iss(stdDeviation); - float devX = 0, devY = 0; - iss >> devX; - if (!(iss >> devY)) { - devY = devX; - } - blurFilter->blurrinessX = devX; - blurFilter->blurrinessY = devY; - filters.push_back(std::move(blurFilter)); - } - child = child->getNextSibling(); - } -} - -void SVGParserImpl::collectAllIds(const std::shared_ptr& node) { - if (!node) { - return; - } - - // Collect id from this node. - auto [found, id] = node->findAttribute("id"); - if (found && !id.empty()) { - _existingIds.insert(id); - } - - // Recursively collect from children. - auto child = node->getFirstChild(); - while (child) { - collectAllIds(child); - child = child->getNextSibling(); - } -} - -std::string SVGParserImpl::generateUniqueId(const std::string& prefix) { - std::string id; - do { - id = "_" + prefix + std::to_string(_nextGeneratedId++); - } while (_existingIds.count(id) > 0); - _existingIds.insert(id); - return id; -} - -void SVGParserImpl::parseCustomData(const std::shared_ptr& element, Layer* layer) { - if (!element || !layer) { - return; - } - - // Iterate through all attributes and find data-* ones. - for (const auto& attr : element->attributes) { - if (attr.name.length() > 5 && attr.name.substr(0, 5) == "data-") { - // Remove "data-" prefix and store in customData. - std::string key = attr.name.substr(5); - layer->customData[key] = attr.value; - } - } -} - -} // namespace pagx From f83d4d93a654e2a3a8da014250ab5fddadcb2984 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 18:27:23 +0800 Subject: [PATCH 099/678] Add named colors and CSS Color Level 4 support to Color::Parse. --- pagx/include/pagx/model/types/Color.h | 98 +++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/pagx/include/pagx/model/types/Color.h b/pagx/include/pagx/model/types/Color.h index 04eaf24c27..7cf0274110 100644 --- a/pagx/include/pagx/model/types/Color.h +++ b/pagx/include/pagx/model/types/Color.h @@ -24,6 +24,7 @@ #include #include #include +#include #include namespace pagx { @@ -82,12 +83,46 @@ struct Color { * Parses a color string. Supports: * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" + * - Named colors: "white", "black", "red", etc. + * - CSS Color Level 4: "color(display-p3 r g b)" with colorspace conversion * Returns black if parsing fails. */ static Color Parse(const std::string& str) { if (str.empty()) { return {}; } + // Named colors (CSS Level 1-4 basic colors). + static const std::unordered_map namedColors = { + {"black", 0x000000}, {"white", 0xFFFFFF}, + {"red", 0xFF0000}, {"green", 0x008000}, + {"blue", 0x0000FF}, {"yellow", 0xFFFF00}, + {"cyan", 0x00FFFF}, {"magenta", 0xFF00FF}, + {"gray", 0x808080}, {"grey", 0x808080}, + {"silver", 0xC0C0C0}, {"maroon", 0x800000}, + {"olive", 0x808000}, {"lime", 0x00FF00}, + {"aqua", 0x00FFFF}, {"teal", 0x008080}, + {"navy", 0x000080}, {"fuchsia", 0xFF00FF}, + {"purple", 0x800080}, {"orange", 0xFFA500}, + {"pink", 0xFFC0CB}, {"brown", 0xA52A2A}, + {"coral", 0xFF7F50}, {"crimson", 0xDC143C}, + {"darkblue", 0x00008B}, {"darkgray", 0xA9A9A9}, + {"darkgreen", 0x006400}, {"darkred", 0x8B0000}, + {"gold", 0xFFD700}, {"indigo", 0x4B0082}, + {"ivory", 0xFFFFF0}, {"khaki", 0xF0E68C}, + {"lavender", 0xE6E6FA}, {"lightblue", 0xADD8E6}, + {"lightgray", 0xD3D3D3}, {"lightgreen", 0x90EE90}, + {"lightyellow", 0xFFFFE0}, {"none", 0x000000}, + {"transparent", 0x000000}, + }; + auto it = namedColors.find(str); + if (it != namedColors.end()) { + if (str == "transparent" || str == "none") { + auto color = Color::FromHex(0x000000); + color.alpha = 0; + return color; + } + return Color::FromHex(it->second); + } if (str[0] == '#') { auto hex = str.substr(1); if (hex.size() == 3) { @@ -137,6 +172,69 @@ struct Color { } } } + // CSS Color Level 4: color(colorspace r g b) or color(colorspace r g b / a) + if (str.substr(0, 6) == "color(") { + auto start = str.find('('); + auto end = str.find(')'); + if (start != std::string::npos && end != std::string::npos) { + auto inner = str.substr(start + 1, end - start - 1); + inner.erase(0, inner.find_first_not_of(" \t")); + inner.erase(inner.find_last_not_of(" \t") + 1); + + std::istringstream iss(inner); + std::string colorspace = {}; + iss >> colorspace; + + std::vector components = {}; + std::string token = {}; + float alpha = 1.0f; + bool foundSlash = false; + + while (iss >> token) { + if (token == "/") { + foundSlash = true; + continue; + } + float value = std::stof(token); + if (foundSlash) { + alpha = value; + } else { + components.push_back(value); + } + } + + if (components.size() >= 3) { + float r = components[0]; + float g = components[1]; + float b = components[2]; + + // Convert from wide gamut colorspace to sRGB (approximate clipping). + if (colorspace == "display-p3") { + float sR = 1.2249f * r - 0.2247f * g - 0.0002f * b; + float sG = -0.0420f * r + 1.0419f * g + 0.0001f * b; + float sB = -0.0197f * r - 0.0786f * g + 1.0983f * b; + r = std::max(0.0f, std::min(1.0f, sR)); + g = std::max(0.0f, std::min(1.0f, sG)); + b = std::max(0.0f, std::min(1.0f, sB)); + } else if (colorspace == "a98-rgb") { + float sR = 1.3982f * r - 0.3982f * g + 0.0f * b; + float sG = 0.0f * r + 1.0f * g + 0.0f * b; + float sB = 0.0f * r - 0.0429f * g + 1.0429f * b; + r = std::max(0.0f, std::min(1.0f, sR)); + g = std::max(0.0f, std::min(1.0f, sG)); + b = std::max(0.0f, std::min(1.0f, sB)); + } else if (colorspace == "rec2020") { + float sR = 1.6605f * r - 0.5877f * g - 0.0728f * b; + float sG = -0.1246f * r + 1.1330f * g - 0.0084f * b; + float sB = -0.0182f * r - 0.1006f * g + 1.1188f * b; + r = std::max(0.0f, std::min(1.0f, sR)); + g = std::max(0.0f, std::min(1.0f, sG)); + b = std::max(0.0f, std::min(1.0f, sB)); + } + return Color::FromRGBA(r, g, b, alpha); + } + } + } return {}; } From 2d6466b3bd70ef502adf43a31e7df04c1491cd1c Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 18:27:28 +0800 Subject: [PATCH 100/678] Change ID reference syntax from #id to @id Adopt Android-style @ prefix for resource references to clearly distinguish from HEX color values: - Color values: #FF0000, #RRGGBB, #RRGGBBAA - ID references: @gradientId, @maskShape, @img1 This eliminates ambiguity between color values and resource references. Updated all documentation, examples, and attribute descriptions. --- pagx/docs/pagx_spec.md | 54 +++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index d647ee42c2..1e729cc0a5 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -96,7 +96,7 @@ PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视 | `bool` | 布尔值 | `true`、`false` | | `string` | 字符串 | `"Arial"`、`"myLayer"` | | `enum` | 枚举值 | `normal`、`multiply` | -| `idref` | ID 引用 | `#gradientId`、`#maskLayer` | +| `idref` | ID 引用 | `@gradientId`、`@maskLayer` | ### 2.5 点(Point) @@ -155,7 +155,7 @@ PAGX 支持多种颜色格式: | RGB | `rgb(255,0,0)`、`rgba(255,0,0,0.5)` | RGB 带可选透明度 | | HSL | `hsl(0,100%,50%)`、`hsla(0,100%,50%,0.5)` | HSL 带可选透明度 | | 色域 | `color(display-p3 1 0 0)` | 广色域颜色 | -| 引用 | `#resourceId` | 引用 Resources 中定义的颜色源 | +| 引用 | `@resourceId` | 引用 Resources 中定义的颜色源 | ### 2.9 路径数据语法(Path Data Syntax) @@ -234,7 +234,7 @@ PAGX 使用标准的 2D 笛卡尔坐标系: ### 3.3 资源区(Resources) -`` 定义可复用的资源,包括图片、路径数据、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `#id` 形式引用。 +`` 定义可复用的资源,包括图片、路径数据、颜色源和合成。资源通过 `id` 属性标识,在文档其他位置通过 `@id` 形式引用。 **元素位置**:Resources 元素可放置在根元素内的任意位置,对位置没有限制。解析器必须支持元素引用在文档后面定义的资源或图层(即前向引用)。 @@ -247,7 +247,7 @@ PAGX 使用标准的 2D 笛卡尔坐标系: - + ``` @@ -283,7 +283,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 颜色源定义可用于填充和描边的颜色,支持两种使用方式: -1. **共享定义**:在 `` 中预定义,通过 `#id` 引用。适用于**被多处引用**的颜色源。 +1. **共享定义**:在 `` 中预定义,通过 `@id` 引用。适用于**被多处引用**的颜色源。 2. **内联定义**:直接嵌套在 `` 或 `` 元素内部。适用于**仅使用一次**的颜色源,更简洁。 ##### 纯色(SolidColor) @@ -398,12 +398,12 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 图片图案使用图片作为颜色源。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `image` | idref | (必填) | 图片引用 "#id" | +| `image` | idref | (必填) | 图片引用 "@id" | | `tileModeX` | TileMode | clamp | X 方向平铺模式 | | `tileModeY` | TileMode | clamp | Y 方向平铺模式 | | `sampling` | SamplingMode | linear | 采样模式 | @@ -435,7 +435,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 - + ``` @@ -507,7 +507,7 @@ PAGX 文档采用层级结构组织内容: - + ``` #### 子元素 @@ -541,9 +541,9 @@ Layer 的子元素按类型自动归类为四个集合: | `passThroughBackground` | bool | true | 是否允许背景透传给子图层 | | `excludeChildEffectsInLayerStyle` | bool | false | 图层样式是否排除子图层效果 | | `scrollRect` | string | - | 滚动裁剪区域 "x,y,w,h" | -| `mask` | idref | - | 遮罩图层引用 "#id" | +| `mask` | idref | - | 遮罩图层引用 "@id" | | `maskType` | MaskType | alpha | 遮罩类型 | -| `composition` | idref | - | 合成引用 "#id" | +| `composition` | idref | - | 合成引用 "@id" | **变换属性优先级**:`x`/`y`、`matrix`、`matrix3D` 三者存在覆盖关系: - 仅设置 `x`/`y`:使用 `x`/`y` 作为平移 @@ -786,7 +786,7 @@ Layer 的子元素按类型自动归类为四个集合: - + @@ -984,12 +984,12 @@ y = center.y + outerRadius * sin(angle) - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `data` | string/idref | (必填) | SVG 路径数据或 PathData 资源引用 "#id" | +| `data` | string/idref | (必填) | SVG 路径数据或 PathData 资源引用 "@id" | | `reversed` | bool | false | 反转路径方向 | #### 5.2.5 文本片段(TextSpan) @@ -1034,7 +1034,7 @@ y = center.y + outerRadius * sin(angle) - + @@ -1046,7 +1046,7 @@ y = center.y + outerRadius * sin(angle) - + ``` @@ -1412,12 +1412,12 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 将文本沿指定路径排列。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `path` | idref | (必填) | PathData 资源引用 "#id" | +| `path` | idref | (必填) | PathData 资源引用 "@id" | | `align` | TextPathAlign | start | 对齐模式(见下方) | | `firstMargin` | float | 0 | 起始边距 | | `lastMargin` | float | 0 | 结束边距 | @@ -1684,7 +1684,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ```xml - + ``` @@ -1707,7 +1707,7 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: - + ``` @@ -1802,7 +1802,7 @@ Layer / Group - + @@ -1818,8 +1818,8 @@ Layer / Group - - + + @@ -1827,7 +1827,7 @@ Layer / Group - + @@ -1840,7 +1840,7 @@ Layer / Group - @@ -1860,7 +1860,7 @@ Layer / Group - + @@ -1946,7 +1946,7 @@ Layer / Group - + ``` From 0d950dda9d7aa0778b4a920cfce4910e8c892524 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 18:29:47 +0800 Subject: [PATCH 101/678] =?UTF-8?q?Standardize=20cross-reference=20format?= =?UTF-8?q?=20to=20=EF=BC=88=E8=A7=81=20x.x=20=E8=8A=82=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pagx/docs/pagx_spec.md | 12 ++++++------ pagx/src/PAGXXMLParser.cpp | 32 +++++++++++++++++++++++++++++--- pagx/src/PAGXXMLWriter.cpp | 31 ++++++++++--------------------- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 1e729cc0a5..07ffa978a2 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -638,7 +638,7 @@ Layer 的子元素按类型自动归类为四个集合: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `blendMode` | BlendMode | normal | 混合模式(见 4.1) | +| `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | #### 4.2.1 投影阴影(DropShadowStyle) @@ -745,7 +745,7 @@ Layer 的子元素按类型自动归类为四个集合: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `color` | color | (必填) | 混合颜色 | -| `blendMode` | BlendMode | normal | 混合模式(见 4.1) | +| `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | #### 4.3.5 颜色矩阵滤镜(ColorMatrixFilter) @@ -1054,9 +1054,9 @@ y = center.y + outerRadius * sin(angle) |------|------|--------|------| | `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | | `alpha` | float | 1 | 透明度 0~1 | -| `blendMode` | BlendMode | normal | 混合模式(见 4.1) | +| `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | | `fillRule` | FillRule | winding | 填充规则(见下方) | -| `placement` | LayerPlacement | background | 绘制位置(见 5.3.3) | +| `placement` | LayerPlacement | background | 绘制位置(见 5.3.3 节) | **FillRule(填充规则)**: @@ -1095,14 +1095,14 @@ y = center.y + outerRadius * sin(angle) | `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | | `width` | float | 1 | 描边宽度 | | `alpha` | float | 1 | 透明度 0~1 | -| `blendMode` | BlendMode | normal | 混合模式(见 4.1) | +| `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | | `cap` | LineCap | butt | 线帽样式(见下方) | | `join` | LineJoin | miter | 线连接样式(见下方) | | `miterLimit` | float | 4 | 斜接限制 | | `dashes` | string | - | 虚线模式 "d1,d2,..." | | `dashOffset` | float | 0 | 虚线偏移 | | `align` | StrokeAlign | center | 描边对齐(见下方) | -| `placement` | LayerPlacement | background | 绘制位置(见 5.3.3) | +| `placement` | LayerPlacement | background | 绘制位置(见 5.3.3 节) | **LineCap(线帽样式)**: diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 77bc7bce8c..4542d37036 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -360,17 +360,43 @@ std::unique_ptr PAGXXMLParser::parseLayer(const XMLNode* node) { } for (const auto& child : node->children) { + // Legacy format: support container nodes for backward compatibility. if (child->tag == "contents") { parseContents(child.get(), layer.get()); - } else if (child->tag == "styles") { + continue; + } + if (child->tag == "styles") { parseStyles(child.get(), layer.get()); - } else if (child->tag == "filters") { + continue; + } + if (child->tag == "filters") { parseFilters(child.get(), layer.get()); - } else if (child->tag == "Layer") { + continue; + } + // New format: direct child elements without container nodes. + if (child->tag == "Layer") { auto childLayer = parseLayer(child.get()); if (childLayer) { layer->children.push_back(std::move(childLayer)); } + continue; + } + // Try to parse as VectorElement. + auto element = parseElement(child.get()); + if (element) { + layer->contents.push_back(std::move(element)); + continue; + } + // Try to parse as LayerStyle. + auto style = parseLayerStyle(child.get()); + if (style) { + layer->styles.push_back(std::move(style)); + continue; + } + // Try to parse as LayerFilter. + auto filter = parseLayerFilter(child.get()); + if (filter) { + layer->filters.push_back(std::move(filter)); } } diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index 4f28483637..c8d0e12dc2 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -1240,33 +1240,22 @@ static void writeLayer(XMLBuilder& xml, const Layer* node, const ResourceContext xml.closeElementStart(); - if (!node->contents.empty()) { - xml.openElement("contents"); - xml.closeElementStart(); - for (const auto& element : node->contents) { - writeVectorElement(xml, element.get(), ctx); - } - xml.closeElement(); + // Write VectorElement (contents) directly without container node. + for (const auto& element : node->contents) { + writeVectorElement(xml, element.get(), ctx); } - if (!node->styles.empty()) { - xml.openElement("styles"); - xml.closeElementStart(); - for (const auto& style : node->styles) { - writeLayerStyle(xml, style.get()); - } - xml.closeElement(); + // Write LayerStyle (styles) directly without container node. + for (const auto& style : node->styles) { + writeLayerStyle(xml, style.get()); } - if (!node->filters.empty()) { - xml.openElement("filters"); - xml.closeElementStart(); - for (const auto& filter : node->filters) { - writeLayerFilter(xml, filter.get()); - } - xml.closeElement(); + // Write LayerFilter (filters) directly without container node. + for (const auto& filter : node->filters) { + writeLayerFilter(xml, filter.get()); } + // Write child Layers. for (const auto& child : node->children) { writeLayer(xml, child.get(), ctx); } From d3cc861342bdf89ef675583ac94a044fff8580ed Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 19:54:45 +0800 Subject: [PATCH 102/678] SVGImporter generates TextLayout for text-anchor alignment and LayerBuilder handles TextLayout to adjust TextSpan position. --- pagx/src/svg/SVGImporter.cpp | 53 +++++++++++--- pagx/src/svg/SVGParserInternal.h | 4 + pagx/src/tgfx/LayerBuilder.cpp | 122 ++++++++++++++++++++++++++++++- 3 files changed, 166 insertions(+), 13 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index d5b86d5c5c..a502630e28 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -514,14 +514,9 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); - // Parse text-anchor attribute for x position adjustment. - // SVG text-anchor affects horizontal alignment: start (default), middle, end. - // Since PAGX TextSpan doesn't have text-anchor, we'll note this for future - // position adjustment after text shaping (requires knowing text width). + // Parse text-anchor attribute for horizontal alignment. + // SVG values: start (default), middle, end. std::string anchor = getAttribute(element, "text-anchor"); - // Note: text-anchor adjustment would require knowing the text width after shaping. - // For now, we store the x position as-is. A full implementation would need to - // adjust x based on anchor after calculating the text bounds. // Get text content from child text nodes. std::string textContent; @@ -550,6 +545,23 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr } group->elements.push_back(std::move(textSpan)); + + // Add TextLayout modifier if text-anchor requires alignment. + // SVG text-anchor maps to PAGX TextLayout.textAlign: + // start -> Left (default, no TextLayout needed) + // middle -> Center + // end -> Right + if (!anchor.empty() && anchor != "start") { + auto textLayout = std::make_unique(); + textLayout->width = 0; // auto-width + textLayout->height = 0; // auto-height + if (anchor == "middle") { + textLayout->textAlign = TextAlign::Center; + } else if (anchor == "end") { + textLayout->textAlign = TextAlign::Right; + } + group->elements.push_back(std::move(textLayout)); + } } addFillStroke(element, group->elements, inheritedStyle); @@ -903,8 +915,9 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, fillNode->alpha = std::stof(fillOpacity); } - // Store the original color string for later parsing in LayerBuilder. - fillNode->color = fill; + // Convert color to hex format for PAGX compatibility. + // Named colors (e.g., "black", "red") are converted to their hex values. + fillNode->color = colorToHex(fill); // Determine effective fill-rule. std::string fillRule = getAttribute(element, "fill-rule"); @@ -952,8 +965,9 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, strokeNode->alpha = std::stof(strokeOpacity); } - // Store the original color string for later parsing in LayerBuilder. - strokeNode->color = stroke; + // Convert color to hex format for PAGX compatibility. + // Named colors (e.g., "black", "red") are converted to their hex values. + strokeNode->color = colorToHex(stroke); } std::string strokeWidth = getAttribute(element, "stroke-width"); @@ -1378,6 +1392,23 @@ Color SVGParserImpl::parseColor(const std::string& value) { return {0, 0, 0, 1}; } +std::string SVGParserImpl::colorToHex(const std::string& value) { + if (value.empty() || value == "none") { + return value; + } + // Already a hex color, return as-is. + if (value[0] == '#') { + return value; + } + // url() references should be returned as-is. + if (value.find("url(") == 0) { + return value; + } + // Parse the color (handles named colors, rgb, rgba, etc.) and convert to hex. + Color color = parseColor(value); + return color.toHexString(color.alpha < 1.0f); +} + float SVGParserImpl::parseLength(const std::string& value, float containerSize) { if (value.empty()) { return 0; diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 0e5d5e0fb5..04ea460519 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -36,6 +36,7 @@ #include "pagx/model/RadialGradient.h" #include "pagx/model/Rectangle.h" #include "pagx/model/Stroke.h" +#include "pagx/model/TextLayout.h" #include "pagx/model/TextSpan.h" #include "pagx/SVGImporter.h" #include "xml/XMLDOM.h" @@ -111,6 +112,9 @@ class SVGParserImpl { Matrix parseTransform(const std::string& value); Color parseColor(const std::string& value); + // Convert a color string (hex, rgb, rgba, or named color) to hex format. + // Named colors are converted to their hex equivalents for PAGX compatibility. + std::string colorToHex(const std::string& value); float parseLength(const std::string& value, float containerSize); std::vector parseViewBox(const std::string& value); PathData parsePoints(const std::string& value, bool closed); diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 301f75eacb..cdfde8d841 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -45,6 +45,7 @@ #include "pagx/model/RoundCorner.h" #include "pagx/model/SolidColor.h" #include "pagx/model/Stroke.h" +#include "pagx/model/TextLayout.h" #include "pagx/model/TextSpan.h" #include "pagx/model/TrimPath.h" #include "pagx/SVGImporter.h" @@ -370,6 +371,9 @@ class LayerBuilderImpl { return convertRepeater(static_cast(node)); case ElementType::Group: return convertGroup(static_cast(node)); + case ElementType::TextLayout: + // TextLayout is handled in convertGroup, not converted directly. + return nullptr; default: return nullptr; } @@ -433,7 +437,6 @@ class LayerBuilderImpl { typeface = _options.fallbackTypefaces[0]; } - float xOffset = 0; if (!node->text.empty()) { std::shared_ptr textBlob = nullptr; // Use TextShaper for fallback support (including emoji). @@ -446,7 +449,7 @@ class LayerBuilderImpl { textSpan->setTextBlob(textBlob); } - textSpan->setPosition(tgfx::Point::Make(node->x + xOffset, node->y)); + textSpan->setPosition(tgfx::Point::Make(node->x, node->y)); return textSpan; } @@ -659,7 +662,122 @@ class LayerBuilderImpl { auto group = std::make_shared(); std::vector> elements; + // Check if group contains TextLayout modifier. + const TextLayout* textLayout = nullptr; + for (const auto& element : node->elements) { + if (element->type() == ElementType::TextLayout) { + textLayout = static_cast(element.get()); + break; + } + } + + // Collect TextSpan info for layout calculation if TextLayout is present. + struct TextSpanInfo { + const pagx::TextSpan* span = nullptr; + std::shared_ptr blob = nullptr; + tgfx::Rect bounds = {}; + }; + std::vector textSpans; + + if (textLayout != nullptr) { + for (const auto& element : node->elements) { + if (element->type() == ElementType::TextSpan) { + auto span = static_cast(element.get()); + TextSpanInfo info; + info.span = span; + + // Create TextBlob to measure bounds. + std::shared_ptr typeface = nullptr; + if (!span->font.empty() && !_options.fallbackTypefaces.empty()) { + for (const auto& tf : _options.fallbackTypefaces) { + if (tf && tf->fontFamily() == span->font) { + typeface = tf; + break; + } + } + } + if (!typeface && !_options.fallbackTypefaces.empty()) { + typeface = _options.fallbackTypefaces[0]; + } + + if (!span->text.empty()) { + if (_textShaper) { + info.blob = _textShaper->shape(span->text, typeface, span->fontSize); + } else if (typeface) { + auto font = tgfx::Font(typeface, span->fontSize); + info.blob = tgfx::TextBlob::MakeFrom(span->text, font); + } + if (info.blob) { + info.bounds = info.blob->getBounds(); + } + } + textSpans.push_back(info); + } + } + } + for (const auto& element : node->elements) { + // Skip TextLayout modifier, it's handled by adjusting TextSpan positions. + if (element->type() == ElementType::TextLayout) { + continue; + } + + // Handle TextSpan with layout adjustments. + if (element->type() == ElementType::TextSpan && textLayout != nullptr) { + auto span = static_cast(element.get()); + auto tgfxTextSpan = std::make_shared(); + + // Find the matching TextSpanInfo. + TextSpanInfo* info = nullptr; + for (auto& ts : textSpans) { + if (ts.span == span) { + info = &ts; + break; + } + } + + if (info != nullptr && info->blob) { + tgfxTextSpan->setTextBlob(info->blob); + + // Calculate x offset based on textAlign. + // TextBlob bounds are relative to the drawing origin (0, 0). + // When position is (x, y), text left edge is at x + bounds.left, + // and text right edge is at x + bounds.right. + // + // For textAlign: + // Left: text left edge at x → xOffset = -bounds.left + // Center: text center at x → xOffset = -(bounds.left + bounds.right) / 2 + // Right: text right edge at x → xOffset = -bounds.right + float xOffset = 0; + switch (textLayout->textAlign) { + case TextAlign::Left: + // x is the left edge of text. + xOffset = -info->bounds.left; + break; + case TextAlign::Center: + // x is the center of text. + xOffset = -(info->bounds.left + info->bounds.right) / 2.0f; + break; + case TextAlign::Right: + // x is the right edge of text. + xOffset = -info->bounds.right; + break; + case TextAlign::Justify: + // Justify requires more complex handling, treat as left for now. + xOffset = -info->bounds.left; + break; + } + + tgfxTextSpan->setPosition(tgfx::Point::Make(span->x + xOffset, span->y)); + } else { + // No blob, use original position. + tgfxTextSpan->setPosition(tgfx::Point::Make(span->x, span->y)); + } + + elements.push_back(tgfxTextSpan); + continue; + } + auto tgfxElement = convertVectorElement(element.get()); if (tgfxElement) { elements.push_back(tgfxElement); From f768a60aa0bb2a0023304d44f63b3afa2f0031fb Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 20:05:41 +0800 Subject: [PATCH 103/678] Fix TextLayout text alignment by using getTightBounds and correct offset calculation. --- pagx/src/tgfx/LayerBuilder.cpp | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index cdfde8d841..d119a52556 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -708,7 +708,7 @@ class LayerBuilderImpl { info.blob = tgfx::TextBlob::MakeFrom(span->text, font); } if (info.blob) { - info.bounds = info.blob->getBounds(); + info.bounds = info.blob->getTightBounds(); } } textSpans.push_back(info); @@ -740,31 +740,22 @@ class LayerBuilderImpl { tgfxTextSpan->setTextBlob(info->blob); // Calculate x offset based on textAlign. - // TextBlob bounds are relative to the drawing origin (0, 0). - // When position is (x, y), text left edge is at x + bounds.left, - // and text right edge is at x + bounds.right. - // - // For textAlign: - // Left: text left edge at x → xOffset = -bounds.left - // Center: text center at x → xOffset = -(bounds.left + bounds.right) / 2 - // Right: text right edge at x → xOffset = -bounds.right + // This follows tgfx SVG text-anchor handling: xOffset = alignmentFactor * width + // where alignmentFactor is: Left=0, Center=-0.5, Right=-1.0 float xOffset = 0; + float textWidth = info->bounds.width(); switch (textLayout->textAlign) { case TextAlign::Left: - // x is the left edge of text. - xOffset = -info->bounds.left; + // No offset needed. break; case TextAlign::Center: - // x is the center of text. - xOffset = -(info->bounds.left + info->bounds.right) / 2.0f; + xOffset = -0.5f * textWidth; break; case TextAlign::Right: - // x is the right edge of text. - xOffset = -info->bounds.right; + xOffset = -textWidth; break; case TextAlign::Justify: // Justify requires more complex handling, treat as left for now. - xOffset = -info->bounds.left; break; } From 240bffbd7365b2d44a3ac57275d41307aec0cb5f Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 21:37:47 +0800 Subject: [PATCH 104/678] Add CSS Color Level 4 wide gamut color support to SVGImporter and fix Display P3 color rendering. --- pagx/src/svg/SVGImporter.cpp | 196 ++++++++++++++++++++++++++++----- pagx/src/tgfx/LayerBuilder.cpp | 35 ++++-- 2 files changed, 195 insertions(+), 36 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index a502630e28..23599ced67 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -21,7 +21,9 @@ #include #include #include +#include "PAGXEnumUtils.h" #include "pagx/model/Document.h" +#include "pagx/model/SolidColor.h" #include "SVGParserInternal.h" #include "xml/XMLDOM.h" @@ -884,22 +886,23 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, if (fill.empty()) { // No fill specified anywhere - use SVG default black. auto fillNode = std::make_unique(); - fillNode->color = "#000000"; + auto solidColor = std::make_unique(); + solidColor->color = {0, 0, 0, 1, ColorSpace::SRGB}; + fillNode->color = std::move(solidColor); contents.push_back(std::move(fillNode)); } else if (fill.find("url(") == 0) { auto fillNode = std::make_unique(); std::string refId = resolveUrl(fill); // Try to inline the gradient or pattern. - // Don't set fillNode->color when using colorSource. auto it = _defs.find(refId); if (it != _defs.end()) { if (it->second->name == "linearGradient") { - fillNode->colorSource = convertLinearGradient(it->second, shapeBounds); + fillNode->color = convertLinearGradient(it->second, shapeBounds); } else if (it->second->name == "radialGradient") { - fillNode->colorSource = convertRadialGradient(it->second, shapeBounds); + fillNode->color = convertRadialGradient(it->second, shapeBounds); } else if (it->second->name == "pattern") { - fillNode->colorSource = convertPattern(it->second, shapeBounds); + fillNode->color = convertPattern(it->second, shapeBounds); } } contents.push_back(std::move(fillNode)); @@ -915,9 +918,11 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, fillNode->alpha = std::stof(fillOpacity); } - // Convert color to hex format for PAGX compatibility. - // Named colors (e.g., "black", "red") are converted to their hex values. - fillNode->color = colorToHex(fill); + // Convert color to SolidColor for PAGX compatibility. + Color parsedColor = parseColor(fill); + auto solidColor = std::make_unique(); + solidColor->color = parsedColor; + fillNode->color = std::move(solidColor); // Determine effective fill-rule. std::string fillRule = getAttribute(element, "fill-rule"); @@ -944,15 +949,14 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, if (stroke.find("url(") == 0) { std::string refId = resolveUrl(stroke); - // Don't set strokeNode->color when using colorSource. auto it = _defs.find(refId); if (it != _defs.end()) { if (it->second->name == "linearGradient") { - strokeNode->colorSource = convertLinearGradient(it->second, shapeBounds); + strokeNode->color = convertLinearGradient(it->second, shapeBounds); } else if (it->second->name == "radialGradient") { - strokeNode->colorSource = convertRadialGradient(it->second, shapeBounds); + strokeNode->color = convertRadialGradient(it->second, shapeBounds); } else if (it->second->name == "pattern") { - strokeNode->colorSource = convertPattern(it->second, shapeBounds); + strokeNode->color = convertPattern(it->second, shapeBounds); } } } else { @@ -965,9 +969,11 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, strokeNode->alpha = std::stof(strokeOpacity); } - // Convert color to hex format for PAGX compatibility. - // Named colors (e.g., "black", "red") are converted to their hex values. - strokeNode->color = colorToHex(stroke); + // Convert color to SolidColor for PAGX compatibility. + Color parsedColor = parseColor(stroke); + auto solidColor = std::make_unique(); + solidColor->color = parsedColor; + strokeNode->color = std::move(solidColor); } std::string strokeWidth = getAttribute(element, "stroke-width"); @@ -1186,11 +1192,13 @@ Matrix SVGParserImpl::parseTransform(const std::string& value) { Color SVGParserImpl::parseColor(const std::string& value) { if (value.empty() || value == "none") { - return {0, 0, 0, 0}; + return {0, 0, 0, 0, ColorSpace::SRGB}; } if (value[0] == '#') { uint32_t hex = 0; + Color color = {}; + color.colorSpace = ColorSpace::SRGB; if (value.length() == 4) { // #RGB -> #RRGGBB char r = value[1]; @@ -1198,13 +1206,25 @@ Color SVGParserImpl::parseColor(const std::string& value) { char b = value[3]; std::string expanded = std::string() + r + r + g + g + b + b; hex = std::stoul(expanded, nullptr, 16); - return Color::FromHex(hex); + color.red = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.blue = static_cast(hex & 0xFF) / 255.0f; + color.alpha = 1.0f; + return color; } else if (value.length() == 7) { hex = std::stoul(value.substr(1), nullptr, 16); - return Color::FromHex(hex); + color.red = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.blue = static_cast(hex & 0xFF) / 255.0f; + color.alpha = 1.0f; + return color; } else if (value.length() == 9) { hex = std::stoul(value.substr(1), nullptr, 16); - return Color::FromHex(hex, true); + color.red = static_cast((hex >> 24) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.blue = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.alpha = static_cast(hex & 0xFF) / 255.0f; + return color; } } @@ -1220,7 +1240,68 @@ Color SVGParserImpl::parseColor(const std::string& value) { if (value.find("rgba") == 0) { iss >> comma >> a; } - return Color::FromRGBA(r / 255.0f, g / 255.0f, b / 255.0f, a); + return {r / 255.0f, g / 255.0f, b / 255.0f, a, ColorSpace::SRGB}; + } + } + + // CSS Color Level 4: color(display-p3 r g b) or color(display-p3 r g b / a) + if (value.find("color(") == 0) { + auto start = value.find('('); + auto end = value.find(')'); + if (start != std::string::npos && end != std::string::npos) { + auto inner = value.substr(start + 1, end - start - 1); + // Trim leading whitespace + inner.erase(0, inner.find_first_not_of(" \t")); + + // Detect color space identifier + ColorSpace colorSpace = ColorSpace::SRGB; + if (inner.find("display-p3") == 0) { + colorSpace = ColorSpace::DisplayP3; + inner = inner.substr(10); // Skip "display-p3" + } else if (inner.find("a98-rgb") == 0) { + // Adobe RGB 1998 - convert to sRGB approximation + colorSpace = ColorSpace::SRGB; + inner = inner.substr(7); // Skip "a98-rgb" + } else if (inner.find("rec2020") == 0) { + // Rec.2020 - convert to sRGB approximation + colorSpace = ColorSpace::SRGB; + inner = inner.substr(7); // Skip "rec2020" + } else if (inner.find("srgb") == 0) { + colorSpace = ColorSpace::SRGB; + inner = inner.substr(4); // Skip "srgb" + } + + // Trim whitespace after color space name + inner.erase(0, inner.find_first_not_of(" \t")); + inner.erase(inner.find_last_not_of(" \t") + 1); + + // Parse space-separated values and optional "/ alpha" + std::istringstream iss(inner); + std::vector components = {}; + std::string token = {}; + float alpha = 1.0f; + bool foundSlash = false; + while (iss >> token) { + if (token == "/") { + foundSlash = true; + continue; + } + float val = std::stof(token); + if (foundSlash) { + alpha = val; + } else { + components.push_back(val); + } + } + if (components.size() >= 3) { + Color color = {}; + color.red = components[0]; + color.green = components[1]; + color.blue = components[2]; + color.alpha = alpha; + color.colorSpace = colorSpace; + return color; + } } } @@ -1382,14 +1463,17 @@ Color SVGParserImpl::parseColor(const std::string& value) { auto it = namedColors.find(value); if (it != namedColors.end()) { - auto color = Color::FromHex(it->second); - if (value == "transparent") { - color.alpha = 0; - } + uint32_t hex = it->second; + Color color = {}; + color.red = static_cast((hex >> 16) & 0xFF) / 255.0f; + color.green = static_cast((hex >> 8) & 0xFF) / 255.0f; + color.blue = static_cast(hex & 0xFF) / 255.0f; + color.alpha = (value == "transparent") ? 0.0f : 1.0f; + color.colorSpace = ColorSpace::SRGB; return color; } - return {0, 0, 0, 1}; + return {0, 0, 0, 1, ColorSpace::SRGB}; } std::string SVGParserImpl::colorToHex(const std::string& value) { @@ -1400,13 +1484,73 @@ std::string SVGParserImpl::colorToHex(const std::string& value) { if (value[0] == '#') { return value; } + // Already a PAGX p3() color, return as-is. + if (value.substr(0, 3) == "p3(") { + return value; + } + // Already a PAGX srgb() color, return as-is. + if (value.substr(0, 5) == "srgb(") { + return value; + } // url() references should be returned as-is. if (value.find("url(") == 0) { return value; } + // CSS Color Level 4: color(display-p3 r g b) -> p3(r, g, b) + if (value.find("color(display-p3") == 0) { + auto start = value.find("display-p3"); + auto end = value.find(')'); + if (start != std::string::npos && end != std::string::npos) { + auto inner = value.substr(start + 10, end - start - 10); // skip "display-p3" + // Trim whitespace + inner.erase(0, inner.find_first_not_of(" \t")); + inner.erase(inner.find_last_not_of(" \t") + 1); + // Parse space-separated values and optional "/ alpha" + std::istringstream iss(inner); + std::vector components = {}; + std::string token = {}; + float alpha = 1.0f; + bool foundSlash = false; + while (iss >> token) { + if (token == "/") { + foundSlash = true; + continue; + } + float val = std::stof(token); + if (foundSlash) { + alpha = val; + } else { + components.push_back(val); + } + } + if (components.size() >= 3) { + char buf[64] = {}; + if (alpha < 1.0f) { + snprintf(buf, sizeof(buf), "p3(%.4g, %.4g, %.4g, %.4g)", components[0], components[1], + components[2], alpha); + } else { + snprintf(buf, sizeof(buf), "p3(%.4g, %.4g, %.4g)", components[0], components[1], + components[2]); + } + return std::string(buf); + } + } + } // Parse the color (handles named colors, rgb, rgba, etc.) and convert to hex. Color color = parseColor(value); - return color.toHexString(color.alpha < 1.0f); + // Convert to hex string. + auto toHex = [](float v) { + int i = static_cast(std::round(v * 255.0f)); + i = std::max(0, std::min(255, i)); + char buf[3] = {}; + snprintf(buf, sizeof(buf), "%02X", i); + return std::string(buf); + }; + std::string result = "#" + toHex(color.red) + toHex(color.green) + toHex(color.blue); + if (color.alpha < 1.0f) { + result += toHex(color.alpha); + } + return result; } float SVGParserImpl::parseLength(const std::string& value, float containerSize) { diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index d119a52556..6e0857cd4a 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -21,6 +21,8 @@ #include #include #include "pagx/model/BlurFilter.h" +#include "pagx/model/types/ColorSpace.h" +#include "tgfx/core/ColorSpace.h" #include "pagx/model/Composition.h" #include "pagx/model/ConicGradient.h" #include "pagx/model/DiamondGradient.h" @@ -163,6 +165,21 @@ static tgfx::Point ToTGFX(const Point& p) { } static tgfx::Color ToTGFX(const Color& c) { + // tgfx::Color is always in sRGB color space. If source color is in Display P3, + // we need to convert it to sRGB for correct rendering. + if (c.colorSpace == ColorSpace::DisplayP3) { + // Convert Display P3 to sRGB using tgfx::ColorSpace + tgfx::ColorMatrix33 p3ToSRGB = {}; + tgfx::ColorSpace::DisplayP3()->gamutTransformTo(tgfx::ColorSpace::SRGB().get(), &p3ToSRGB); + + float r = c.red * p3ToSRGB.values[0][0] + c.green * p3ToSRGB.values[0][1] + + c.blue * p3ToSRGB.values[0][2]; + float g = c.red * p3ToSRGB.values[1][0] + c.green * p3ToSRGB.values[1][1] + + c.blue * p3ToSRGB.values[1][2]; + float b = c.red * p3ToSRGB.values[2][0] + c.green * p3ToSRGB.values[2][1] + + c.blue * p3ToSRGB.values[2][2]; + return {r, g, b, c.alpha}; + } return {c.red, c.green, c.blue, c.alpha}; } @@ -457,11 +474,10 @@ class LayerBuilderImpl { auto fill = std::make_shared(); std::shared_ptr colorSource = nullptr; - if (node->colorSource) { - colorSource = convertColorSource(node->colorSource.get()); - } else if (!node->color.empty()) { - auto color = Color::Parse(node->color); - colorSource = tgfx::SolidColor::Make(ToTGFX(color)); + if (node->color) { + colorSource = convertColorSource(node->color.get()); + } else if (!node->colorRef.empty()) { + // TODO: Resolve color reference from resources } if (colorSource) { @@ -476,11 +492,10 @@ class LayerBuilderImpl { auto stroke = std::make_shared(); std::shared_ptr colorSource = nullptr; - if (node->colorSource) { - colorSource = convertColorSource(node->colorSource.get()); - } else if (!node->color.empty()) { - auto color = Color::Parse(node->color); - colorSource = tgfx::SolidColor::Make(ToTGFX(color)); + if (node->color) { + colorSource = convertColorSource(node->color.get()); + } else if (!node->colorRef.empty()) { + // TODO: Resolve color reference from resources } if (colorSource) { From 5118e71a282ea778dddb7727ae0ccaf5ba32231c Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 21:49:25 +0800 Subject: [PATCH 105/678] Always inline SolidColor in PAGX output instead of extracting to Resources. --- pagx/src/PAGXXMLWriter.cpp | 181 +++++++++++++++++++++++++------------ 1 file changed, 122 insertions(+), 59 deletions(-) diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index c8d0e12dc2..65d7225367 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -20,6 +20,7 @@ #include #include #include +#include "PAGXEnumUtils.h" #include "pagx/model/BackgroundBlurStyle.h" #include "pagx/model/BlendFilter.h" #include "pagx/model/BlurFilter.h" @@ -40,7 +41,7 @@ #include "pagx/model/LinearGradient.h" #include "pagx/model/MergePath.h" #include "pagx/model/Path.h" -#include "pagx/model/PathDataResource.h" +#include "pagx/model/PathData.h" #include "pagx/model/Polystar.h" #include "pagx/model/RadialGradient.h" #include "pagx/model/RangeSelector.h" @@ -57,6 +58,42 @@ namespace pagx { +//============================================================================== +// Color to string helper +//============================================================================== + +static std::string colorToString(const Color& color) { + auto formatFloat = [](float value) -> std::string { + std::ostringstream oss = {}; + oss << value; + auto str = oss.str(); + if (str.find('.') != std::string::npos) { + while (str.back() == '0') { + str.pop_back(); + } + if (str.back() == '.') { + str.pop_back(); + } + } + return str; + }; + + std::ostringstream oss = {}; + // 色域标识符使用小写: srgb(), p3() + if (color.colorSpace == ColorSpace::DisplayP3) { + oss << "p3("; + } else { + oss << "srgb("; + } + oss << formatFloat(color.red) << "," << formatFloat(color.green) << "," + << formatFloat(color.blue); + if (color.alpha < 1.0f) { + oss << "," << formatFloat(color.alpha); + } + oss << ")"; + return oss.str(); +} + //============================================================================== // XMLBuilder - XML generation helper //============================================================================== @@ -229,7 +266,7 @@ static std::string colorSourceToKey(const ColorSource* node) { switch (node->type()) { case ColorSourceType::SolidColor: { auto solid = static_cast(node); - oss << "SolidColor:" << solid->color.toHexString(true); + oss << "SolidColor:" << colorToString(solid->color); break; } case ColorSourceType::LinearGradient: { @@ -237,7 +274,7 @@ static std::string colorSourceToKey(const ColorSource* node) { oss << "LinearGradient:" << grad->startPoint.x << "," << grad->startPoint.y << ":" << grad->endPoint.x << "," << grad->endPoint.y << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; + oss << stop.offset << "=" << colorToString(stop.color) << ";"; } break; } @@ -246,7 +283,7 @@ static std::string colorSourceToKey(const ColorSource* node) { oss << "RadialGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->radius << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; + oss << stop.offset << "=" << colorToString(stop.color) << ";"; } break; } @@ -255,7 +292,7 @@ static std::string colorSourceToKey(const ColorSource* node) { oss << "ConicGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->startAngle << ":" << grad->endAngle << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; + oss << stop.offset << "=" << colorToString(stop.color) << ";"; } break; } @@ -264,7 +301,7 @@ static std::string colorSourceToKey(const ColorSource* node) { oss << "DiamondGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->halfDiagonal << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; + oss << stop.offset << "=" << colorToString(stop.color) << ";"; } break; } @@ -328,10 +365,15 @@ class ResourceContext { } // Register ColorSource usage (for counting) + // SolidColor is skipped since it's always inlined void registerColorSource(const ColorSource* node) { if (!node) { return; } + // Skip SolidColor - always inlined, no need to track + if (node->type() == ColorSourceType::SolidColor) { + return; + } std::string key = colorSourceToKey(node); if (key.empty()) { return; @@ -354,20 +396,30 @@ class ResourceContext { } // Check if ColorSource should be extracted to Resources + // SolidColor is always inlined, never extracted to Resources bool shouldExtractColorSource(const ColorSource* node) const { if (!node) { return false; } + // SolidColor is always inlined for simplicity + if (node->type() == ColorSourceType::SolidColor) { + return false; + } std::string key = colorSourceToKey(node); auto it = colorSourceMap.find(key); return it != colorSourceMap.end() && it->second.second > 1; } // Get ColorSource resource id (empty if should inline) + // SolidColor always returns empty (always inlined) std::string getColorSourceId(const ColorSource* node) const { if (!node) { return ""; } + // SolidColor is always inlined + if (node->type() == ColorSourceType::SolidColor) { + return ""; + } std::string key = colorSourceToKey(node); auto it = colorSourceMap.find(key); if (it != colorSourceMap.end() && it->second.second > 1) { @@ -397,15 +449,15 @@ class ResourceContext { } case ElementType::Fill: { auto fill = static_cast(element); - if (fill->colorSource) { - registerColorSource(fill->colorSource.get()); + if (fill->color) { + registerColorSource(fill->color.get()); } break; } case ElementType::Stroke: { auto stroke = static_cast(element); - if (stroke->colorSource) { - registerColorSource(stroke->colorSource.get()); + if (stroke->color) { + registerColorSource(stroke->color.get()); } break; } @@ -442,7 +494,7 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& stops for (const auto& stop : stops) { xml.openElement("ColorStop"); xml.addRequiredAttribute("offset", stop.offset); - xml.addRequiredAttribute("color", stop.color.toHexString(stop.color.alpha < 1.0f)); + xml.addRequiredAttribute("color", colorToString(stop.color)); xml.closeElementSelfClosing(); } } @@ -455,7 +507,13 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ if (writeId && !solid->id.empty()) { xml.addAttribute("id", solid->id); } - xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); + xml.addRequiredAttribute("red", solid->color.red); + xml.addRequiredAttribute("green", solid->color.green); + xml.addRequiredAttribute("blue", solid->color.blue); + xml.addAttribute("alpha", solid->color.alpha, 1.0f); + if (solid->color.colorSpace != ColorSpace::SRGB) { + xml.addAttribute("colorSpace", ColorSpaceToString(solid->color.colorSpace)); + } xml.closeElementSelfClosing(); break; } @@ -581,7 +639,13 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, auto solid = static_cast(node); xml.openElement("SolidColor"); xml.addAttribute("id", id); - xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); + xml.addRequiredAttribute("red", solid->color.red); + xml.addRequiredAttribute("green", solid->color.green); + xml.addRequiredAttribute("blue", solid->color.blue); + xml.addAttribute("alpha", solid->color.alpha, 1.0f); + if (solid->color.colorSpace != ColorSpace::SRGB) { + xml.addAttribute("colorSpace", ColorSpaceToString(solid->color.colorSpace)); + } xml.closeElementSelfClosing(); break; } @@ -780,15 +844,15 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, auto fill = static_cast(node); xml.openElement("Fill"); // Check if ColorSource should be referenced or inlined - if (fill->colorSource) { - std::string colorId = ctx.getColorSourceId(fill->colorSource.get()); + if (fill->color) { + std::string colorId = ctx.getColorSourceId(fill->color.get()); if (!colorId.empty()) { // Reference the ColorSource from Resources - xml.addAttribute("color", "#" + colorId); + xml.addAttribute("color", "@" + colorId); } // If colorId is empty, we'll inline it below - } else { - xml.addAttribute("color", fill->color); + } else if (!fill->colorRef.empty()) { + xml.addAttribute("color", fill->colorRef); } xml.addAttribute("alpha", fill->alpha, 1.0f); if (fill->blendMode != BlendMode::Normal) { @@ -801,9 +865,9 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.addAttribute("placement", LayerPlacementToString(fill->placement)); } // Inline ColorSource only if not extracted to Resources - if (fill->colorSource && ctx.getColorSourceId(fill->colorSource.get()).empty()) { + if (fill->color && ctx.getColorSourceId(fill->color.get()).empty()) { xml.closeElementStart(); - writeColorSource(xml, fill->colorSource.get(), false); + writeColorSource(xml, fill->color.get(), false); xml.closeElement(); } else { xml.closeElementSelfClosing(); @@ -814,15 +878,15 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, auto stroke = static_cast(node); xml.openElement("Stroke"); // Check if ColorSource should be referenced or inlined - if (stroke->colorSource) { - std::string colorId = ctx.getColorSourceId(stroke->colorSource.get()); + if (stroke->color) { + std::string colorId = ctx.getColorSourceId(stroke->color.get()); if (!colorId.empty()) { // Reference the ColorSource from Resources - xml.addAttribute("color", "#" + colorId); + xml.addAttribute("color", "@" + colorId); } // If colorId is empty, we'll inline it below - } else { - xml.addAttribute("color", stroke->color); + } else if (!stroke->colorRef.empty()) { + xml.addAttribute("color", stroke->colorRef); } xml.addAttribute("width", stroke->width, 1.0f); xml.addAttribute("alpha", stroke->alpha, 1.0f); @@ -847,9 +911,9 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.addAttribute("placement", LayerPlacementToString(stroke->placement)); } // Inline ColorSource only if not extracted to Resources - if (stroke->colorSource && ctx.getColorSourceId(stroke->colorSource.get()).empty()) { + if (stroke->color && ctx.getColorSourceId(stroke->color.get()).empty()) { xml.closeElementStart(); - writeColorSource(xml, stroke->colorSource.get(), false); + writeColorSource(xml, stroke->color.get(), false); xml.closeElement(); } else { xml.closeElementSelfClosing(); @@ -1043,7 +1107,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.addAttribute("offsetY", style->offsetY); xml.addAttribute("blurrinessX", style->blurrinessX); xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); + xml.addAttribute("color", colorToString(style->color)); xml.addAttribute("showBehindLayer", style->showBehindLayer, true); xml.closeElementSelfClosing(); break; @@ -1058,7 +1122,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.addAttribute("offsetY", style->offsetY); xml.addAttribute("blurrinessX", style->blurrinessX); xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); + xml.addAttribute("color", colorToString(style->color)); xml.closeElementSelfClosing(); break; } @@ -1105,7 +1169,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.addAttribute("offsetY", filter->offsetY); xml.addAttribute("blurrinessX", filter->blurrinessX); xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); + xml.addAttribute("color", colorToString(filter->color)); xml.addAttribute("shadowOnly", filter->shadowOnly); xml.closeElementSelfClosing(); break; @@ -1117,7 +1181,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.addAttribute("offsetY", filter->offsetY); xml.addAttribute("blurrinessX", filter->blurrinessX); xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); + xml.addAttribute("color", colorToString(filter->color)); xml.addAttribute("shadowOnly", filter->shadowOnly); xml.closeElementSelfClosing(); break; @@ -1125,7 +1189,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { case LayerFilterType::BlendFilter: { auto filter = static_cast(node); xml.openElement("BlendFilter"); - xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); + xml.addAttribute("color", colorToString(filter->color)); if (filter->blendMode != BlendMode::Normal) { xml.addAttribute("blendMode", BlendModeToString(filter->blendMode)); } @@ -1160,10 +1224,10 @@ static void writeResource(XMLBuilder& xml, const Node* node, const ResourceConte break; } case NodeType::PathData: { - auto pathData = static_cast(node); + auto pathData = static_cast(node); xml.openElement("PathData"); xml.addAttribute("id", pathData->id); - xml.addAttribute("data", pathData->data); + xml.addAttribute("data", pathData->toSVGString()); xml.closeElementSelfClosing(); break; } @@ -1281,18 +1345,18 @@ std::string PAGXXMLWriter::Write(const Document& doc) { for (const auto& element : layer->contents) { if (element->type() == ElementType::Fill) { auto fill = static_cast(element.get()); - if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { - std::string key = colorSourceToKey(fill->colorSource.get()); + if (fill->color && ctx.shouldExtractColorSource(fill->color.get())) { + std::string key = colorSourceToKey(fill->color.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->colorSource.get(); + colorSourceByKey[key] = fill->color.get(); } } } else if (element->type() == ElementType::Stroke) { auto stroke = static_cast(element.get()); - if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { - std::string key = colorSourceToKey(stroke->colorSource.get()); + if (stroke->color && ctx.shouldExtractColorSource(stroke->color.get())) { + std::string key = colorSourceToKey(stroke->color.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->colorSource.get(); + colorSourceByKey[key] = stroke->color.get(); } } } else if (element->type() == ElementType::Group) { @@ -1301,18 +1365,18 @@ std::string PAGXXMLWriter::Write(const Document& doc) { for (const auto& child : g->elements) { if (child->type() == ElementType::Fill) { auto fill = static_cast(child.get()); - if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { - std::string key = colorSourceToKey(fill->colorSource.get()); + if (fill->color && ctx.shouldExtractColorSource(fill->color.get())) { + std::string key = colorSourceToKey(fill->color.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->colorSource.get(); + colorSourceByKey[key] = fill->color.get(); } } } else if (child->type() == ElementType::Stroke) { auto stroke = static_cast(child.get()); - if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { - std::string key = colorSourceToKey(stroke->colorSource.get()); + if (stroke->color && ctx.shouldExtractColorSource(stroke->color.get())) { + std::string key = colorSourceToKey(stroke->color.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->colorSource.get(); + colorSourceByKey[key] = stroke->color.get(); } } } else if (child->type() == ElementType::Group) { @@ -1338,18 +1402,18 @@ std::string PAGXXMLWriter::Write(const Document& doc) { for (const auto& element : layer->contents) { if (element->type() == ElementType::Fill) { auto fill = static_cast(element.get()); - if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { - std::string key = colorSourceToKey(fill->colorSource.get()); + if (fill->color && ctx.shouldExtractColorSource(fill->color.get())) { + std::string key = colorSourceToKey(fill->color.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->colorSource.get(); + colorSourceByKey[key] = fill->color.get(); } } } else if (element->type() == ElementType::Stroke) { auto stroke = static_cast(element.get()); - if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { - std::string key = colorSourceToKey(stroke->colorSource.get()); + if (stroke->color && ctx.shouldExtractColorSource(stroke->color.get())) { + std::string key = colorSourceToKey(stroke->color.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->colorSource.get(); + colorSourceByKey[key] = stroke->color.get(); } } } else if (element->type() == ElementType::Group) { @@ -1358,19 +1422,18 @@ std::string PAGXXMLWriter::Write(const Document& doc) { for (const auto& child : g->elements) { if (child->type() == ElementType::Fill) { auto fill = static_cast(child.get()); - if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { - std::string key = colorSourceToKey(fill->colorSource.get()); + if (fill->color && ctx.shouldExtractColorSource(fill->color.get())) { + std::string key = colorSourceToKey(fill->color.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->colorSource.get(); + colorSourceByKey[key] = fill->color.get(); } } } else if (child->type() == ElementType::Stroke) { auto stroke = static_cast(child.get()); - if (stroke->colorSource && - ctx.shouldExtractColorSource(stroke->colorSource.get())) { - std::string key = colorSourceToKey(stroke->colorSource.get()); + if (stroke->color && ctx.shouldExtractColorSource(stroke->color.get())) { + std::string key = colorSourceToKey(stroke->color.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->colorSource.get(); + colorSourceByKey[key] = stroke->color.get(); } } } else if (child->type() == ElementType::Group) { From d2d021fc66707039c241e5d75c31df34c1003656 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 21:51:15 +0800 Subject: [PATCH 106/678] Revert "Always inline SolidColor in PAGX output instead of extracting to Resources." This reverts commit 5118e71a282ea778dddb7727ae0ccaf5ba32231c. --- pagx/src/PAGXXMLWriter.cpp | 181 ++++++++++++------------------------- 1 file changed, 59 insertions(+), 122 deletions(-) diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp index 65d7225367..c8d0e12dc2 100644 --- a/pagx/src/PAGXXMLWriter.cpp +++ b/pagx/src/PAGXXMLWriter.cpp @@ -20,7 +20,6 @@ #include #include #include -#include "PAGXEnumUtils.h" #include "pagx/model/BackgroundBlurStyle.h" #include "pagx/model/BlendFilter.h" #include "pagx/model/BlurFilter.h" @@ -41,7 +40,7 @@ #include "pagx/model/LinearGradient.h" #include "pagx/model/MergePath.h" #include "pagx/model/Path.h" -#include "pagx/model/PathData.h" +#include "pagx/model/PathDataResource.h" #include "pagx/model/Polystar.h" #include "pagx/model/RadialGradient.h" #include "pagx/model/RangeSelector.h" @@ -58,42 +57,6 @@ namespace pagx { -//============================================================================== -// Color to string helper -//============================================================================== - -static std::string colorToString(const Color& color) { - auto formatFloat = [](float value) -> std::string { - std::ostringstream oss = {}; - oss << value; - auto str = oss.str(); - if (str.find('.') != std::string::npos) { - while (str.back() == '0') { - str.pop_back(); - } - if (str.back() == '.') { - str.pop_back(); - } - } - return str; - }; - - std::ostringstream oss = {}; - // 色域标识符使用小写: srgb(), p3() - if (color.colorSpace == ColorSpace::DisplayP3) { - oss << "p3("; - } else { - oss << "srgb("; - } - oss << formatFloat(color.red) << "," << formatFloat(color.green) << "," - << formatFloat(color.blue); - if (color.alpha < 1.0f) { - oss << "," << formatFloat(color.alpha); - } - oss << ")"; - return oss.str(); -} - //============================================================================== // XMLBuilder - XML generation helper //============================================================================== @@ -266,7 +229,7 @@ static std::string colorSourceToKey(const ColorSource* node) { switch (node->type()) { case ColorSourceType::SolidColor: { auto solid = static_cast(node); - oss << "SolidColor:" << colorToString(solid->color); + oss << "SolidColor:" << solid->color.toHexString(true); break; } case ColorSourceType::LinearGradient: { @@ -274,7 +237,7 @@ static std::string colorSourceToKey(const ColorSource* node) { oss << "LinearGradient:" << grad->startPoint.x << "," << grad->startPoint.y << ":" << grad->endPoint.x << "," << grad->endPoint.y << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << colorToString(stop.color) << ";"; + oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; } break; } @@ -283,7 +246,7 @@ static std::string colorSourceToKey(const ColorSource* node) { oss << "RadialGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->radius << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << colorToString(stop.color) << ";"; + oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; } break; } @@ -292,7 +255,7 @@ static std::string colorSourceToKey(const ColorSource* node) { oss << "ConicGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->startAngle << ":" << grad->endAngle << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << colorToString(stop.color) << ";"; + oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; } break; } @@ -301,7 +264,7 @@ static std::string colorSourceToKey(const ColorSource* node) { oss << "DiamondGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->halfDiagonal << ":" << grad->matrix.toString() << ":"; for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << colorToString(stop.color) << ";"; + oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; } break; } @@ -365,15 +328,10 @@ class ResourceContext { } // Register ColorSource usage (for counting) - // SolidColor is skipped since it's always inlined void registerColorSource(const ColorSource* node) { if (!node) { return; } - // Skip SolidColor - always inlined, no need to track - if (node->type() == ColorSourceType::SolidColor) { - return; - } std::string key = colorSourceToKey(node); if (key.empty()) { return; @@ -396,30 +354,20 @@ class ResourceContext { } // Check if ColorSource should be extracted to Resources - // SolidColor is always inlined, never extracted to Resources bool shouldExtractColorSource(const ColorSource* node) const { if (!node) { return false; } - // SolidColor is always inlined for simplicity - if (node->type() == ColorSourceType::SolidColor) { - return false; - } std::string key = colorSourceToKey(node); auto it = colorSourceMap.find(key); return it != colorSourceMap.end() && it->second.second > 1; } // Get ColorSource resource id (empty if should inline) - // SolidColor always returns empty (always inlined) std::string getColorSourceId(const ColorSource* node) const { if (!node) { return ""; } - // SolidColor is always inlined - if (node->type() == ColorSourceType::SolidColor) { - return ""; - } std::string key = colorSourceToKey(node); auto it = colorSourceMap.find(key); if (it != colorSourceMap.end() && it->second.second > 1) { @@ -449,15 +397,15 @@ class ResourceContext { } case ElementType::Fill: { auto fill = static_cast(element); - if (fill->color) { - registerColorSource(fill->color.get()); + if (fill->colorSource) { + registerColorSource(fill->colorSource.get()); } break; } case ElementType::Stroke: { auto stroke = static_cast(element); - if (stroke->color) { - registerColorSource(stroke->color.get()); + if (stroke->colorSource) { + registerColorSource(stroke->colorSource.get()); } break; } @@ -494,7 +442,7 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& stops for (const auto& stop : stops) { xml.openElement("ColorStop"); xml.addRequiredAttribute("offset", stop.offset); - xml.addRequiredAttribute("color", colorToString(stop.color)); + xml.addRequiredAttribute("color", stop.color.toHexString(stop.color.alpha < 1.0f)); xml.closeElementSelfClosing(); } } @@ -507,13 +455,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writ if (writeId && !solid->id.empty()) { xml.addAttribute("id", solid->id); } - xml.addRequiredAttribute("red", solid->color.red); - xml.addRequiredAttribute("green", solid->color.green); - xml.addRequiredAttribute("blue", solid->color.blue); - xml.addAttribute("alpha", solid->color.alpha, 1.0f); - if (solid->color.colorSpace != ColorSpace::SRGB) { - xml.addAttribute("colorSpace", ColorSpaceToString(solid->color.colorSpace)); - } + xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); xml.closeElementSelfClosing(); break; } @@ -639,13 +581,7 @@ static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, auto solid = static_cast(node); xml.openElement("SolidColor"); xml.addAttribute("id", id); - xml.addRequiredAttribute("red", solid->color.red); - xml.addRequiredAttribute("green", solid->color.green); - xml.addRequiredAttribute("blue", solid->color.blue); - xml.addAttribute("alpha", solid->color.alpha, 1.0f); - if (solid->color.colorSpace != ColorSpace::SRGB) { - xml.addAttribute("colorSpace", ColorSpaceToString(solid->color.colorSpace)); - } + xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); xml.closeElementSelfClosing(); break; } @@ -844,15 +780,15 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, auto fill = static_cast(node); xml.openElement("Fill"); // Check if ColorSource should be referenced or inlined - if (fill->color) { - std::string colorId = ctx.getColorSourceId(fill->color.get()); + if (fill->colorSource) { + std::string colorId = ctx.getColorSourceId(fill->colorSource.get()); if (!colorId.empty()) { // Reference the ColorSource from Resources - xml.addAttribute("color", "@" + colorId); + xml.addAttribute("color", "#" + colorId); } // If colorId is empty, we'll inline it below - } else if (!fill->colorRef.empty()) { - xml.addAttribute("color", fill->colorRef); + } else { + xml.addAttribute("color", fill->color); } xml.addAttribute("alpha", fill->alpha, 1.0f); if (fill->blendMode != BlendMode::Normal) { @@ -865,9 +801,9 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.addAttribute("placement", LayerPlacementToString(fill->placement)); } // Inline ColorSource only if not extracted to Resources - if (fill->color && ctx.getColorSourceId(fill->color.get()).empty()) { + if (fill->colorSource && ctx.getColorSourceId(fill->colorSource.get()).empty()) { xml.closeElementStart(); - writeColorSource(xml, fill->color.get(), false); + writeColorSource(xml, fill->colorSource.get(), false); xml.closeElement(); } else { xml.closeElementSelfClosing(); @@ -878,15 +814,15 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, auto stroke = static_cast(node); xml.openElement("Stroke"); // Check if ColorSource should be referenced or inlined - if (stroke->color) { - std::string colorId = ctx.getColorSourceId(stroke->color.get()); + if (stroke->colorSource) { + std::string colorId = ctx.getColorSourceId(stroke->colorSource.get()); if (!colorId.empty()) { // Reference the ColorSource from Resources - xml.addAttribute("color", "@" + colorId); + xml.addAttribute("color", "#" + colorId); } // If colorId is empty, we'll inline it below - } else if (!stroke->colorRef.empty()) { - xml.addAttribute("color", stroke->colorRef); + } else { + xml.addAttribute("color", stroke->color); } xml.addAttribute("width", stroke->width, 1.0f); xml.addAttribute("alpha", stroke->alpha, 1.0f); @@ -911,9 +847,9 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, xml.addAttribute("placement", LayerPlacementToString(stroke->placement)); } // Inline ColorSource only if not extracted to Resources - if (stroke->color && ctx.getColorSourceId(stroke->color.get()).empty()) { + if (stroke->colorSource && ctx.getColorSourceId(stroke->colorSource.get()).empty()) { xml.closeElementStart(); - writeColorSource(xml, stroke->color.get(), false); + writeColorSource(xml, stroke->colorSource.get(), false); xml.closeElement(); } else { xml.closeElementSelfClosing(); @@ -1107,7 +1043,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.addAttribute("offsetY", style->offsetY); xml.addAttribute("blurrinessX", style->blurrinessX); xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", colorToString(style->color)); + xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); xml.addAttribute("showBehindLayer", style->showBehindLayer, true); xml.closeElementSelfClosing(); break; @@ -1122,7 +1058,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.addAttribute("offsetY", style->offsetY); xml.addAttribute("blurrinessX", style->blurrinessX); xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", colorToString(style->color)); + xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); xml.closeElementSelfClosing(); break; } @@ -1169,7 +1105,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.addAttribute("offsetY", filter->offsetY); xml.addAttribute("blurrinessX", filter->blurrinessX); xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", colorToString(filter->color)); + xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); xml.addAttribute("shadowOnly", filter->shadowOnly); xml.closeElementSelfClosing(); break; @@ -1181,7 +1117,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.addAttribute("offsetY", filter->offsetY); xml.addAttribute("blurrinessX", filter->blurrinessX); xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", colorToString(filter->color)); + xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); xml.addAttribute("shadowOnly", filter->shadowOnly); xml.closeElementSelfClosing(); break; @@ -1189,7 +1125,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { case LayerFilterType::BlendFilter: { auto filter = static_cast(node); xml.openElement("BlendFilter"); - xml.addAttribute("color", colorToString(filter->color)); + xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); if (filter->blendMode != BlendMode::Normal) { xml.addAttribute("blendMode", BlendModeToString(filter->blendMode)); } @@ -1224,10 +1160,10 @@ static void writeResource(XMLBuilder& xml, const Node* node, const ResourceConte break; } case NodeType::PathData: { - auto pathData = static_cast(node); + auto pathData = static_cast(node); xml.openElement("PathData"); xml.addAttribute("id", pathData->id); - xml.addAttribute("data", pathData->toSVGString()); + xml.addAttribute("data", pathData->data); xml.closeElementSelfClosing(); break; } @@ -1345,18 +1281,18 @@ std::string PAGXXMLWriter::Write(const Document& doc) { for (const auto& element : layer->contents) { if (element->type() == ElementType::Fill) { auto fill = static_cast(element.get()); - if (fill->color && ctx.shouldExtractColorSource(fill->color.get())) { - std::string key = colorSourceToKey(fill->color.get()); + if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { + std::string key = colorSourceToKey(fill->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->color.get(); + colorSourceByKey[key] = fill->colorSource.get(); } } } else if (element->type() == ElementType::Stroke) { auto stroke = static_cast(element.get()); - if (stroke->color && ctx.shouldExtractColorSource(stroke->color.get())) { - std::string key = colorSourceToKey(stroke->color.get()); + if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { + std::string key = colorSourceToKey(stroke->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->color.get(); + colorSourceByKey[key] = stroke->colorSource.get(); } } } else if (element->type() == ElementType::Group) { @@ -1365,18 +1301,18 @@ std::string PAGXXMLWriter::Write(const Document& doc) { for (const auto& child : g->elements) { if (child->type() == ElementType::Fill) { auto fill = static_cast(child.get()); - if (fill->color && ctx.shouldExtractColorSource(fill->color.get())) { - std::string key = colorSourceToKey(fill->color.get()); + if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { + std::string key = colorSourceToKey(fill->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->color.get(); + colorSourceByKey[key] = fill->colorSource.get(); } } } else if (child->type() == ElementType::Stroke) { auto stroke = static_cast(child.get()); - if (stroke->color && ctx.shouldExtractColorSource(stroke->color.get())) { - std::string key = colorSourceToKey(stroke->color.get()); + if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { + std::string key = colorSourceToKey(stroke->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->color.get(); + colorSourceByKey[key] = stroke->colorSource.get(); } } } else if (child->type() == ElementType::Group) { @@ -1402,18 +1338,18 @@ std::string PAGXXMLWriter::Write(const Document& doc) { for (const auto& element : layer->contents) { if (element->type() == ElementType::Fill) { auto fill = static_cast(element.get()); - if (fill->color && ctx.shouldExtractColorSource(fill->color.get())) { - std::string key = colorSourceToKey(fill->color.get()); + if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { + std::string key = colorSourceToKey(fill->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->color.get(); + colorSourceByKey[key] = fill->colorSource.get(); } } } else if (element->type() == ElementType::Stroke) { auto stroke = static_cast(element.get()); - if (stroke->color && ctx.shouldExtractColorSource(stroke->color.get())) { - std::string key = colorSourceToKey(stroke->color.get()); + if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { + std::string key = colorSourceToKey(stroke->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->color.get(); + colorSourceByKey[key] = stroke->colorSource.get(); } } } else if (element->type() == ElementType::Group) { @@ -1422,18 +1358,19 @@ std::string PAGXXMLWriter::Write(const Document& doc) { for (const auto& child : g->elements) { if (child->type() == ElementType::Fill) { auto fill = static_cast(child.get()); - if (fill->color && ctx.shouldExtractColorSource(fill->color.get())) { - std::string key = colorSourceToKey(fill->color.get()); + if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { + std::string key = colorSourceToKey(fill->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->color.get(); + colorSourceByKey[key] = fill->colorSource.get(); } } } else if (child->type() == ElementType::Stroke) { auto stroke = static_cast(child.get()); - if (stroke->color && ctx.shouldExtractColorSource(stroke->color.get())) { - std::string key = colorSourceToKey(stroke->color.get()); + if (stroke->colorSource && + ctx.shouldExtractColorSource(stroke->colorSource.get())) { + std::string key = colorSourceToKey(stroke->colorSource.get()); if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->color.get(); + colorSourceByKey[key] = stroke->colorSource.get(); } } } else if (child->type() == ElementType::Group) { From e1d3f6ff4fc684ed9506e3ac2427db798fd56d30 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 22:08:07 +0800 Subject: [PATCH 107/678] Change TextSpan x/y to position Point to match tgfx. --- pagx/docs/pagx_spec.md | 194 +++++++++++++++++++--------- pagx/include/pagx/model/TextSpan.h | 10 +- pagx/src/PAGXXMLParser.cpp | 161 +++++++++++++++++++---- pagx/src/svg/SVGImporter.cpp | 199 +++++++++++++++++++++++++---- pagx/src/tgfx/LayerBuilder.cpp | 6 +- 5 files changed, 452 insertions(+), 118 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 07ffa978a2..c6b9b0d051 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -147,15 +147,37 @@ PAGX 是纯 XML 文件(`.pagx`),可引用外部资源文件(图片、视 ### 2.8 颜色(Color) -PAGX 支持多种颜色格式: +PAGX 支持两种颜色表示方式: + +#### HEX 格式(十六进制) + +HEX 格式用于表示 sRGB 色域的颜色,使用 `#` 前缀的十六进制值: | 格式 | 示例 | 说明 | |------|------|------| -| HEX | `#RGB`、`#RRGGBB`、`#RRGGBBAA` | 十六进制 | -| RGB | `rgb(255,0,0)`、`rgba(255,0,0,0.5)` | RGB 带可选透明度 | -| HSL | `hsl(0,100%,50%)`、`hsla(0,100%,50%,0.5)` | HSL 带可选透明度 | -| 色域 | `color(display-p3 1 0 0)` | 广色域颜色 | -| 引用 | `@resourceId` | 引用 Resources 中定义的颜色源 | +| `#RGB` | `#F00` | 3 位简写,每位扩展为两位(等价于 `#FF0000`) | +| `#RRGGBB` | `#FF0000` | 6 位标准格式,不透明 | +| `#RRGGBBAA` | `#FF000080` | 8 位带 Alpha,Alpha 在末尾(与 CSS 一致) | + +#### 浮点数格式 + +浮点数格式使用 `色域(r, g, b)` 或 `色域(r, g, b, a)` 的形式表示颜色,支持 sRGB 和 Display P3 两种色域: + +| 色域 | 格式 | 示例 | 说明 | +|------|------|------|------| +| sRGB | `srgb(r, g, b)` | `srgb(1.0, 0.5, 0.2)` | sRGB 色域,各分量 0.0~1.0 | +| sRGB | `srgb(r, g, b, a)` | `srgb(1.0, 0.5, 0.2, 0.8)` | 带透明度 | +| Display P3 | `p3(r, g, b)` | `p3(1.0, 0.5, 0.2)` | Display P3 广色域 | +| Display P3 | `p3(r, g, b, a)` | `p3(1.0, 0.5, 0.2, 0.8)` | 带透明度 | + +**注意**: +- 色域标识符(`srgb` 或 `p3`)和括号**不能省略** +- 广色域颜色(Display P3)的分量值可以超出 [0, 1] 范围,以表示超出 sRGB 色域的颜色 +- sRGB 浮点格式与 HEX 格式表示相同的色域,可根据需要选择 + +#### 颜色源引用 + +使用 `@resourceId` 引用 Resources 中定义的颜色源(渐变、图案等)。 ### 2.9 路径数据语法(Path Data Syntax) @@ -289,12 +311,18 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 ##### 纯色(SolidColor) ```xml - + + + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color | (必填) | 颜色值 | +| `red` | float | 0 | 红色分量,sRGB 为 0.0~1.0,广色域可超出 | +| `green` | float | 0 | 绿色分量 | +| `blue` | float | 0 | 蓝色分量 | +| `alpha` | float | 1 | 透明度,0.0~1.0 | +| `colorSpace` | ColorSpace | sRGB | 色域:`sRGB` 或 `displayP3` | ##### 线性渐变(LinearGradient) @@ -997,15 +1025,14 @@ y = center.y + outerRadius * sin(angle) 文本片段提供文本内容的几何形状。一个 TextSpan 经过塑形后会产生**字形列表**(多个字形),而非单一 Path。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `x` | float | 0 | X 位置 | -| `y` | float | 0 | Y 位置 | +| `position` | point | 0,0 | 文本位置 | | `font` | string | (必填) | 字体族 | | `fontSize` | float | 12 | 字号 | | `fontWeight` | int | 400 | 字重(100-900) | @@ -1031,7 +1058,9 @@ y = center.y + outerRadius * sin(angle) ```xml - + + + @@ -1052,12 +1081,14 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | +| `color` | idref | - | 颜色源引用(如 `@gradientId`) | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | | `fillRule` | FillRule | winding | 填充规则(见下方) | | `placement` | LayerPlacement | background | 绘制位置(见 5.3.3 节) | +子元素:可内嵌一个颜色源(SolidColor、LinearGradient、RadialGradient、ConicGradient、DiamondGradient、ImagePattern) + **FillRule(填充规则)**: | 值 | 说明 | @@ -1076,10 +1107,14 @@ y = center.y + outerRadius * sin(angle) ```xml - + + + - + + + @@ -1092,7 +1127,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | +| `color` | idref | - | 颜色源引用(如 `@gradientId`) | | `width` | float | 1 | 描边宽度 | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | @@ -1412,26 +1447,26 @@ finalColor = blend(originalColor, overrideColor, blendFactor) 将文本沿指定路径排列。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `path` | idref | (必填) | PathData 资源引用 "@id" | -| `align` | TextPathAlign | start | 对齐模式(见下方) | +| `textAlign` | TextAlign | start | 对齐模式(见下方) | | `firstMargin` | float | 0 | 起始边距 | | `lastMargin` | float | 0 | 结束边距 | | `perpendicularToPath` | bool | true | 垂直于路径 | | `reversed` | bool | false | 反转方向 | -| `forceAlignment` | bool | false | 强制对齐 | -**TextPathAlign(对齐模式)**: +**TextAlign 在 TextPath 中的含义**: | 值 | 说明 | |------|------| | `start` | 从路径起点开始排列 | | `center` | 文本居中于路径 | | `end` | 文本结束于路径终点 | +| `justify` | 强制填满路径,自动调整字间距以填满可用路径长度(减去边距) | **边距**: - `firstMargin`:起点边距(从路径起点向内偏移) @@ -1444,46 +1479,55 @@ finalColor = blend(originalColor, overrideColor, blendFactor) **闭合路径**:对于闭合路径,超出范围的字形会环绕到路径另一端。 -**强制对齐**:`forceAlignment="true"` 时,自动调整字间距以填满可用路径长度(减去边距)。 - #### 5.5.6 文本排版(TextLayout) -文本排版修改器对累积的文本元素应用段落排版,是 PAGX 格式特有的元素。与 TextPath 类似,TextLayout 作用于累积的字形列表,为其应用自动换行和对齐。 +文本排版修改器对累积的文本元素应用段落排版,是 PAGX 格式特有的元素。TextLayout 支持两种模式: + +- **Point Text 模式**(`width=0`):单行文本,(x, y) 作为锚点,textAlign 控制文本相对于锚点的对齐 +- **Box Text 模式**(`width>0`):多行文本框,支持自动换行、垂直对齐等 渲染时会由附加的文字排版模块预先排版,重新计算每个字形的位置。转换为 PAG 二进制格式时,TextLayout 会被预排版展开,字形位置直接写入 TextSpan。 ```xml + + + Hello World + + + + + 第一段内容... 粗体 普通文本。 - + lineHeight="1.5"/> ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `width` | float | (必填) | 文本框宽度 | -| `height` | float | (必填) | 文本框高度 | -| `align` | TextAlign | left | 水平对齐(见下方) | +| `x` | float | 0 | 文本布局原点 x 坐标 | +| `y` | float | 0 | 文本布局原点 y 坐标 | +| `width` | float | 0 | 文本框宽度(0 = Point Text 模式) | +| `height` | float | 0 | 文本框高度(0 = 高度自适应) | +| `textAlign` | TextAlign | start | 水平对齐(见下方) | +| `textAlignLast` | TextAlign | start | 最后一行对齐(仅 Justify 时生效) | | `verticalAlign` | VerticalAlign | top | 垂直对齐(见下方) | | `lineHeight` | float | 1.2 | 行高倍数 | -| `indent` | float | 0 | 首行缩进 | -| `overflow` | Overflow | clip | 溢出处理(见下方) | +| `direction` | TextDirection | horizontal | 文本方向(见下方) | **TextAlign(水平对齐)**: | 值 | 说明 | |------|------| -| `left` | 左对齐 | +| `start` | 起始对齐(LTR 为左对齐,RTL 为右对齐) | | `center` | 居中对齐 | -| `right` | 右对齐 | +| `end` | 结束对齐(LTR 为右对齐,RTL 为左对齐) | | `justify` | 两端对齐 | **VerticalAlign(垂直对齐)**: @@ -1494,13 +1538,12 @@ finalColor = blend(originalColor, overrideColor, blendFactor) | `center` | 垂直居中 | | `bottom` | 底部对齐 | -**Overflow(溢出处理)**: +**TextDirection(文本方向)**: | 值 | 说明 | |------|------| -| `clip` | 裁剪:超出部分不显示 | -| `visible` | 可见:超出部分仍然显示 | -| `ellipsis` | 省略:超出部分显示省略号 | +| `horizontal` | 横排文本 | +| `vertical` | 竖排文本 | #### 5.5.7 富文本 @@ -1508,8 +1551,8 @@ finalColor = blend(originalColor, overrideColor, blendFactor) ```xml - Hello - World + Hello + World ``` @@ -1808,7 +1851,7 @@ Layer / Group - + @@ -1907,7 +1950,7 @@ Layer / Group ```xml - + @@ -1916,7 +1959,7 @@ Layer / Group - + @@ -1928,13 +1971,21 @@ Layer / Group #### B.2.5 TextLayout 富文本排版 ```xml + + + Centered Text + + + + + This is bold and italic text in a paragraph that will automatically wrap to fit the container width. - + ``` @@ -1943,11 +1994,19 @@ Layer / Group ```xml + - + + + + + + + + ``` --- @@ -1993,7 +2052,11 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `color` | color | (必填) | +| `red` | float | 0 | +| `green` | float | 0 | +| `blue` | float | 0 | +| `alpha` | float | 1 | +| `colorSpace` | ColorSpace | sRGB | #### LinearGradient @@ -2202,8 +2265,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `x` | float | 0 | -| `y` | float | 0 | +| `position` | point | 0,0 | | `font` | string | (必填) | | `fontSize` | float | 12 | | `fontWeight` | int | 400 | @@ -2219,17 +2281,19 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `color` | color/idref | #000000 | +| `color` | idref | - | | `alpha` | float | 1 | | `blendMode` | BlendMode | normal | | `fillRule` | FillRule | winding | | `placement` | LayerPlacement | background | +子元素:ColorSource(SolidColor、LinearGradient 等) + #### Stroke | 属性 | 类型 | 默认值 | |------|------|--------| -| `color` | color/idref | #000000 | +| `color` | idref | - | | `width` | float | 1 | | `alpha` | float | 1 | | `blendMode` | BlendMode | normal | @@ -2241,6 +2305,8 @@ Layer / Group | `align` | StrokeAlign | center | | `placement` | LayerPlacement | background | +子元素:ColorSource(SolidColor、LinearGradient 等) + ### C.8 形状修改器节点 #### TrimPath @@ -2304,24 +2370,25 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| | `path` | idref | (必填) | -| `align` | TextPathAlign | start | +| `textAlign` | TextAlign | start | | `firstMargin` | float | 0 | | `lastMargin` | float | 0 | | `perpendicularToPath` | bool | true | | `reversed` | bool | false | -| `forceAlignment` | bool | false | #### TextLayout | 属性 | 类型 | 默认值 | |------|------|--------| -| `width` | float | (必填) | -| `height` | float | (必填) | -| `align` | TextAlign | left | +| `x` | float | 0 | +| `y` | float | 0 | +| `width` | float | 0 | +| `height` | float | 0 | +| `textAlign` | TextAlign | start | +| `textAlignLast` | TextAlign | start | | `verticalAlign` | VerticalAlign | top | | `lineHeight` | float | 1.2 | -| `indent` | float | 0 | -| `overflow` | Overflow | clip | +| `direction` | TextDirection | horizontal | ### C.10 其他节点 @@ -2364,6 +2431,12 @@ Layer / Group | **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | | **SamplingMode** | `nearest`, `linear`, `mipmap` | +#### 颜色相关 + +| 枚举 | 值 | +|------|------| +| **ColorSpace** | `sRGB`, `displayP3` | + #### 绘制器相关 | 枚举 | 值 | @@ -2389,8 +2462,7 @@ Layer / Group | **SelectorUnit** | `index`, `percentage` | | **SelectorShape** | `square`, `rampUp`, `rampDown`, `triangle`, `round`, `smooth` | | **SelectorMode** | `add`, `subtract`, `intersect`, `min`, `max`, `difference` | -| **TextPathAlign** | `start`, `center`, `end` | -| **TextAlign** | `left`, `center`, `right`, `justify` | +| **TextAlign** | `start`, `center`, `end`, `justify` | | **VerticalAlign** | `top`, `center`, `bottom` | -| **Overflow** | `clip`, `visible`, `ellipsis` | +| **TextDirection** | `horizontal`, `vertical` | | **RepeaterOrder** | `belowOriginal`, `aboveOriginal` | diff --git a/pagx/include/pagx/model/TextSpan.h b/pagx/include/pagx/model/TextSpan.h index bd568352f9..f4643e956c 100644 --- a/pagx/include/pagx/model/TextSpan.h +++ b/pagx/include/pagx/model/TextSpan.h @@ -20,6 +20,7 @@ #include #include "pagx/model/Element.h" +#include "pagx/model/types/Point.h" namespace pagx { @@ -30,14 +31,9 @@ namespace pagx { class TextSpan : public Element { public: /** - * The x-coordinate of the text baseline starting point. The default value is 0. + * The position of the text blob. */ - float x = 0; - - /** - * The y-coordinate of the text baseline starting point. The default value is 0. - */ - float y = 0; + Point position = {}; /** * The font family name. diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index 4542d37036..ffd3306dcb 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -19,6 +19,7 @@ #include "PAGXXMLParser.h" #include #include +#include "PAGXEnumUtils.h" namespace pagx { @@ -293,16 +294,16 @@ void PAGXXMLParser::parseDocument(const XMLNode* root, Document* doc) { void PAGXXMLParser::parseResources(const XMLNode* node, Document* doc) { for (const auto& child : node->children) { - // Try to parse as a resource first + // Try to parse as a resource (including color sources) auto resource = parseResource(child.get()); if (resource) { doc->resources.push_back(std::move(resource)); continue; } - // Try to parse as a color source + // Try to parse as a color source (which is also a Node) auto colorSource = parseColorSource(child.get()); if (colorSource) { - doc->colorSources.push_back(std::move(colorSource)); + doc->resources.push_back(std::move(colorSource)); } } } @@ -585,8 +586,8 @@ std::unique_ptr PAGXXMLParser::parsePath(const XMLNode* node) { std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) { auto textSpan = std::make_unique(); - textSpan->x = getFloatAttribute(node, "x", 0); - textSpan->y = getFloatAttribute(node, "y", 0); + auto positionStr = getAttribute(node, "position", "0,0"); + textSpan->position = parsePoint(positionStr); textSpan->font = getAttribute(node, "font"); textSpan->fontSize = getFloatAttribute(node, "fontSize", 12); textSpan->fontWeight = getIntAttribute(node, "fontWeight", 400); @@ -603,7 +604,7 @@ std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) { std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { auto fill = std::make_unique(); - fill->color = getAttribute(node, "color"); + fill->colorRef = getAttribute(node, "color"); fill->alpha = getFloatAttribute(node, "alpha", 1); fill->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); fill->fillRule = FillRuleFromString(getAttribute(node, "fillRule", "winding")); @@ -612,7 +613,7 @@ std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { for (const auto& child : node->children) { auto colorSource = parseColorSource(child.get()); if (colorSource) { - fill->colorSource = std::move(colorSource); + fill->color = std::move(colorSource); break; } } @@ -622,7 +623,7 @@ std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { std::unique_ptr PAGXXMLParser::parseStroke(const XMLNode* node) { auto stroke = std::make_unique(); - stroke->color = getAttribute(node, "color"); + stroke->colorRef = getAttribute(node, "color"); stroke->width = getFloatAttribute(node, "width", 1); stroke->alpha = getFloatAttribute(node, "alpha", 1); stroke->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); @@ -640,7 +641,7 @@ std::unique_ptr PAGXXMLParser::parseStroke(const XMLNode* node) { for (const auto& child : node->children) { auto colorSource = parseColorSource(child.get()); if (colorSource) { - stroke->colorSource = std::move(colorSource); + stroke->color = std::move(colorSource); break; } } @@ -786,10 +787,11 @@ RangeSelector PAGXXMLParser::parseRangeSelector(const XMLNode* node) { std::unique_ptr PAGXXMLParser::parseSolidColor(const XMLNode* node) { auto solid = std::make_unique(); solid->id = getAttribute(node, "id"); - auto colorStr = getAttribute(node, "color"); - if (!colorStr.empty()) { - solid->color = Color::Parse(colorStr); - } + solid->color.red = getFloatAttribute(node, "red", 0); + solid->color.green = getFloatAttribute(node, "green", 0); + solid->color.blue = getFloatAttribute(node, "blue", 0); + solid->color.alpha = getFloatAttribute(node, "alpha", 1); + solid->color.colorSpace = ColorSpaceFromString(getAttribute(node, "colorSpace", "sRGB")); return solid; } @@ -886,7 +888,7 @@ ColorStop PAGXXMLParser::parseColorStop(const XMLNode* node) { stop.offset = getFloatAttribute(node, "offset", 0); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { - stop.color = Color::Parse(colorStr); + stop.color = parseColor(colorStr); } return stop; } @@ -902,10 +904,10 @@ std::unique_ptr PAGXXMLParser::parseImage(const XMLNode* node) { return image; } -std::unique_ptr PAGXXMLParser::parsePathData(const XMLNode* node) { - auto pathData = std::make_unique(); +std::unique_ptr PAGXXMLParser::parsePathData(const XMLNode* node) { + auto data = getAttribute(node, "data"); + auto pathData = std::make_unique(PathData::FromSVGString(data)); pathData->id = getAttribute(node, "id"); - pathData->data = getAttribute(node, "data"); return pathData; } @@ -938,7 +940,7 @@ std::unique_ptr PAGXXMLParser::parseDropShadowStyle(const XMLNo style->blurrinessY = getFloatAttribute(node, "blurrinessY", 0); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { - style->color = Color::Parse(colorStr); + style->color = parseColor(colorStr); } style->showBehindLayer = getBoolAttribute(node, "showBehindLayer", true); return style; @@ -953,7 +955,7 @@ std::unique_ptr PAGXXMLParser::parseInnerShadowStyle(const XML style->blurrinessY = getFloatAttribute(node, "blurrinessY", 0); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { - style->color = Color::Parse(colorStr); + style->color = parseColor(colorStr); } return style; } @@ -988,7 +990,7 @@ std::unique_ptr PAGXXMLParser::parseDropShadowFilter(const XML filter->blurrinessY = getFloatAttribute(node, "blurrinessY", 0); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { - filter->color = Color::Parse(colorStr); + filter->color = parseColor(colorStr); } filter->shadowOnly = getBoolAttribute(node, "shadowOnly", false); return filter; @@ -1002,7 +1004,7 @@ std::unique_ptr PAGXXMLParser::parseInnerShadowFilter(const X filter->blurrinessY = getFloatAttribute(node, "blurrinessY", 0); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { - filter->color = Color::Parse(colorStr); + filter->color = parseColor(colorStr); } filter->shadowOnly = getBoolAttribute(node, "shadowOnly", false); return filter; @@ -1012,7 +1014,7 @@ std::unique_ptr PAGXXMLParser::parseBlendFilter(const XMLNode* node auto filter = std::make_unique(); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { - filter->color = Color::Parse(colorStr); + filter->color = parseColor(colorStr); } filter->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); return filter; @@ -1136,6 +1138,121 @@ Rect PAGXXMLParser::parseRect(const std::string& str) { return rect; } +namespace { +int parseHexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return 0; +} +} // namespace + +Color PAGXXMLParser::parseColor(const std::string& str) { + if (str.empty()) { + return {}; + } + // Hex format: #RGB, #RRGGBB, #RRGGBBAA (sRGB) + if (str[0] == '#') { + auto hex = str.substr(1); + Color color = {}; + color.colorSpace = ColorSpace::SRGB; + if (hex.size() == 3) { + int r = parseHexDigit(hex[0]); + int g = parseHexDigit(hex[1]); + int b = parseHexDigit(hex[2]); + color.red = static_cast(r * 17) / 255.0f; + color.green = static_cast(g * 17) / 255.0f; + color.blue = static_cast(b * 17) / 255.0f; + color.alpha = 1.0f; + return color; + } + if (hex.size() == 6) { + int r = parseHexDigit(hex[0]) * 16 + parseHexDigit(hex[1]); + int g = parseHexDigit(hex[2]) * 16 + parseHexDigit(hex[3]); + int b = parseHexDigit(hex[4]) * 16 + parseHexDigit(hex[5]); + color.red = static_cast(r) / 255.0f; + color.green = static_cast(g) / 255.0f; + color.blue = static_cast(b) / 255.0f; + color.alpha = 1.0f; + return color; + } + if (hex.size() == 8) { + int r = parseHexDigit(hex[0]) * 16 + parseHexDigit(hex[1]); + int g = parseHexDigit(hex[2]) * 16 + parseHexDigit(hex[3]); + int b = parseHexDigit(hex[4]) * 16 + parseHexDigit(hex[5]); + int a = parseHexDigit(hex[6]) * 16 + parseHexDigit(hex[7]); + color.red = static_cast(r) / 255.0f; + color.green = static_cast(g) / 255.0f; + color.blue = static_cast(b) / 255.0f; + color.alpha = static_cast(a) / 255.0f; + return color; + } + } + // sRGB float format: srgb(r, g, b) or srgb(r, g, b, a) + if (str.substr(0, 5) == "srgb(") { + auto start = str.find('('); + auto end = str.find(')'); + if (start != std::string::npos && end != std::string::npos) { + auto inner = str.substr(start + 1, end - start - 1); + std::istringstream iss(inner); + std::string token = {}; + std::vector components = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + if (!trimmed.empty()) { + components.push_back(std::stof(trimmed)); + } + } + if (components.size() >= 3) { + Color color = {}; + color.red = components[0]; + color.green = components[1]; + color.blue = components[2]; + color.alpha = components.size() >= 4 ? components[3] : 1.0f; + color.colorSpace = ColorSpace::SRGB; + return color; + } + } + } + // Display P3 format: p3(r, g, b) or p3(r, g, b, a) + if (str.substr(0, 3) == "p3(") { + auto start = str.find('('); + auto end = str.find(')'); + if (start != std::string::npos && end != std::string::npos) { + auto inner = str.substr(start + 1, end - start - 1); + std::istringstream iss(inner); + std::string token = {}; + std::vector components = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + if (!trimmed.empty()) { + components.push_back(std::stof(trimmed)); + } + } + if (components.size() >= 3) { + Color color = {}; + color.red = components[0]; + color.green = components[1]; + color.blue = components[2]; + color.alpha = components.size() >= 4 ? components[3] : 1.0f; + color.colorSpace = ColorSpace::DisplayP3; + return color; + } + } + } + return {}; +} + std::vector PAGXXMLParser::parseFloatList(const std::string& str) { std::vector values = {}; std::istringstream iss(str); diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 23599ced67..9029ef2d79 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -130,6 +130,10 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& do child = child->getNextSibling(); } + // Second pass: count references to gradients/patterns. + // This determines which ColorSources should be extracted to resources. + countColorSourceReferences(root); + // Check if we need a viewBox transform. bool needsViewBoxTransform = false; Matrix viewBoxMatrix = Matrix::Identity(); @@ -532,8 +536,7 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr if (!textContent.empty()) { auto textSpan = std::make_unique(); - textSpan->x = x; - textSpan->y = y; + textSpan->position = {x, y}; textSpan->text = textContent; std::string fontFamily = getAttribute(element, "font-family"); @@ -893,18 +896,8 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } else if (fill.find("url(") == 0) { auto fillNode = std::make_unique(); std::string refId = resolveUrl(fill); - - // Try to inline the gradient or pattern. - auto it = _defs.find(refId); - if (it != _defs.end()) { - if (it->second->name == "linearGradient") { - fillNode->color = convertLinearGradient(it->second, shapeBounds); - } else if (it->second->name == "radialGradient") { - fillNode->color = convertRadialGradient(it->second, shapeBounds); - } else if (it->second->name == "pattern") { - fillNode->color = convertPattern(it->second, shapeBounds); - } - } + // Use getColorSourceForRef which handles reference counting. + fillNode->color = getColorSourceForRef(refId, shapeBounds); contents.push_back(std::move(fillNode)); } else { auto fillNode = std::make_unique(); @@ -919,6 +912,7 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } // Convert color to SolidColor for PAGX compatibility. + // SolidColor is always inlined (no id). Color parsedColor = parseColor(fill); auto solidColor = std::make_unique(); solidColor->color = parsedColor; @@ -948,17 +942,8 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, if (stroke.find("url(") == 0) { std::string refId = resolveUrl(stroke); - - auto it = _defs.find(refId); - if (it != _defs.end()) { - if (it->second->name == "linearGradient") { - strokeNode->color = convertLinearGradient(it->second, shapeBounds); - } else if (it->second->name == "radialGradient") { - strokeNode->color = convertRadialGradient(it->second, shapeBounds); - } else if (it->second->name == "pattern") { - strokeNode->color = convertPattern(it->second, shapeBounds); - } - } + // Use getColorSourceForRef which handles reference counting. + strokeNode->color = getColorSourceForRef(refId, shapeBounds); } else { // Determine effective stroke-opacity. std::string strokeOpacity = getAttribute(element, "stroke-opacity"); @@ -970,6 +955,7 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } // Convert color to SolidColor for PAGX compatibility. + // SolidColor is always inlined (no id). Color parsedColor = parseColor(stroke); auto solidColor = std::make_unique(); solidColor->color = parsedColor; @@ -1890,4 +1876,167 @@ void SVGParserImpl::parseCustomData(const std::shared_ptr& element, Lay } } +void SVGParserImpl::countColorSourceReferences(const std::shared_ptr& root) { + // Traverse all elements and count references to gradients/patterns. + auto child = root->getFirstChild(); + while (child) { + if (child->name != "defs") { + countColorSourceReferencesInElement(child); + } + child = child->getNextSibling(); + } +} + +void SVGParserImpl::countColorSourceReferencesInElement(const std::shared_ptr& element) { + if (!element) { + return; + } + + // Check fill attribute for url() references. + std::string fill = getAttribute(element, "fill"); + if (!fill.empty() && fill.find("url(") == 0) { + std::string refId = resolveUrl(fill); + auto it = _defs.find(refId); + if (it != _defs.end()) { + // Only count gradients and patterns, not masks/clipPaths/filters. + const auto& defName = it->second->name; + if (defName == "linearGradient" || defName == "radialGradient" || defName == "pattern") { + _colorSourceRefCount[refId]++; + } + } + } + + // Check stroke attribute for url() references. + std::string stroke = getAttribute(element, "stroke"); + if (!stroke.empty() && stroke.find("url(") == 0) { + std::string refId = resolveUrl(stroke); + auto it = _defs.find(refId); + if (it != _defs.end()) { + const auto& defName = it->second->name; + if (defName == "linearGradient" || defName == "radialGradient" || defName == "pattern") { + _colorSourceRefCount[refId]++; + } + } + } + + // Recurse into children. + auto child = element->getFirstChild(); + while (child) { + countColorSourceReferencesInElement(child); + child = child->getNextSibling(); + } +} + +std::string SVGParserImpl::generateColorSourceId() { + std::string id; + do { + id = "color" + std::to_string(_nextColorSourceId++); + } while (_existingIds.count(id) > 0); + _existingIds.insert(id); + return id; +} + +std::unique_ptr SVGParserImpl::getColorSourceForRef(const std::string& refId, + const Rect& shapeBounds) { + auto defIt = _defs.find(refId); + if (defIt == _defs.end()) { + return nullptr; + } + + const auto& defNode = defIt->second; + const auto& defName = defNode->name; + + // Check if this reference is used multiple times. + int refCount = 0; + auto countIt = _colorSourceRefCount.find(refId); + if (countIt != _colorSourceRefCount.end()) { + refCount = countIt->second; + } + + // If refCount > 1, we need to create/reuse a resource with an ID. + if (refCount > 1) { + // Check if we already created a resource for this ref. + auto idIt = _colorSourceIdMap.find(refId); + if (idIt != _colorSourceIdMap.end()) { + // Return a new ColorSource instance that references the existing resource. + // The caller should check if colorSource->id is non-empty to know it's a reference. + // We need to create a "stub" ColorSource with just the id set. + // Actually, the PAGXExporter uses colorSource->id to determine if it's a reference. + // So we create a new instance with just the id filled in. + + // For proper behavior, we create a copy of the cached ColorSource with the same id. + auto cacheIt = _colorSourceCache.find(refId); + if (cacheIt != _colorSourceCache.end()) { + // Clone the cached ColorSource + ColorSource* cached = cacheIt->second; + if (defName == "linearGradient") { + auto grad = std::make_unique(); + auto* src = static_cast(cached); + grad->id = src->id; + grad->startPoint = src->startPoint; + grad->endPoint = src->endPoint; + grad->matrix = src->matrix; + grad->colorStops = src->colorStops; + return grad; + } else if (defName == "radialGradient") { + auto grad = std::make_unique(); + auto* src = static_cast(cached); + grad->id = src->id; + grad->center = src->center; + grad->radius = src->radius; + grad->matrix = src->matrix; + grad->colorStops = src->colorStops; + return grad; + } else if (defName == "pattern") { + auto pattern = std::make_unique(); + auto* src = static_cast(cached); + pattern->id = src->id; + pattern->image = src->image; + pattern->tileModeX = src->tileModeX; + pattern->tileModeY = src->tileModeY; + pattern->sampling = src->sampling; + pattern->matrix = src->matrix; + return pattern; + } + } + return nullptr; + } + + // First time seeing this multi-referenced def, create the resource. + std::string colorSourceId = generateColorSourceId(); + _colorSourceIdMap[refId] = colorSourceId; + + std::unique_ptr colorSource; + if (defName == "linearGradient") { + colorSource = convertLinearGradient(defNode, shapeBounds); + } else if (defName == "radialGradient") { + colorSource = convertRadialGradient(defNode, shapeBounds); + } else if (defName == "pattern") { + colorSource = convertPattern(defNode, shapeBounds); + } + + if (colorSource) { + colorSource->id = colorSourceId; + // Cache the raw pointer before moving to resources. + _colorSourceCache[refId] = colorSource.get(); + _document->resources.push_back(std::move(colorSource)); + + // Now return a clone with the same id (as a reference). + return getColorSourceForRef(refId, shapeBounds); + } + return nullptr; + } + + // refCount <= 1: inline the ColorSource (no id). + if (defName == "linearGradient") { + return convertLinearGradient(defNode, shapeBounds); + } else if (defName == "radialGradient") { + return convertRadialGradient(defNode, shapeBounds); + } else if (defName == "pattern") { + return convertPattern(defNode, shapeBounds); + } + + return nullptr; +} + } // namespace pagx diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 6e0857cd4a..69442cfd3e 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -466,7 +466,7 @@ class LayerBuilderImpl { textSpan->setTextBlob(textBlob); } - textSpan->setPosition(tgfx::Point::Make(node->x, node->y)); + textSpan->setPosition(tgfx::Point::Make(node->position.x, node->position.y)); return textSpan; } @@ -774,10 +774,10 @@ class LayerBuilderImpl { break; } - tgfxTextSpan->setPosition(tgfx::Point::Make(span->x + xOffset, span->y)); + tgfxTextSpan->setPosition(tgfx::Point::Make(span->position.x + xOffset, span->position.y)); } else { // No blob, use original position. - tgfxTextSpan->setPosition(tgfx::Point::Make(span->x, span->y)); + tgfxTextSpan->setPosition(tgfx::Point::Make(span->position.x, span->position.y)); } elements.push_back(tgfxTextSpan); From 9248575e6213b29d3976e15e3afc22218dd14c60 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 22:13:59 +0800 Subject: [PATCH 108/678] Fix compilation errors in PAGXExporter for Color toHexString and TextSpan position. --- pagx/src/PAGXExporter.cpp | 977 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 977 insertions(+) create mode 100644 pagx/src/PAGXExporter.cpp diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp new file mode 100644 index 0000000000..de144d7edf --- /dev/null +++ b/pagx/src/PAGXExporter.cpp @@ -0,0 +1,977 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PAGXExporter.h" +#include +#include +#include "PAGXEnumUtils.h" +#include "pagx/model/BackgroundBlurStyle.h" +#include "pagx/model/BlendFilter.h" +#include "pagx/model/BlurFilter.h" +#include "pagx/model/ColorMatrixFilter.h" +#include "pagx/model/Composition.h" +#include "pagx/model/ConicGradient.h" +#include "pagx/model/DiamondGradient.h" +#include "pagx/model/Document.h" +#include "pagx/model/DropShadowFilter.h" +#include "pagx/model/DropShadowStyle.h" +#include "pagx/model/Ellipse.h" +#include "pagx/model/Fill.h" +#include "pagx/model/Group.h" +#include "pagx/model/Image.h" +#include "pagx/model/ImagePattern.h" +#include "pagx/model/InnerShadowFilter.h" +#include "pagx/model/InnerShadowStyle.h" +#include "pagx/model/LinearGradient.h" +#include "pagx/model/MergePath.h" +#include "pagx/model/Path.h" +#include "pagx/model/Polystar.h" +#include "pagx/model/RadialGradient.h" +#include "pagx/model/RangeSelector.h" +#include "pagx/model/Rectangle.h" +#include "pagx/model/Repeater.h" +#include "pagx/model/RoundCorner.h" +#include "pagx/model/SolidColor.h" +#include "pagx/model/Stroke.h" +#include "pagx/model/TextLayout.h" +#include "pagx/model/TextModifier.h" +#include "pagx/model/TextPath.h" +#include "pagx/model/TextSpan.h" +#include "pagx/model/TrimPath.h" + +namespace pagx { + +//============================================================================== +// XMLBuilder - XML generation helper +//============================================================================== + +class XMLBuilder { + public: + void appendDeclaration() { + buffer << "\n"; + } + + void openElement(const std::string& tag) { + writeIndent(); + buffer << "<" << tag; + tagStack.push_back(tag); + } + + void addAttribute(const std::string& name, const std::string& value) { + if (!value.empty()) { + buffer << " " << name << "=\"" << escapeXML(value) << "\""; + } + } + + void addAttribute(const std::string& name, float value, float defaultValue = 0) { + if (value != defaultValue) { + buffer << " " << name << "=\"" << formatFloat(value) << "\""; + } + } + + void addRequiredAttribute(const std::string& name, float value) { + buffer << " " << name << "=\"" << formatFloat(value) << "\""; + } + + void addRequiredAttribute(const std::string& name, const std::string& value) { + buffer << " " << name << "=\"" << escapeXML(value) << "\""; + } + + void addAttribute(const std::string& name, int value, int defaultValue = 0) { + if (value != defaultValue) { + buffer << " " << name << "=\"" << value << "\""; + } + } + + void addAttribute(const std::string& name, bool value, bool defaultValue = false) { + if (value != defaultValue) { + buffer << " " << name << "=\"" << (value ? "true" : "false") << "\""; + } + } + + void closeElementStart() { + buffer << ">\n"; + indentLevel++; + } + + void closeElementSelfClosing() { + buffer << "/>\n"; + tagStack.pop_back(); + } + + void closeElement() { + indentLevel--; + writeIndent(); + buffer << "\n"; + tagStack.pop_back(); + } + + void addTextContent(const std::string& text) { + buffer << ""; + } + + std::string str() const { + return buffer.str(); + } + + private: + std::ostringstream buffer = {}; + std::vector tagStack = {}; + int indentLevel = 0; + + void writeIndent() { + for (int i = 0; i < indentLevel; i++) { + buffer << " "; + } + } + + static std::string escapeXML(const std::string& str) { + std::string result = {}; + for (char c : str) { + switch (c) { + case '<': + result += "<"; + break; + case '>': + result += ">"; + break; + case '&': + result += "&"; + break; + case '"': + result += """; + break; + case '\'': + result += "'"; + break; + default: + result += c; + break; + } + } + return result; + } + + static std::string formatFloat(float value) { + std::ostringstream oss = {}; + oss << value; + auto str = oss.str(); + if (str.find('.') != std::string::npos) { + while (str.back() == '0') { + str.pop_back(); + } + if (str.back() == '.') { + str.pop_back(); + } + } + return str; + } +}; + +//============================================================================== +// Helper functions for converting types to strings +//============================================================================== + +static std::string pointToString(const Point& p) { + std::ostringstream oss = {}; + oss << p.x << "," << p.y; + return oss.str(); +} + +static std::string sizeToString(const Size& s) { + std::ostringstream oss = {}; + oss << s.width << "," << s.height; + return oss.str(); +} + +static std::string rectToString(const Rect& r) { + std::ostringstream oss = {}; + oss << r.x << "," << r.y << "," << r.width << "," << r.height; + return oss.str(); +} + +static std::string floatListToString(const std::vector& values) { + std::ostringstream oss = {}; + for (size_t i = 0; i < values.size(); i++) { + if (i > 0) { + oss << ","; + } + oss << values[i]; + } + return oss.str(); +} + +static std::string colorToHexString(const Color& color, bool includeAlpha = false) { + auto toHex = [](float value) { + int v = static_cast(std::round(value * 255)); + v = std::max(0, std::min(255, v)); + char hex[3]; + snprintf(hex, sizeof(hex), "%02X", v); + return std::string(hex); + }; + std::string result = "#" + toHex(color.red) + toHex(color.green) + toHex(color.blue); + if (includeAlpha && color.alpha < 1.0f) { + result += toHex(color.alpha); + } + return result; +} + +//============================================================================== +// Forward declarations +//============================================================================== + +static void writeColorSource(XMLBuilder& xml, const ColorSource* node); +static void writeVectorElement(XMLBuilder& xml, const Element* node); +static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node); +static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node); +static void writeResource(XMLBuilder& xml, const Node* node); +static void writeLayer(XMLBuilder& xml, const Layer* node); + +//============================================================================== +// ColorStop and ColorSource writing +//============================================================================== + +static void writeColorStops(XMLBuilder& xml, const std::vector& stops) { + for (const auto& stop : stops) { + xml.openElement("ColorStop"); + xml.addRequiredAttribute("offset", stop.offset); + xml.addRequiredAttribute("color", colorToHexString(stop.color, stop.color.alpha < 1.0f)); + xml.closeElementSelfClosing(); + } +} + +static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { + switch (node->type()) { + case ColorSourceType::SolidColor: { + auto solid = static_cast(node); + xml.openElement("SolidColor"); + xml.addAttribute("id", solid->id); + xml.addAttribute("color", colorToHexString(solid->color, solid->color.alpha < 1.0f)); + xml.closeElementSelfClosing(); + break; + } + case ColorSourceType::LinearGradient: { + auto grad = static_cast(node); + xml.openElement("LinearGradient"); + xml.addAttribute("id", grad->id); + if (grad->startPoint.x != 0 || grad->startPoint.y != 0) { + xml.addAttribute("startPoint", pointToString(grad->startPoint)); + } + xml.addRequiredAttribute("endPoint", pointToString(grad->endPoint)); + if (!grad->matrix.isIdentity()) { + xml.addAttribute("matrix", grad->matrix.toString()); + } + if (grad->colorStops.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + writeColorStops(xml, grad->colorStops); + xml.closeElement(); + } + break; + } + case ColorSourceType::RadialGradient: { + auto grad = static_cast(node); + xml.openElement("RadialGradient"); + xml.addAttribute("id", grad->id); + if (grad->center.x != 0 || grad->center.y != 0) { + xml.addAttribute("center", pointToString(grad->center)); + } + xml.addRequiredAttribute("radius", grad->radius); + if (!grad->matrix.isIdentity()) { + xml.addAttribute("matrix", grad->matrix.toString()); + } + if (grad->colorStops.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + writeColorStops(xml, grad->colorStops); + xml.closeElement(); + } + break; + } + case ColorSourceType::ConicGradient: { + auto grad = static_cast(node); + xml.openElement("ConicGradient"); + xml.addAttribute("id", grad->id); + if (grad->center.x != 0 || grad->center.y != 0) { + xml.addAttribute("center", pointToString(grad->center)); + } + xml.addAttribute("startAngle", grad->startAngle); + xml.addAttribute("endAngle", grad->endAngle, 360.0f); + if (!grad->matrix.isIdentity()) { + xml.addAttribute("matrix", grad->matrix.toString()); + } + if (grad->colorStops.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + writeColorStops(xml, grad->colorStops); + xml.closeElement(); + } + break; + } + case ColorSourceType::DiamondGradient: { + auto grad = static_cast(node); + xml.openElement("DiamondGradient"); + xml.addAttribute("id", grad->id); + if (grad->center.x != 0 || grad->center.y != 0) { + xml.addAttribute("center", pointToString(grad->center)); + } + xml.addRequiredAttribute("halfDiagonal", grad->halfDiagonal); + if (!grad->matrix.isIdentity()) { + xml.addAttribute("matrix", grad->matrix.toString()); + } + if (grad->colorStops.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + writeColorStops(xml, grad->colorStops); + xml.closeElement(); + } + break; + } + case ColorSourceType::ImagePattern: { + auto pattern = static_cast(node); + xml.openElement("ImagePattern"); + xml.addAttribute("id", pattern->id); + xml.addAttribute("image", pattern->image); + if (pattern->tileModeX != TileMode::Clamp) { + xml.addAttribute("tileModeX", TileModeToString(pattern->tileModeX)); + } + if (pattern->tileModeY != TileMode::Clamp) { + xml.addAttribute("tileModeY", TileModeToString(pattern->tileModeY)); + } + if (pattern->sampling != SamplingMode::Linear) { + xml.addAttribute("sampling", SamplingModeToString(pattern->sampling)); + } + if (!pattern->matrix.isIdentity()) { + xml.addAttribute("matrix", pattern->matrix.toString()); + } + xml.closeElementSelfClosing(); + break; + } + } +} + +//============================================================================== +// VectorElement writing +//============================================================================== + +static void writeVectorElement(XMLBuilder& xml, const Element* node) { + switch (node->type()) { + case ElementType::Rectangle: { + auto rect = static_cast(node); + xml.openElement("Rectangle"); + if (rect->center.x != 0 || rect->center.y != 0) { + xml.addAttribute("center", pointToString(rect->center)); + } + if (rect->size.width != 100 || rect->size.height != 100) { + xml.addAttribute("size", sizeToString(rect->size)); + } + xml.addAttribute("roundness", rect->roundness); + xml.addAttribute("reversed", rect->reversed); + xml.closeElementSelfClosing(); + break; + } + case ElementType::Ellipse: { + auto ellipse = static_cast(node); + xml.openElement("Ellipse"); + if (ellipse->center.x != 0 || ellipse->center.y != 0) { + xml.addAttribute("center", pointToString(ellipse->center)); + } + if (ellipse->size.width != 100 || ellipse->size.height != 100) { + xml.addAttribute("size", sizeToString(ellipse->size)); + } + xml.addAttribute("reversed", ellipse->reversed); + xml.closeElementSelfClosing(); + break; + } + case ElementType::Polystar: { + auto polystar = static_cast(node); + xml.openElement("Polystar"); + if (polystar->center.x != 0 || polystar->center.y != 0) { + xml.addAttribute("center", pointToString(polystar->center)); + } + xml.addAttribute("polystarType", PolystarTypeToString(polystar->polystarType)); + xml.addAttribute("pointCount", polystar->pointCount, 5.0f); + xml.addAttribute("outerRadius", polystar->outerRadius, 100.0f); + xml.addAttribute("innerRadius", polystar->innerRadius, 50.0f); + xml.addAttribute("rotation", polystar->rotation); + xml.addAttribute("outerRoundness", polystar->outerRoundness); + xml.addAttribute("innerRoundness", polystar->innerRoundness); + xml.addAttribute("reversed", polystar->reversed); + xml.closeElementSelfClosing(); + break; + } + case ElementType::Path: { + auto path = static_cast(node); + xml.openElement("Path"); + if (!path->data.isEmpty()) { + // Check if pathData has a reference (id starts with #) + if (!path->dataRef.empty()) { + xml.addAttribute("data", path->dataRef); + } else { + // Inline the path data + xml.addAttribute("data", path->data.toSVGString()); + } + } + xml.addAttribute("reversed", path->reversed); + xml.closeElementSelfClosing(); + break; + } + case ElementType::TextSpan: { + auto text = static_cast(node); + xml.openElement("TextSpan"); + if (text->position.x != 0 || text->position.y != 0) { + xml.addAttribute("position", pointToString(text->position)); + } + xml.addAttribute("font", text->font); + xml.addAttribute("fontSize", text->fontSize, 12.0f); + xml.addAttribute("fontWeight", text->fontWeight, 400); + if (text->fontStyle != "normal" && !text->fontStyle.empty()) { + xml.addAttribute("fontStyle", text->fontStyle); + } + xml.addAttribute("tracking", text->tracking); + xml.addAttribute("baselineShift", text->baselineShift); + xml.closeElementStart(); + xml.addTextContent(text->text); + xml.closeElement(); + break; + } + case ElementType::Fill: { + auto fill = static_cast(node); + xml.openElement("Fill"); + // If colorSource has an id, output reference; otherwise inline the colorSource below + if (fill->color && !fill->color->id.empty()) { + // Reference by id + xml.addAttribute("color", "#" + fill->color->id); + } + xml.addAttribute("alpha", fill->alpha, 1.0f); + if (fill->blendMode != BlendMode::Normal) { + xml.addAttribute("blendMode", BlendModeToString(fill->blendMode)); + } + if (fill->fillRule != FillRule::Winding) { + xml.addAttribute("fillRule", FillRuleToString(fill->fillRule)); + } + if (fill->placement != LayerPlacement::Background) { + xml.addAttribute("placement", LayerPlacementToString(fill->placement)); + } + // Inline ColorSource if it doesn't have an id + if (fill->color && fill->color->id.empty()) { + xml.closeElementStart(); + writeColorSource(xml, fill->color.get()); + xml.closeElement(); + } else { + xml.closeElementSelfClosing(); + } + break; + } + case ElementType::Stroke: { + auto stroke = static_cast(node); + xml.openElement("Stroke"); + // If colorSource has an id, output reference; otherwise inline the colorSource below + if (stroke->color && !stroke->color->id.empty()) { + // Reference by id + xml.addAttribute("color", "#" + stroke->color->id); + } + xml.addAttribute("width", stroke->width, 1.0f); + xml.addAttribute("alpha", stroke->alpha, 1.0f); + if (stroke->blendMode != BlendMode::Normal) { + xml.addAttribute("blendMode", BlendModeToString(stroke->blendMode)); + } + if (stroke->cap != LineCap::Butt) { + xml.addAttribute("cap", LineCapToString(stroke->cap)); + } + if (stroke->join != LineJoin::Miter) { + xml.addAttribute("join", LineJoinToString(stroke->join)); + } + xml.addAttribute("miterLimit", stroke->miterLimit, 4.0f); + if (!stroke->dashes.empty()) { + xml.addAttribute("dashes", floatListToString(stroke->dashes)); + } + xml.addAttribute("dashOffset", stroke->dashOffset); + if (stroke->align != StrokeAlign::Center) { + xml.addAttribute("align", StrokeAlignToString(stroke->align)); + } + if (stroke->placement != LayerPlacement::Background) { + xml.addAttribute("placement", LayerPlacementToString(stroke->placement)); + } + // Inline ColorSource if it doesn't have an id + if (stroke->color && stroke->color->id.empty()) { + xml.closeElementStart(); + writeColorSource(xml, stroke->color.get()); + xml.closeElement(); + } else { + xml.closeElementSelfClosing(); + } + break; + } + case ElementType::TrimPath: { + auto trim = static_cast(node); + xml.openElement("TrimPath"); + xml.addAttribute("start", trim->start); + xml.addAttribute("end", trim->end, 1.0f); + xml.addAttribute("offset", trim->offset); + if (trim->trimType != TrimType::Separate) { + xml.addAttribute("type", TrimTypeToString(trim->trimType)); + } + xml.closeElementSelfClosing(); + break; + } + case ElementType::RoundCorner: { + auto round = static_cast(node); + xml.openElement("RoundCorner"); + xml.addAttribute("radius", round->radius, 10.0f); + xml.closeElementSelfClosing(); + break; + } + case ElementType::MergePath: { + auto merge = static_cast(node); + xml.openElement("MergePath"); + if (merge->mode != MergePathMode::Append) { + xml.addAttribute("mode", MergePathModeToString(merge->mode)); + } + xml.closeElementSelfClosing(); + break; + } + case ElementType::TextModifier: { + auto modifier = static_cast(node); + xml.openElement("TextModifier"); + if (modifier->anchorPoint.x != 0 || modifier->anchorPoint.y != 0) { + xml.addAttribute("anchorPoint", pointToString(modifier->anchorPoint)); + } + if (modifier->position.x != 0 || modifier->position.y != 0) { + xml.addAttribute("position", pointToString(modifier->position)); + } + xml.addAttribute("rotation", modifier->rotation); + if (modifier->scale.x != 1 || modifier->scale.y != 1) { + xml.addAttribute("scale", pointToString(modifier->scale)); + } + xml.addAttribute("skew", modifier->skew); + xml.addAttribute("skewAxis", modifier->skewAxis); + xml.addAttribute("alpha", modifier->alpha, 1.0f); + xml.addAttribute("fillColor", modifier->fillColor); + xml.addAttribute("strokeColor", modifier->strokeColor); + if (modifier->strokeWidth >= 0) { + xml.addAttribute("strokeWidth", modifier->strokeWidth); + } + if (modifier->selectors.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + for (const auto& selector : modifier->selectors) { + if (selector->type() != TextSelectorType::RangeSelector) { + continue; + } + auto rangeSelector = static_cast(selector.get()); + xml.openElement("RangeSelector"); + xml.addAttribute("start", rangeSelector->start); + xml.addAttribute("end", rangeSelector->end, 1.0f); + xml.addAttribute("offset", rangeSelector->offset); + if (rangeSelector->unit != SelectorUnit::Percentage) { + xml.addAttribute("unit", SelectorUnitToString(rangeSelector->unit)); + } + if (rangeSelector->shape != SelectorShape::Square) { + xml.addAttribute("shape", SelectorShapeToString(rangeSelector->shape)); + } + xml.addAttribute("easeIn", rangeSelector->easeIn); + xml.addAttribute("easeOut", rangeSelector->easeOut); + if (rangeSelector->mode != SelectorMode::Add) { + xml.addAttribute("mode", SelectorModeToString(rangeSelector->mode)); + } + xml.addAttribute("weight", rangeSelector->weight, 1.0f); + xml.addAttribute("randomizeOrder", rangeSelector->randomizeOrder); + xml.addAttribute("randomSeed", rangeSelector->randomSeed); + xml.closeElementSelfClosing(); + } + xml.closeElement(); + } + break; + } + case ElementType::TextPath: { + auto textPath = static_cast(node); + xml.openElement("TextPath"); + xml.addAttribute("path", textPath->path); + if (textPath->textAlign != TextAlign::Start) { + xml.addAttribute("textAlign", TextAlignToString(textPath->textAlign)); + } + xml.addAttribute("firstMargin", textPath->firstMargin); + xml.addAttribute("lastMargin", textPath->lastMargin); + xml.addAttribute("perpendicularToPath", textPath->perpendicularToPath, true); + xml.addAttribute("reversed", textPath->reversed); + xml.closeElementSelfClosing(); + break; + } + case ElementType::TextLayout: { + auto layout = static_cast(node); + xml.openElement("TextLayout"); + xml.addAttribute("x", layout->x); + xml.addAttribute("y", layout->y); + xml.addAttribute("width", layout->width); + xml.addAttribute("height", layout->height); + if (layout->textAlign != TextAlign::Start) { + xml.addAttribute("textAlign", TextAlignToString(layout->textAlign)); + } + if (layout->textAlignLast != TextAlign::Start) { + xml.addAttribute("textAlignLast", TextAlignToString(layout->textAlignLast)); + } + if (layout->verticalAlign != VerticalAlign::Top) { + xml.addAttribute("verticalAlign", VerticalAlignToString(layout->verticalAlign)); + } + xml.addAttribute("lineHeight", layout->lineHeight, 1.2f); + if (layout->direction != TextDirection::Horizontal) { + xml.addAttribute("direction", TextDirectionToString(layout->direction)); + } + xml.closeElementSelfClosing(); + break; + } + case ElementType::Repeater: { + auto repeater = static_cast(node); + xml.openElement("Repeater"); + xml.addAttribute("copies", repeater->copies, 3.0f); + xml.addAttribute("offset", repeater->offset); + if (repeater->order != RepeaterOrder::BelowOriginal) { + xml.addAttribute("order", RepeaterOrderToString(repeater->order)); + } + if (repeater->anchorPoint.x != 0 || repeater->anchorPoint.y != 0) { + xml.addAttribute("anchorPoint", pointToString(repeater->anchorPoint)); + } + if (repeater->position.x != 100 || repeater->position.y != 100) { + xml.addAttribute("position", pointToString(repeater->position)); + } + xml.addAttribute("rotation", repeater->rotation); + if (repeater->scale.x != 1 || repeater->scale.y != 1) { + xml.addAttribute("scale", pointToString(repeater->scale)); + } + xml.addAttribute("startAlpha", repeater->startAlpha, 1.0f); + xml.addAttribute("endAlpha", repeater->endAlpha, 1.0f); + xml.closeElementSelfClosing(); + break; + } + case ElementType::Group: { + auto group = static_cast(node); + xml.openElement("Group"); + if (group->anchorPoint.x != 0 || group->anchorPoint.y != 0) { + xml.addAttribute("anchorPoint", pointToString(group->anchorPoint)); + } + if (group->position.x != 0 || group->position.y != 0) { + xml.addAttribute("position", pointToString(group->position)); + } + xml.addAttribute("rotation", group->rotation); + if (group->scale.x != 1 || group->scale.y != 1) { + xml.addAttribute("scale", pointToString(group->scale)); + } + xml.addAttribute("skew", group->skew); + xml.addAttribute("skewAxis", group->skewAxis); + xml.addAttribute("alpha", group->alpha, 1.0f); + if (group->elements.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + for (const auto& element : group->elements) { + writeVectorElement(xml, element.get()); + } + xml.closeElement(); + } + break; + } + default: + break; + } +} + +//============================================================================== +// LayerStyle writing +//============================================================================== + +static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { + switch (node->type()) { + case LayerStyleType::DropShadowStyle: { + auto style = static_cast(node); + xml.openElement("DropShadowStyle"); + if (style->blendMode != BlendMode::Normal) { + xml.addAttribute("blendMode", BlendModeToString(style->blendMode)); + } + xml.addAttribute("offsetX", style->offsetX); + xml.addAttribute("offsetY", style->offsetY); + xml.addAttribute("blurrinessX", style->blurrinessX); + xml.addAttribute("blurrinessY", style->blurrinessY); + xml.addAttribute("color", colorToHexString(style->color, style->color.alpha < 1.0f)); + xml.addAttribute("showBehindLayer", style->showBehindLayer, true); + xml.closeElementSelfClosing(); + break; + } + case LayerStyleType::InnerShadowStyle: { + auto style = static_cast(node); + xml.openElement("InnerShadowStyle"); + if (style->blendMode != BlendMode::Normal) { + xml.addAttribute("blendMode", BlendModeToString(style->blendMode)); + } + xml.addAttribute("offsetX", style->offsetX); + xml.addAttribute("offsetY", style->offsetY); + xml.addAttribute("blurrinessX", style->blurrinessX); + xml.addAttribute("blurrinessY", style->blurrinessY); + xml.addAttribute("color", colorToHexString(style->color, style->color.alpha < 1.0f)); + xml.closeElementSelfClosing(); + break; + } + case LayerStyleType::BackgroundBlurStyle: { + auto style = static_cast(node); + xml.openElement("BackgroundBlurStyle"); + if (style->blendMode != BlendMode::Normal) { + xml.addAttribute("blendMode", BlendModeToString(style->blendMode)); + } + xml.addAttribute("blurrinessX", style->blurrinessX); + xml.addAttribute("blurrinessY", style->blurrinessY); + if (style->tileMode != TileMode::Mirror) { + xml.addAttribute("tileMode", TileModeToString(style->tileMode)); + } + xml.closeElementSelfClosing(); + break; + } + default: + break; + } +} + +//============================================================================== +// LayerFilter writing +//============================================================================== + +static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { + switch (node->type()) { + case LayerFilterType::BlurFilter: { + auto filter = static_cast(node); + xml.openElement("BlurFilter"); + xml.addRequiredAttribute("blurrinessX", filter->blurrinessX); + xml.addRequiredAttribute("blurrinessY", filter->blurrinessY); + if (filter->tileMode != TileMode::Decal) { + xml.addAttribute("tileMode", TileModeToString(filter->tileMode)); + } + xml.closeElementSelfClosing(); + break; + } + case LayerFilterType::DropShadowFilter: { + auto filter = static_cast(node); + xml.openElement("DropShadowFilter"); + xml.addAttribute("offsetX", filter->offsetX); + xml.addAttribute("offsetY", filter->offsetY); + xml.addAttribute("blurrinessX", filter->blurrinessX); + xml.addAttribute("blurrinessY", filter->blurrinessY); + xml.addAttribute("color", colorToHexString(filter->color, filter->color.alpha < 1.0f)); + xml.addAttribute("shadowOnly", filter->shadowOnly); + xml.closeElementSelfClosing(); + break; + } + case LayerFilterType::InnerShadowFilter: { + auto filter = static_cast(node); + xml.openElement("InnerShadowFilter"); + xml.addAttribute("offsetX", filter->offsetX); + xml.addAttribute("offsetY", filter->offsetY); + xml.addAttribute("blurrinessX", filter->blurrinessX); + xml.addAttribute("blurrinessY", filter->blurrinessY); + xml.addAttribute("color", colorToHexString(filter->color, filter->color.alpha < 1.0f)); + xml.addAttribute("shadowOnly", filter->shadowOnly); + xml.closeElementSelfClosing(); + break; + } + case LayerFilterType::BlendFilter: { + auto filter = static_cast(node); + xml.openElement("BlendFilter"); + xml.addAttribute("color", colorToHexString(filter->color, filter->color.alpha < 1.0f)); + if (filter->blendMode != BlendMode::Normal) { + xml.addAttribute("blendMode", BlendModeToString(filter->blendMode)); + } + xml.closeElementSelfClosing(); + break; + } + case LayerFilterType::ColorMatrixFilter: { + auto filter = static_cast(node); + xml.openElement("ColorMatrixFilter"); + std::vector values(filter->matrix.begin(), filter->matrix.end()); + xml.addAttribute("matrix", floatListToString(values)); + xml.closeElementSelfClosing(); + break; + } + default: + break; + } +} + +//============================================================================== +// Resource writing +//============================================================================== + +static void writeResource(XMLBuilder& xml, const Node* node) { + switch (node->nodeType()) { + case NodeType::Image: { + auto image = static_cast(node); + xml.openElement("Image"); + xml.addAttribute("id", image->id); + xml.addAttribute("source", image->source); + xml.closeElementSelfClosing(); + break; + } + case NodeType::PathData: { + // PathData resources are not currently supported as separate Node type. + // Path data is stored inline in Path elements. + break; + } + case NodeType::Composition: { + auto comp = static_cast(node); + xml.openElement("Composition"); + xml.addAttribute("id", comp->id); + xml.addRequiredAttribute("width", static_cast(comp->width)); + xml.addRequiredAttribute("height", static_cast(comp->height)); + if (comp->layers.empty()) { + xml.closeElementSelfClosing(); + } else { + xml.closeElementStart(); + for (const auto& layer : comp->layers) { + writeLayer(xml, layer.get()); + } + xml.closeElement(); + } + break; + } + case NodeType::SolidColor: + case NodeType::LinearGradient: + case NodeType::RadialGradient: + case NodeType::ConicGradient: + case NodeType::DiamondGradient: + case NodeType::ImagePattern: { + writeColorSource(xml, static_cast(node)); + break; + } + default: + break; + } +} + +//============================================================================== +// Layer writing +//============================================================================== + +static void writeLayer(XMLBuilder& xml, const Layer* node) { + xml.openElement("Layer"); + if (!node->id.empty()) { + xml.addAttribute("id", node->id); + } + xml.addAttribute("name", node->name); + xml.addAttribute("visible", node->visible, true); + xml.addAttribute("alpha", node->alpha, 1.0f); + if (node->blendMode != BlendMode::Normal) { + xml.addAttribute("blendMode", BlendModeToString(node->blendMode)); + } + xml.addAttribute("x", node->x); + xml.addAttribute("y", node->y); + if (!node->matrix.isIdentity()) { + xml.addAttribute("matrix", node->matrix.toString()); + } + if (!node->matrix3D.empty()) { + xml.addAttribute("matrix3D", floatListToString(node->matrix3D)); + } + xml.addAttribute("preserve3D", node->preserve3D); + xml.addAttribute("antiAlias", node->antiAlias, true); + xml.addAttribute("groupOpacity", node->groupOpacity); + xml.addAttribute("passThroughBackground", node->passThroughBackground, true); + xml.addAttribute("excludeChildEffectsInLayerStyle", node->excludeChildEffectsInLayerStyle); + if (node->hasScrollRect) { + xml.addAttribute("scrollRect", rectToString(node->scrollRect)); + } + xml.addAttribute("mask", node->mask); + if (node->maskType != MaskType::Alpha) { + xml.addAttribute("maskType", MaskTypeToString(node->maskType)); + } + xml.addAttribute("composition", node->composition); + + // Write custom data as data-* attributes. + for (const auto& [key, value] : node->customData) { + xml.addAttribute("data-" + key, value); + } + + bool hasChildren = !node->contents.empty() || !node->styles.empty() || !node->filters.empty() || + !node->children.empty(); + if (!hasChildren) { + xml.closeElementSelfClosing(); + return; + } + + xml.closeElementStart(); + + // Write VectorElement (contents) directly without container node. + for (const auto& element : node->contents) { + writeVectorElement(xml, element.get()); + } + + // Write LayerStyle (styles) directly without container node. + for (const auto& style : node->styles) { + writeLayerStyle(xml, style.get()); + } + + // Write LayerFilter (filters) directly without container node. + for (const auto& filter : node->filters) { + writeLayerFilter(xml, filter.get()); + } + + // Write child Layers. + for (const auto& child : node->children) { + writeLayer(xml, child.get()); + } + + xml.closeElement(); +} + +//============================================================================== +// Main Export function +//============================================================================== + +std::string PAGXExporter::Export(const Document& doc) { + XMLBuilder xml = {}; + xml.appendDeclaration(); + + xml.openElement("pagx"); + xml.addAttribute("version", doc.version); + xml.addAttribute("width", doc.width); + xml.addAttribute("height", doc.height); + xml.closeElementStart(); + + // Write Layers first (for better readability) + for (const auto& layer : doc.layers) { + writeLayer(xml, layer.get()); + } + + // Write Resources section at the end (only if there are resources) + if (!doc.resources.empty()) { + xml.openElement("Resources"); + xml.closeElementStart(); + + for (const auto& resource : doc.resources) { + writeResource(xml, resource.get()); + } + + xml.closeElement(); + } + + xml.closeElement(); + + return xml.str(); +} + +} // namespace pagx From 6759649fd76af4724977b40775231f36d2f8bada Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 22:27:54 +0800 Subject: [PATCH 109/678] Rename Placement.h to LayerPlacement.h and remove duplicate colorToHexString function. --- pagx/include/pagx/model/Fill.h | 23 ++- pagx/include/pagx/model/Polystar.h | 4 - pagx/include/pagx/model/Stroke.h | 25 +-- .../types/{Placement.h => LayerPlacement.h} | 5 - pagx/src/PAGXEnumUtils.h | 166 ++++++++++++++++++ pagx/src/PAGXExporter.cpp | 31 +--- 6 files changed, 193 insertions(+), 61 deletions(-) rename pagx/include/pagx/model/types/{Placement.h => LayerPlacement.h} (88%) create mode 100644 pagx/src/PAGXEnumUtils.h diff --git a/pagx/include/pagx/model/Fill.h b/pagx/include/pagx/model/Fill.h index f3c257adb7..ea84f0d80a 100644 --- a/pagx/include/pagx/model/Fill.h +++ b/pagx/include/pagx/model/Fill.h @@ -23,7 +23,7 @@ #include "pagx/model/ColorSource.h" #include "pagx/model/Element.h" #include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Placement.h" +#include "pagx/model/types/LayerPlacement.h" namespace pagx { @@ -35,27 +35,24 @@ enum class FillRule { EvenOdd }; -std::string FillRuleToString(FillRule rule); -FillRule FillRuleFromString(const std::string& str); - /** - * Fill represents a fill painter that fills shapes with a solid color, gradient, or pattern. The - * color can be specified as a simple color string (e.g., "#FF0000"), a reference to a defined - * color source (e.g., "#gradientId"), or an inline ColorSource node. + * Fill represents a fill painter that fills shapes with a solid color, gradient, or pattern. + * The color is specified through a ColorSource node (SolidColor, LinearGradient, etc.) or + * a reference to a defined color source (e.g., "@gradientId"). */ class Fill : public Element { public: /** - * The fill color as a string. Can be a hex color (e.g., "#FF0000"), a reference to a color - * source (e.g., "#gradientId"), or empty if colorSource is used. + * The color source for this fill. Can be a SolidColor, LinearGradient, RadialGradient, + * ConicGradient, DiamondGradient, or ImagePattern. If null, uses the colorRef reference. */ - std::string color = {}; + std::unique_ptr color = nullptr; /** - * An inline color source node (SolidColor, LinearGradient, etc.) for complex fills. If provided, - * this takes precedence over the color string. + * A reference to a color source defined in Resources (e.g., "@gradientId"). + * Only used if color is null. */ - std::unique_ptr colorSource = nullptr; + std::string colorRef = {}; /** * The opacity of the fill, ranging from 0 (transparent) to 1 (opaque). The default value is 1. diff --git a/pagx/include/pagx/model/Polystar.h b/pagx/include/pagx/model/Polystar.h index fe98727d85..c0e47e64d7 100644 --- a/pagx/include/pagx/model/Polystar.h +++ b/pagx/include/pagx/model/Polystar.h @@ -18,7 +18,6 @@ #pragma once -#include #include "pagx/model/Element.h" #include "pagx/model/types/Point.h" @@ -32,9 +31,6 @@ enum class PolystarType { Star }; -std::string PolystarTypeToString(PolystarType type); -PolystarType PolystarTypeFromString(const std::string& str); - /** * Polystar represents a polygon or star shape with configurable points, radii, and roundness. */ diff --git a/pagx/include/pagx/model/Stroke.h b/pagx/include/pagx/model/Stroke.h index cee9cbaa45..4308d9bd9f 100644 --- a/pagx/include/pagx/model/Stroke.h +++ b/pagx/include/pagx/model/Stroke.h @@ -24,7 +24,7 @@ #include "pagx/model/ColorSource.h" #include "pagx/model/Element.h" #include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Placement.h" +#include "pagx/model/types/LayerPlacement.h" namespace pagx { @@ -37,9 +37,6 @@ enum class LineCap { Square }; -std::string LineCapToString(LineCap cap); -LineCap LineCapFromString(const std::string& str); - /** * Line join styles for strokes. */ @@ -49,9 +46,6 @@ enum class LineJoin { Bevel }; -std::string LineJoinToString(LineJoin join); -LineJoin LineJoinFromString(const std::string& str); - /** * Stroke alignment relative to path. */ @@ -61,26 +55,25 @@ enum class StrokeAlign { Outside }; -std::string StrokeAlignToString(StrokeAlign align); -StrokeAlign StrokeAlignFromString(const std::string& str); - /** * Stroke represents a stroke painter that outlines shapes with a solid color, gradient, or * pattern. It supports various line cap styles, join styles, dash patterns, and stroke alignment. + * The color is specified through a ColorSource node (SolidColor, LinearGradient, etc.) or + * a reference to a defined color source (e.g., "@gradientId"). */ class Stroke : public Element { public: /** - * The stroke color as a string. Can be a hex color (e.g., "#FF0000"), a reference to a color - * source (e.g., "#gradientId"), or empty if colorSource is used. + * The color source for this stroke. Can be a SolidColor, LinearGradient, RadialGradient, + * ConicGradient, DiamondGradient, or ImagePattern. If null, uses the colorRef reference. */ - std::string color = {}; + std::unique_ptr color = nullptr; /** - * An inline color source node (SolidColor, LinearGradient, etc.) for complex strokes. If - * provided, this takes precedence over the color string. + * A reference to a color source defined in Resources (e.g., "@gradientId"). + * Only used if color is null. */ - std::unique_ptr colorSource = nullptr; + std::string colorRef = {}; /** * The stroke width in pixels. The default value is 1. diff --git a/pagx/include/pagx/model/types/Placement.h b/pagx/include/pagx/model/types/LayerPlacement.h similarity index 88% rename from pagx/include/pagx/model/types/Placement.h rename to pagx/include/pagx/model/types/LayerPlacement.h index 294c153d98..46f8e9e839 100644 --- a/pagx/include/pagx/model/types/Placement.h +++ b/pagx/include/pagx/model/types/LayerPlacement.h @@ -18,8 +18,6 @@ #pragma once -#include - namespace pagx { /** @@ -30,7 +28,4 @@ enum class LayerPlacement { Foreground }; -std::string LayerPlacementToString(LayerPlacement placement); -LayerPlacement LayerPlacementFromString(const std::string& str); - } // namespace pagx diff --git a/pagx/src/PAGXEnumUtils.h b/pagx/src/PAGXEnumUtils.h new file mode 100644 index 0000000000..a3fa602ea4 --- /dev/null +++ b/pagx/src/PAGXEnumUtils.h @@ -0,0 +1,166 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/model/ColorSource.h" +#include "pagx/model/Element.h" +#include "pagx/model/Fill.h" +#include "pagx/model/ImagePattern.h" +#include "pagx/model/Layer.h" +#include "pagx/model/LayerFilter.h" +#include "pagx/model/LayerStyle.h" +#include "pagx/model/MergePath.h" +#include "pagx/model/Node.h" +#include "pagx/model/Polystar.h" +#include "pagx/model/RangeSelector.h" +#include "pagx/model/Repeater.h" +#include "pagx/model/Stroke.h" +#include "pagx/model/TextLayout.h" +#include "pagx/model/TextPath.h" +#include "pagx/model/TrimPath.h" +#include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/ColorSpace.h" +#include "pagx/model/types/LayerPlacement.h" +#include "pagx/model/types/TileMode.h" + +namespace pagx { + +//============================================================================== +// Node types +//============================================================================== +const char* NodeTypeName(NodeType type); + +//============================================================================== +// Element types +//============================================================================== +const char* ElementTypeName(ElementType type); + +//============================================================================== +// ColorSource types +//============================================================================== +const char* ColorSourceTypeName(ColorSourceType type); + +//============================================================================== +// LayerStyle types +//============================================================================== +const char* LayerStyleTypeName(LayerStyleType type); + +//============================================================================== +// LayerFilter types +//============================================================================== +const char* LayerFilterTypeName(LayerFilterType type); + +//============================================================================== +// BlendMode +//============================================================================== +std::string BlendModeToString(BlendMode mode); +BlendMode BlendModeFromString(const std::string& str); + +//============================================================================== +// FillRule +//============================================================================== +std::string FillRuleToString(FillRule rule); +FillRule FillRuleFromString(const std::string& str); + +//============================================================================== +// LineCap, LineJoin, StrokeAlign +//============================================================================== +std::string LineCapToString(LineCap cap); +LineCap LineCapFromString(const std::string& str); +std::string LineJoinToString(LineJoin join); +LineJoin LineJoinFromString(const std::string& str); +std::string StrokeAlignToString(StrokeAlign align); +StrokeAlign StrokeAlignFromString(const std::string& str); + +//============================================================================== +// TileMode +//============================================================================== +std::string TileModeToString(TileMode mode); +TileMode TileModeFromString(const std::string& str); + +//============================================================================== +// LayerPlacement +//============================================================================== +std::string LayerPlacementToString(LayerPlacement placement); +LayerPlacement LayerPlacementFromString(const std::string& str); + +//============================================================================== +// ColorSpace +//============================================================================== +std::string ColorSpaceToString(ColorSpace space); +ColorSpace ColorSpaceFromString(const std::string& str); + +//============================================================================== +// TrimType +//============================================================================== +std::string TrimTypeToString(TrimType type); +TrimType TrimTypeFromString(const std::string& str); + +//============================================================================== +// MaskType +//============================================================================== +std::string MaskTypeToString(MaskType type); +MaskType MaskTypeFromString(const std::string& str); + +//============================================================================== +// PolystarType +//============================================================================== +std::string PolystarTypeToString(PolystarType type); +PolystarType PolystarTypeFromString(const std::string& str); + +//============================================================================== +// MergePathMode +//============================================================================== +std::string MergePathModeToString(MergePathMode mode); +MergePathMode MergePathModeFromString(const std::string& str); + +//============================================================================== +// SamplingMode +//============================================================================== +std::string SamplingModeToString(SamplingMode mode); +SamplingMode SamplingModeFromString(const std::string& str); + +//============================================================================== +// TextAlign, VerticalAlign, Overflow +//============================================================================== +std::string TextAlignToString(TextAlign align); +TextAlign TextAlignFromString(const std::string& str); +std::string VerticalAlignToString(VerticalAlign align); +VerticalAlign VerticalAlignFromString(const std::string& str); +std::string TextDirectionToString(TextDirection direction); +TextDirection TextDirectionFromString(const std::string& str); + +//============================================================================== +// RepeaterOrder +//============================================================================== +std::string RepeaterOrderToString(RepeaterOrder order); +RepeaterOrder RepeaterOrderFromString(const std::string& str); + +//============================================================================== +// Selector types +//============================================================================== +std::string SelectorUnitToString(SelectorUnit unit); +SelectorUnit SelectorUnitFromString(const std::string& str); +std::string SelectorShapeToString(SelectorShape shape); +SelectorShape SelectorShapeFromString(const std::string& str); +std::string SelectorModeToString(SelectorMode mode); +SelectorMode SelectorModeFromString(const std::string& str); + +} // namespace pagx diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index de144d7edf..e5a1a9db84 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -17,9 +17,9 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "PAGXExporter.h" -#include #include #include "PAGXEnumUtils.h" +#include "pagx/model/types/Color.h" #include "pagx/model/BackgroundBlurStyle.h" #include "pagx/model/BlendFilter.h" #include "pagx/model/BlurFilter.h" @@ -216,21 +216,6 @@ static std::string floatListToString(const std::vector& values) { return oss.str(); } -static std::string colorToHexString(const Color& color, bool includeAlpha = false) { - auto toHex = [](float value) { - int v = static_cast(std::round(value * 255)); - v = std::max(0, std::min(255, v)); - char hex[3]; - snprintf(hex, sizeof(hex), "%02X", v); - return std::string(hex); - }; - std::string result = "#" + toHex(color.red) + toHex(color.green) + toHex(color.blue); - if (includeAlpha && color.alpha < 1.0f) { - result += toHex(color.alpha); - } - return result; -} - //============================================================================== // Forward declarations //============================================================================== @@ -250,7 +235,7 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& stops for (const auto& stop : stops) { xml.openElement("ColorStop"); xml.addRequiredAttribute("offset", stop.offset); - xml.addRequiredAttribute("color", colorToHexString(stop.color, stop.color.alpha < 1.0f)); + xml.addRequiredAttribute("color", stop.color.toHexString(stop.color.alpha < 1.0f)); xml.closeElementSelfClosing(); } } @@ -261,7 +246,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { auto solid = static_cast(node); xml.openElement("SolidColor"); xml.addAttribute("id", solid->id); - xml.addAttribute("color", colorToHexString(solid->color, solid->color.alpha < 1.0f)); + xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); xml.closeElementSelfClosing(); break; } @@ -712,7 +697,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.addAttribute("offsetY", style->offsetY); xml.addAttribute("blurrinessX", style->blurrinessX); xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", colorToHexString(style->color, style->color.alpha < 1.0f)); + xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); xml.addAttribute("showBehindLayer", style->showBehindLayer, true); xml.closeElementSelfClosing(); break; @@ -727,7 +712,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.addAttribute("offsetY", style->offsetY); xml.addAttribute("blurrinessX", style->blurrinessX); xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", colorToHexString(style->color, style->color.alpha < 1.0f)); + xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); xml.closeElementSelfClosing(); break; } @@ -774,7 +759,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.addAttribute("offsetY", filter->offsetY); xml.addAttribute("blurrinessX", filter->blurrinessX); xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", colorToHexString(filter->color, filter->color.alpha < 1.0f)); + xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); xml.addAttribute("shadowOnly", filter->shadowOnly); xml.closeElementSelfClosing(); break; @@ -786,7 +771,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.addAttribute("offsetY", filter->offsetY); xml.addAttribute("blurrinessX", filter->blurrinessX); xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", colorToHexString(filter->color, filter->color.alpha < 1.0f)); + xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); xml.addAttribute("shadowOnly", filter->shadowOnly); xml.closeElementSelfClosing(); break; @@ -794,7 +779,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { case LayerFilterType::BlendFilter: { auto filter = static_cast(node); xml.openElement("BlendFilter"); - xml.addAttribute("color", colorToHexString(filter->color, filter->color.alpha < 1.0f)); + xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); if (filter->blendMode != BlendMode::Normal) { xml.addAttribute("blendMode", BlendModeToString(filter->blendMode)); } From 7283ada681bfa043972f77a5a247c7fddd7c7838 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 22:38:48 +0800 Subject: [PATCH 110/678] Move ColorToHexString to PAGXEnumUtils and fix type method naming consistency. --- pagx/include/pagx/model/Fill.h | 4 +- pagx/include/pagx/model/Stroke.h | 4 +- pagx/include/pagx/model/types/Color.h | 236 ++----------------- pagx/src/PAGXEnumUtils.cpp | 318 ++++++++++++++++++++++++++ pagx/src/PAGXEnumUtils.h | 11 +- pagx/src/PAGXExporter.cpp | 53 +++-- pagx/src/PAGXXMLParser.cpp | 15 +- pagx/src/tgfx/LayerBuilder.cpp | 46 ++-- test/src/PAGXTest.cpp | 38 +-- 9 files changed, 424 insertions(+), 301 deletions(-) create mode 100644 pagx/src/PAGXEnumUtils.cpp diff --git a/pagx/include/pagx/model/Fill.h b/pagx/include/pagx/model/Fill.h index ea84f0d80a..7265d8b4c7 100644 --- a/pagx/include/pagx/model/Fill.h +++ b/pagx/include/pagx/model/Fill.h @@ -76,8 +76,8 @@ class Fill : public Element { */ LayerPlacement placement = LayerPlacement::Background; - ElementType type() const override { - return ElementType::Fill; + NodeType nodeType() const override { + return NodeType::Fill; } }; diff --git a/pagx/include/pagx/model/Stroke.h b/pagx/include/pagx/model/Stroke.h index 4308d9bd9f..0a4fb2b12f 100644 --- a/pagx/include/pagx/model/Stroke.h +++ b/pagx/include/pagx/model/Stroke.h @@ -127,8 +127,8 @@ class Stroke : public Element { */ LayerPlacement placement = LayerPlacement::Background; - ElementType type() const override { - return ElementType::Stroke; + NodeType nodeType() const override { + return NodeType::Stroke; } }; diff --git a/pagx/include/pagx/model/types/Color.h b/pagx/include/pagx/model/types/Color.h index 7cf0274110..af9c3a9e11 100644 --- a/pagx/include/pagx/model/types/Color.h +++ b/pagx/include/pagx/model/types/Color.h @@ -18,246 +18,44 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include +#include "pagx/model/types/ColorSpace.h" namespace pagx { -namespace detail { -inline int ParseHexDigit(char c) { - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'a' && c <= 'f') { - return c - 'a' + 10; - } - if (c >= 'A' && c <= 'F') { - return c - 'A' + 10; - } - return 0; -} -} // namespace detail - /** - * An RGBA color with floating-point components in [0, 1]. + * An RGBA color with floating-point components and color space. + * For sRGB colors, components are typically in [0, 1]. + * For wide gamut colors (Display P3), components may exceed [0, 1]. */ struct Color { + /** + * Red component, typically in [0, 1] for sRGB, may exceed for wide gamut. + */ float red = 0; - float green = 0; - float blue = 0; - float alpha = 1; /** - * Returns a Color from a hex value (0xRRGGBB or 0xRRGGBBAA). + * Green component, typically in [0, 1] for sRGB, may exceed for wide gamut. */ - static Color FromHex(uint32_t hex, bool hasAlpha = false) { - Color color = {}; - if (hasAlpha) { - color.red = static_cast((hex >> 24) & 0xFF) / 255.0f; - color.green = static_cast((hex >> 16) & 0xFF) / 255.0f; - color.blue = static_cast((hex >> 8) & 0xFF) / 255.0f; - color.alpha = static_cast(hex & 0xFF) / 255.0f; - } else { - color.red = static_cast((hex >> 16) & 0xFF) / 255.0f; - color.green = static_cast((hex >> 8) & 0xFF) / 255.0f; - color.blue = static_cast(hex & 0xFF) / 255.0f; - color.alpha = 1.0f; - } - return color; - } + float green = 0; /** - * Returns a Color from RGBA components in [0, 1]. + * Blue component, typically in [0, 1] for sRGB, may exceed for wide gamut. */ - static Color FromRGBA(float r, float g, float b, float a = 1) { - return {r, g, b, a}; - } + float blue = 0; /** - * Parses a color string. Supports: - * - Hex: "#RGB", "#RRGGBB", "#RRGGBBAA" - * - RGB: "rgb(r,g,b)", "rgba(r,g,b,a)" - * - Named colors: "white", "black", "red", etc. - * - CSS Color Level 4: "color(display-p3 r g b)" with colorspace conversion - * Returns black if parsing fails. + * Alpha component, in [0, 1] range. Default is 1 (fully opaque). */ - static Color Parse(const std::string& str) { - if (str.empty()) { - return {}; - } - // Named colors (CSS Level 1-4 basic colors). - static const std::unordered_map namedColors = { - {"black", 0x000000}, {"white", 0xFFFFFF}, - {"red", 0xFF0000}, {"green", 0x008000}, - {"blue", 0x0000FF}, {"yellow", 0xFFFF00}, - {"cyan", 0x00FFFF}, {"magenta", 0xFF00FF}, - {"gray", 0x808080}, {"grey", 0x808080}, - {"silver", 0xC0C0C0}, {"maroon", 0x800000}, - {"olive", 0x808000}, {"lime", 0x00FF00}, - {"aqua", 0x00FFFF}, {"teal", 0x008080}, - {"navy", 0x000080}, {"fuchsia", 0xFF00FF}, - {"purple", 0x800080}, {"orange", 0xFFA500}, - {"pink", 0xFFC0CB}, {"brown", 0xA52A2A}, - {"coral", 0xFF7F50}, {"crimson", 0xDC143C}, - {"darkblue", 0x00008B}, {"darkgray", 0xA9A9A9}, - {"darkgreen", 0x006400}, {"darkred", 0x8B0000}, - {"gold", 0xFFD700}, {"indigo", 0x4B0082}, - {"ivory", 0xFFFFF0}, {"khaki", 0xF0E68C}, - {"lavender", 0xE6E6FA}, {"lightblue", 0xADD8E6}, - {"lightgray", 0xD3D3D3}, {"lightgreen", 0x90EE90}, - {"lightyellow", 0xFFFFE0}, {"none", 0x000000}, - {"transparent", 0x000000}, - }; - auto it = namedColors.find(str); - if (it != namedColors.end()) { - if (str == "transparent" || str == "none") { - auto color = Color::FromHex(0x000000); - color.alpha = 0; - return color; - } - return Color::FromHex(it->second); - } - if (str[0] == '#') { - auto hex = str.substr(1); - if (hex.size() == 3) { - int r = detail::ParseHexDigit(hex[0]); - int g = detail::ParseHexDigit(hex[1]); - int b = detail::ParseHexDigit(hex[2]); - return Color::FromRGBA(static_cast(r * 17) / 255.0f, - static_cast(g * 17) / 255.0f, - static_cast(b * 17) / 255.0f, 1.0f); - } - if (hex.size() == 6) { - int r = detail::ParseHexDigit(hex[0]) * 16 + detail::ParseHexDigit(hex[1]); - int g = detail::ParseHexDigit(hex[2]) * 16 + detail::ParseHexDigit(hex[3]); - int b = detail::ParseHexDigit(hex[4]) * 16 + detail::ParseHexDigit(hex[5]); - return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, - static_cast(b) / 255.0f, 1.0f); - } - if (hex.size() == 8) { - int r = detail::ParseHexDigit(hex[0]) * 16 + detail::ParseHexDigit(hex[1]); - int g = detail::ParseHexDigit(hex[2]) * 16 + detail::ParseHexDigit(hex[3]); - int b = detail::ParseHexDigit(hex[4]) * 16 + detail::ParseHexDigit(hex[5]); - int a = detail::ParseHexDigit(hex[6]) * 16 + detail::ParseHexDigit(hex[7]); - return Color::FromRGBA(static_cast(r) / 255.0f, static_cast(g) / 255.0f, - static_cast(b) / 255.0f, static_cast(a) / 255.0f); - } - } - if (str.substr(0, 4) == "rgb(" || str.substr(0, 5) == "rgba(") { - auto start = str.find('('); - auto end = str.find(')'); - if (start != std::string::npos && end != std::string::npos) { - auto values = str.substr(start + 1, end - start - 1); - std::istringstream iss(values); - std::string token = {}; - std::vector components = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - components.push_back(std::stof(trimmed)); - } - if (components.size() >= 3) { - float r = components[0] / 255.0f; - float g = components[1] / 255.0f; - float b = components[2] / 255.0f; - float a = components.size() >= 4 ? components[3] : 1.0f; - return Color::FromRGBA(r, g, b, a); - } - } - } - // CSS Color Level 4: color(colorspace r g b) or color(colorspace r g b / a) - if (str.substr(0, 6) == "color(") { - auto start = str.find('('); - auto end = str.find(')'); - if (start != std::string::npos && end != std::string::npos) { - auto inner = str.substr(start + 1, end - start - 1); - inner.erase(0, inner.find_first_not_of(" \t")); - inner.erase(inner.find_last_not_of(" \t") + 1); - - std::istringstream iss(inner); - std::string colorspace = {}; - iss >> colorspace; - - std::vector components = {}; - std::string token = {}; - float alpha = 1.0f; - bool foundSlash = false; - - while (iss >> token) { - if (token == "/") { - foundSlash = true; - continue; - } - float value = std::stof(token); - if (foundSlash) { - alpha = value; - } else { - components.push_back(value); - } - } - - if (components.size() >= 3) { - float r = components[0]; - float g = components[1]; - float b = components[2]; - - // Convert from wide gamut colorspace to sRGB (approximate clipping). - if (colorspace == "display-p3") { - float sR = 1.2249f * r - 0.2247f * g - 0.0002f * b; - float sG = -0.0420f * r + 1.0419f * g + 0.0001f * b; - float sB = -0.0197f * r - 0.0786f * g + 1.0983f * b; - r = std::max(0.0f, std::min(1.0f, sR)); - g = std::max(0.0f, std::min(1.0f, sG)); - b = std::max(0.0f, std::min(1.0f, sB)); - } else if (colorspace == "a98-rgb") { - float sR = 1.3982f * r - 0.3982f * g + 0.0f * b; - float sG = 0.0f * r + 1.0f * g + 0.0f * b; - float sB = 0.0f * r - 0.0429f * g + 1.0429f * b; - r = std::max(0.0f, std::min(1.0f, sR)); - g = std::max(0.0f, std::min(1.0f, sG)); - b = std::max(0.0f, std::min(1.0f, sB)); - } else if (colorspace == "rec2020") { - float sR = 1.6605f * r - 0.5877f * g - 0.0728f * b; - float sG = -0.1246f * r + 1.1330f * g - 0.0084f * b; - float sB = -0.0182f * r - 0.1006f * g + 1.1188f * b; - r = std::max(0.0f, std::min(1.0f, sR)); - g = std::max(0.0f, std::min(1.0f, sG)); - b = std::max(0.0f, std::min(1.0f, sB)); - } - return Color::FromRGBA(r, g, b, alpha); - } - } - } - return {}; - } + float alpha = 1; /** - * Returns the color as a hex string "#RRGGBB" or "#RRGGBBAA". + * The color space of this color. Default is sRGB. */ - std::string toHexString(bool includeAlpha = false) const { - auto toHex = [](float v) { - int i = static_cast(std::round(v * 255.0f)); - i = std::max(0, std::min(255, i)); - char buf[3] = {}; - snprintf(buf, sizeof(buf), "%02X", i); - return std::string(buf); - }; - std::string result = "#" + toHex(red) + toHex(green) + toHex(blue); - if (includeAlpha && alpha < 1.0f) { - result += toHex(alpha); - } - return result; - } + ColorSpace colorSpace = ColorSpace::SRGB; bool operator==(const Color& other) const { - return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; + return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha && + colorSpace == other.colorSpace; } bool operator!=(const Color& other) const { diff --git a/pagx/src/PAGXEnumUtils.cpp b/pagx/src/PAGXEnumUtils.cpp new file mode 100644 index 0000000000..0153dae790 --- /dev/null +++ b/pagx/src/PAGXEnumUtils.cpp @@ -0,0 +1,318 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PAGXEnumUtils.h" +#include +#include +#include + +namespace pagx { + +//============================================================================== +// Helper macro for enum string conversions +//============================================================================== + +#define DEFINE_ENUM_CONVERSION(EnumType, ...) \ + static const std::unordered_map EnumType##ToStringMap = {__VA_ARGS__}; \ + static const std::unordered_map StringTo##EnumType##Map = [] { \ + std::unordered_map map = {}; \ + for (const auto& pair : EnumType##ToStringMap) { \ + map[pair.second] = pair.first; \ + } \ + return map; \ + }(); \ + std::string EnumType##ToString(EnumType value) { \ + auto it = EnumType##ToStringMap.find(value); \ + return it != EnumType##ToStringMap.end() ? it->second : ""; \ + } \ + EnumType EnumType##FromString(const std::string& str) { \ + auto it = StringTo##EnumType##Map.find(str); \ + return it != StringTo##EnumType##Map.end() ? it->second \ + : EnumType##ToStringMap.begin()->first; \ + } + +//============================================================================== +// TypeName functions +//============================================================================== + +const char* NodeTypeName(NodeType type) { + switch (type) { + case NodeType::PathData: + return "PathData"; + case NodeType::Image: + return "Image"; + case NodeType::Composition: + return "Composition"; + case NodeType::SolidColor: + return "SolidColor"; + case NodeType::LinearGradient: + return "LinearGradient"; + case NodeType::RadialGradient: + return "RadialGradient"; + case NodeType::ConicGradient: + return "ConicGradient"; + case NodeType::DiamondGradient: + return "DiamondGradient"; + case NodeType::ImagePattern: + return "ImagePattern"; + case NodeType::Rectangle: + return "Rectangle"; + case NodeType::Ellipse: + return "Ellipse"; + case NodeType::Polystar: + return "Polystar"; + case NodeType::Path: + return "Path"; + case NodeType::TextSpan: + return "TextSpan"; + case NodeType::Fill: + return "Fill"; + case NodeType::Stroke: + return "Stroke"; + case NodeType::TrimPath: + return "TrimPath"; + case NodeType::RoundCorner: + return "RoundCorner"; + case NodeType::MergePath: + return "MergePath"; + case NodeType::TextModifier: + return "TextModifier"; + case NodeType::TextPath: + return "TextPath"; + case NodeType::TextLayout: + return "TextLayout"; + case NodeType::Group: + return "Group"; + case NodeType::Repeater: + return "Repeater"; + default: + return "Unknown"; + } +} + +const char* ColorSourceTypeName(ColorSourceType type) { + switch (type) { + case ColorSourceType::SolidColor: + return "SolidColor"; + case ColorSourceType::LinearGradient: + return "LinearGradient"; + case ColorSourceType::RadialGradient: + return "RadialGradient"; + case ColorSourceType::ConicGradient: + return "ConicGradient"; + case ColorSourceType::DiamondGradient: + return "DiamondGradient"; + case ColorSourceType::ImagePattern: + return "ImagePattern"; + default: + return "Unknown"; + } +} + +const char* LayerStyleTypeName(LayerStyleType type) { + switch (type) { + case LayerStyleType::DropShadowStyle: + return "DropShadowStyle"; + case LayerStyleType::InnerShadowStyle: + return "InnerShadowStyle"; + case LayerStyleType::BackgroundBlurStyle: + return "BackgroundBlurStyle"; + default: + return "Unknown"; + } +} + +const char* LayerFilterTypeName(LayerFilterType type) { + switch (type) { + case LayerFilterType::BlurFilter: + return "BlurFilter"; + case LayerFilterType::DropShadowFilter: + return "DropShadowFilter"; + case LayerFilterType::InnerShadowFilter: + return "InnerShadowFilter"; + case LayerFilterType::BlendFilter: + return "BlendFilter"; + case LayerFilterType::ColorMatrixFilter: + return "ColorMatrixFilter"; + default: + return "Unknown"; + } +} + +//============================================================================== +// Enum string conversions +//============================================================================== + +DEFINE_ENUM_CONVERSION(BlendMode, + {BlendMode::Normal, "normal"}, + {BlendMode::Multiply, "multiply"}, + {BlendMode::Screen, "screen"}, + {BlendMode::Overlay, "overlay"}, + {BlendMode::Darken, "darken"}, + {BlendMode::Lighten, "lighten"}, + {BlendMode::ColorDodge, "colorDodge"}, + {BlendMode::ColorBurn, "colorBurn"}, + {BlendMode::HardLight, "hardLight"}, + {BlendMode::SoftLight, "softLight"}, + {BlendMode::Difference, "difference"}, + {BlendMode::Exclusion, "exclusion"}, + {BlendMode::Hue, "hue"}, + {BlendMode::Saturation, "saturation"}, + {BlendMode::Color, "color"}, + {BlendMode::Luminosity, "luminosity"}, + {BlendMode::PlusLighter, "plusLighter"}, + {BlendMode::PlusDarker, "plusDarker"}) + +DEFINE_ENUM_CONVERSION(LineCap, + {LineCap::Butt, "butt"}, + {LineCap::Round, "round"}, + {LineCap::Square, "square"}) + +DEFINE_ENUM_CONVERSION(LineJoin, + {LineJoin::Miter, "miter"}, + {LineJoin::Round, "round"}, + {LineJoin::Bevel, "bevel"}) + +DEFINE_ENUM_CONVERSION(FillRule, + {FillRule::Winding, "winding"}, + {FillRule::EvenOdd, "evenOdd"}) + +DEFINE_ENUM_CONVERSION(StrokeAlign, + {StrokeAlign::Center, "center"}, + {StrokeAlign::Inside, "inside"}, + {StrokeAlign::Outside, "outside"}) + +DEFINE_ENUM_CONVERSION(LayerPlacement, + {LayerPlacement::Background, "background"}, + {LayerPlacement::Foreground, "foreground"}) + +DEFINE_ENUM_CONVERSION(TileMode, + {TileMode::Clamp, "clamp"}, + {TileMode::Repeat, "repeat"}, + {TileMode::Mirror, "mirror"}, + {TileMode::Decal, "decal"}) + +DEFINE_ENUM_CONVERSION(SamplingMode, + {SamplingMode::Nearest, "nearest"}, + {SamplingMode::Linear, "linear"}, + {SamplingMode::Mipmap, "mipmap"}) + +DEFINE_ENUM_CONVERSION(MaskType, + {MaskType::Alpha, "alpha"}, + {MaskType::Luminance, "luminance"}, + {MaskType::Contour, "contour"}) + +DEFINE_ENUM_CONVERSION(PolystarType, + {PolystarType::Polygon, "polygon"}, + {PolystarType::Star, "star"}) + +DEFINE_ENUM_CONVERSION(TrimType, + {TrimType::Separate, "separate"}, + {TrimType::Continuous, "continuous"}) + +DEFINE_ENUM_CONVERSION(MergePathMode, + {MergePathMode::Append, "append"}, + {MergePathMode::Union, "union"}, + {MergePathMode::Intersect, "intersect"}, + {MergePathMode::Xor, "xor"}, + {MergePathMode::Difference, "difference"}) + +DEFINE_ENUM_CONVERSION(TextAlign, + {TextAlign::Start, "start"}, + {TextAlign::Center, "center"}, + {TextAlign::End, "end"}, + {TextAlign::Justify, "justify"}) + +DEFINE_ENUM_CONVERSION(VerticalAlign, + {VerticalAlign::Top, "top"}, + {VerticalAlign::Center, "center"}, + {VerticalAlign::Bottom, "bottom"}) + +DEFINE_ENUM_CONVERSION(TextDirection, + {TextDirection::Horizontal, "horizontal"}, + {TextDirection::Vertical, "vertical"}) + +DEFINE_ENUM_CONVERSION(SelectorUnit, + {SelectorUnit::Index, "index"}, + {SelectorUnit::Percentage, "percentage"}) + +DEFINE_ENUM_CONVERSION(SelectorShape, + {SelectorShape::Square, "square"}, + {SelectorShape::RampUp, "rampUp"}, + {SelectorShape::RampDown, "rampDown"}, + {SelectorShape::Triangle, "triangle"}, + {SelectorShape::Round, "round"}, + {SelectorShape::Smooth, "smooth"}) + +DEFINE_ENUM_CONVERSION(SelectorMode, + {SelectorMode::Add, "add"}, + {SelectorMode::Subtract, "subtract"}, + {SelectorMode::Intersect, "intersect"}, + {SelectorMode::Min, "min"}, + {SelectorMode::Max, "max"}, + {SelectorMode::Difference, "difference"}) + +DEFINE_ENUM_CONVERSION(RepeaterOrder, + {RepeaterOrder::BelowOriginal, "belowOriginal"}, + {RepeaterOrder::AboveOriginal, "aboveOriginal"}) + +std::string ColorSpaceToString(ColorSpace space) { + switch (space) { + case ColorSpace::SRGB: + return "srgb"; + case ColorSpace::DisplayP3: + return "p3"; + default: + return "srgb"; + } +} + +ColorSpace ColorSpaceFromString(const std::string& str) { + if (str == "p3" || str == "displayP3" || str == "DisplayP3") { + return ColorSpace::DisplayP3; + } + return ColorSpace::SRGB; +} + +std::string ColorToHexString(const Color& color, bool withAlpha) { + if (color.colorSpace == ColorSpace::DisplayP3) { + std::ostringstream oss = {}; + oss << "p3(" << color.red << ", " << color.green << ", " << color.blue; + if (withAlpha && color.alpha < 1.0f) { + oss << ", " << color.alpha; + } + oss << ")"; + return oss.str(); + } + auto toHex = [](float v) -> std::string { + int i = static_cast(std::round(v * 255.0f)); + i = std::max(0, std::min(255, i)); + char buf[3] = {}; + snprintf(buf, sizeof(buf), "%02X", i); + return std::string(buf); + }; + std::string result = "#" + toHex(color.red) + toHex(color.green) + toHex(color.blue); + if (withAlpha && color.alpha < 1.0f) { + result += toHex(color.alpha); + } + return result; +} + +#undef DEFINE_ENUM_CONVERSION + +} // namespace pagx diff --git a/pagx/src/PAGXEnumUtils.h b/pagx/src/PAGXEnumUtils.h index a3fa602ea4..0f31e0f28a 100644 --- a/pagx/src/PAGXEnumUtils.h +++ b/pagx/src/PAGXEnumUtils.h @@ -36,6 +36,7 @@ #include "pagx/model/TextPath.h" #include "pagx/model/TrimPath.h" #include "pagx/model/types/BlendMode.h" +#include "pagx/model/types/Color.h" #include "pagx/model/types/ColorSpace.h" #include "pagx/model/types/LayerPlacement.h" #include "pagx/model/types/TileMode.h" @@ -47,11 +48,6 @@ namespace pagx { //============================================================================== const char* NodeTypeName(NodeType type); -//============================================================================== -// Element types -//============================================================================== -const char* ElementTypeName(ElementType type); - //============================================================================== // ColorSource types //============================================================================== @@ -163,4 +159,9 @@ SelectorShape SelectorShapeFromString(const std::string& str); std::string SelectorModeToString(SelectorMode mode); SelectorMode SelectorModeFromString(const std::string& str); +//============================================================================== +// Color +//============================================================================== +std::string ColorToHexString(const Color& color, bool withAlpha = false); + } // namespace pagx diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index e5a1a9db84..7e61d82bad 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -19,7 +19,6 @@ #include "PAGXExporter.h" #include #include "PAGXEnumUtils.h" -#include "pagx/model/types/Color.h" #include "pagx/model/BackgroundBlurStyle.h" #include "pagx/model/BlendFilter.h" #include "pagx/model/BlurFilter.h" @@ -235,7 +234,7 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& stops for (const auto& stop : stops) { xml.openElement("ColorStop"); xml.addRequiredAttribute("offset", stop.offset); - xml.addRequiredAttribute("color", stop.color.toHexString(stop.color.alpha < 1.0f)); + xml.addRequiredAttribute("color", ColorToHexString(stop.color, stop.color.alpha < 1.0f)); xml.closeElementSelfClosing(); } } @@ -246,7 +245,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { auto solid = static_cast(node); xml.openElement("SolidColor"); xml.addAttribute("id", solid->id); - xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); + xml.addAttribute("color", ColorToHexString(solid->color, solid->color.alpha < 1.0f)); xml.closeElementSelfClosing(); break; } @@ -359,8 +358,8 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { //============================================================================== static void writeVectorElement(XMLBuilder& xml, const Element* node) { - switch (node->type()) { - case ElementType::Rectangle: { + switch (node->nodeType()) { + case NodeType::Rectangle: { auto rect = static_cast(node); xml.openElement("Rectangle"); if (rect->center.x != 0 || rect->center.y != 0) { @@ -374,7 +373,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.closeElementSelfClosing(); break; } - case ElementType::Ellipse: { + case NodeType::Ellipse: { auto ellipse = static_cast(node); xml.openElement("Ellipse"); if (ellipse->center.x != 0 || ellipse->center.y != 0) { @@ -387,13 +386,13 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.closeElementSelfClosing(); break; } - case ElementType::Polystar: { + case NodeType::Polystar: { auto polystar = static_cast(node); xml.openElement("Polystar"); if (polystar->center.x != 0 || polystar->center.y != 0) { xml.addAttribute("center", pointToString(polystar->center)); } - xml.addAttribute("polystarType", PolystarTypeToString(polystar->polystarType)); + xml.addAttribute("type", PolystarTypeToString(polystar->type)); xml.addAttribute("pointCount", polystar->pointCount, 5.0f); xml.addAttribute("outerRadius", polystar->outerRadius, 100.0f); xml.addAttribute("innerRadius", polystar->innerRadius, 50.0f); @@ -404,7 +403,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.closeElementSelfClosing(); break; } - case ElementType::Path: { + case NodeType::Path: { auto path = static_cast(node); xml.openElement("Path"); if (!path->data.isEmpty()) { @@ -420,7 +419,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.closeElementSelfClosing(); break; } - case ElementType::TextSpan: { + case NodeType::TextSpan: { auto text = static_cast(node); xml.openElement("TextSpan"); if (text->position.x != 0 || text->position.y != 0) { @@ -439,7 +438,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.closeElement(); break; } - case ElementType::Fill: { + case NodeType::Fill: { auto fill = static_cast(node); xml.openElement("Fill"); // If colorSource has an id, output reference; otherwise inline the colorSource below @@ -467,7 +466,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { } break; } - case ElementType::Stroke: { + case NodeType::Stroke: { auto stroke = static_cast(node); xml.openElement("Stroke"); // If colorSource has an id, output reference; otherwise inline the colorSource below @@ -507,26 +506,26 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { } break; } - case ElementType::TrimPath: { + case NodeType::TrimPath: { auto trim = static_cast(node); xml.openElement("TrimPath"); xml.addAttribute("start", trim->start); xml.addAttribute("end", trim->end, 1.0f); xml.addAttribute("offset", trim->offset); - if (trim->trimType != TrimType::Separate) { - xml.addAttribute("type", TrimTypeToString(trim->trimType)); + if (trim->type != TrimType::Separate) { + xml.addAttribute("type", TrimTypeToString(trim->type)); } xml.closeElementSelfClosing(); break; } - case ElementType::RoundCorner: { + case NodeType::RoundCorner: { auto round = static_cast(node); xml.openElement("RoundCorner"); xml.addAttribute("radius", round->radius, 10.0f); xml.closeElementSelfClosing(); break; } - case ElementType::MergePath: { + case NodeType::MergePath: { auto merge = static_cast(node); xml.openElement("MergePath"); if (merge->mode != MergePathMode::Append) { @@ -535,7 +534,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.closeElementSelfClosing(); break; } - case ElementType::TextModifier: { + case NodeType::TextModifier: { auto modifier = static_cast(node); xml.openElement("TextModifier"); if (modifier->anchorPoint.x != 0 || modifier->anchorPoint.y != 0) { @@ -589,7 +588,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { } break; } - case ElementType::TextPath: { + case NodeType::TextPath: { auto textPath = static_cast(node); xml.openElement("TextPath"); xml.addAttribute("path", textPath->path); @@ -603,7 +602,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.closeElementSelfClosing(); break; } - case ElementType::TextLayout: { + case NodeType::TextLayout: { auto layout = static_cast(node); xml.openElement("TextLayout"); xml.addAttribute("x", layout->x); @@ -626,7 +625,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.closeElementSelfClosing(); break; } - case ElementType::Repeater: { + case NodeType::Repeater: { auto repeater = static_cast(node); xml.openElement("Repeater"); xml.addAttribute("copies", repeater->copies, 3.0f); @@ -649,7 +648,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.closeElementSelfClosing(); break; } - case ElementType::Group: { + case NodeType::Group: { auto group = static_cast(node); xml.openElement("Group"); if (group->anchorPoint.x != 0 || group->anchorPoint.y != 0) { @@ -697,7 +696,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.addAttribute("offsetY", style->offsetY); xml.addAttribute("blurrinessX", style->blurrinessX); xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); + xml.addAttribute("color", ColorToHexString(style->color, style->color.alpha < 1.0f)); xml.addAttribute("showBehindLayer", style->showBehindLayer, true); xml.closeElementSelfClosing(); break; @@ -712,7 +711,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.addAttribute("offsetY", style->offsetY); xml.addAttribute("blurrinessX", style->blurrinessX); xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); + xml.addAttribute("color", ColorToHexString(style->color, style->color.alpha < 1.0f)); xml.closeElementSelfClosing(); break; } @@ -759,7 +758,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.addAttribute("offsetY", filter->offsetY); xml.addAttribute("blurrinessX", filter->blurrinessX); xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); + xml.addAttribute("color", ColorToHexString(filter->color, filter->color.alpha < 1.0f)); xml.addAttribute("shadowOnly", filter->shadowOnly); xml.closeElementSelfClosing(); break; @@ -771,7 +770,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.addAttribute("offsetY", filter->offsetY); xml.addAttribute("blurrinessX", filter->blurrinessX); xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); + xml.addAttribute("color", ColorToHexString(filter->color, filter->color.alpha < 1.0f)); xml.addAttribute("shadowOnly", filter->shadowOnly); xml.closeElementSelfClosing(); break; @@ -779,7 +778,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { case LayerFilterType::BlendFilter: { auto filter = static_cast(node); xml.openElement("BlendFilter"); - xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); + xml.addAttribute("color", ColorToHexString(filter->color, filter->color.alpha < 1.0f)); if (filter->blendMode != BlendMode::Normal) { xml.addAttribute("blendMode", BlendModeToString(filter->blendMode)); } diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXXMLParser.cpp index ffd3306dcb..dea2344307 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXXMLParser.cpp @@ -563,7 +563,7 @@ std::unique_ptr PAGXXMLParser::parsePolystar(const XMLNode* node) { auto polystar = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); polystar->center = parsePoint(centerStr); - polystar->polystarType = PolystarTypeFromString(getAttribute(node, "polystarType", "star")); + polystar->type = PolystarTypeFromString(getAttribute(node, "type", "star")); polystar->pointCount = getFloatAttribute(node, "pointCount", 5); polystar->outerRadius = getFloatAttribute(node, "outerRadius", 100); polystar->innerRadius = getFloatAttribute(node, "innerRadius", 50); @@ -658,7 +658,7 @@ std::unique_ptr PAGXXMLParser::parseTrimPath(const XMLNode* node) { trim->start = getFloatAttribute(node, "start", 0); trim->end = getFloatAttribute(node, "end", 1); trim->offset = getFloatAttribute(node, "offset", 0); - trim->trimType = TrimTypeFromString(getAttribute(node, "type", "separate")); + trim->type = TrimTypeFromString(getAttribute(node, "type", "separate")); return trim; } @@ -702,24 +702,25 @@ std::unique_ptr PAGXXMLParser::parseTextModifier(const XMLNode* no std::unique_ptr PAGXXMLParser::parseTextPath(const XMLNode* node) { auto textPath = std::make_unique(); textPath->path = getAttribute(node, "path"); - textPath->pathAlign = TextPathAlignFromString(getAttribute(node, "align", "start")); + textPath->textAlign = TextAlignFromString(getAttribute(node, "textAlign", "start")); textPath->firstMargin = getFloatAttribute(node, "firstMargin", 0); textPath->lastMargin = getFloatAttribute(node, "lastMargin", 0); textPath->perpendicularToPath = getBoolAttribute(node, "perpendicularToPath", true); textPath->reversed = getBoolAttribute(node, "reversed", false); - textPath->forceAlignment = getBoolAttribute(node, "forceAlignment", false); return textPath; } std::unique_ptr PAGXXMLParser::parseTextLayout(const XMLNode* node) { auto layout = std::make_unique(); + layout->x = getFloatAttribute(node, "x", 0); + layout->y = getFloatAttribute(node, "y", 0); layout->width = getFloatAttribute(node, "width", 0); layout->height = getFloatAttribute(node, "height", 0); - layout->textAlign = TextAlignFromString(getAttribute(node, "align", "left")); + layout->textAlign = TextAlignFromString(getAttribute(node, "textAlign", "start")); + layout->textAlignLast = TextAlignFromString(getAttribute(node, "textAlignLast", "start")); layout->verticalAlign = VerticalAlignFromString(getAttribute(node, "verticalAlign", "top")); layout->lineHeight = getFloatAttribute(node, "lineHeight", 1.2f); - layout->indent = getFloatAttribute(node, "indent", 0); - layout->overflow = OverflowFromString(getAttribute(node, "overflow", "clip")); + layout->direction = TextDirectionFromString(getAttribute(node, "direction", "horizontal")); return layout; } diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 69442cfd3e..72d281d8ac 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -363,32 +363,32 @@ class LayerBuilderImpl { return nullptr; } - switch (node->type()) { - case ElementType::Rectangle: + switch (node->nodeType()) { + case NodeType::Rectangle: return convertRectangle(static_cast(node)); - case ElementType::Ellipse: + case NodeType::Ellipse: return convertEllipse(static_cast(node)); - case ElementType::Polystar: + case NodeType::Polystar: return convertPolystar(static_cast(node)); - case ElementType::Path: + case NodeType::Path: return convertPath(static_cast(node)); - case ElementType::TextSpan: + case NodeType::TextSpan: return convertTextSpan(static_cast(node)); - case ElementType::Fill: + case NodeType::Fill: return convertFill(static_cast(node)); - case ElementType::Stroke: + case NodeType::Stroke: return convertStroke(static_cast(node)); - case ElementType::TrimPath: + case NodeType::TrimPath: return convertTrimPath(static_cast(node)); - case ElementType::RoundCorner: + case NodeType::RoundCorner: return convertRoundCorner(static_cast(node)); - case ElementType::MergePath: + case NodeType::MergePath: return convertMergePath(static_cast(node)); - case ElementType::Repeater: + case NodeType::Repeater: return convertRepeater(static_cast(node)); - case ElementType::Group: + case NodeType::Group: return convertGroup(static_cast(node)); - case ElementType::TextLayout: + case NodeType::TextLayout: // TextLayout is handled in convertGroup, not converted directly. return nullptr; default: @@ -423,7 +423,7 @@ class LayerBuilderImpl { polystar->setInnerRoundness(node->innerRoundness); polystar->setRotation(node->rotation); polystar->setReversed(node->reversed); - if (node->polystarType == PolystarType::Polygon) { + if (node->type == PolystarType::Polygon) { polystar->setPolystarType(tgfx::PolystarType::Polygon); } else { polystar->setPolystarType(tgfx::PolystarType::Star); @@ -680,7 +680,7 @@ class LayerBuilderImpl { // Check if group contains TextLayout modifier. const TextLayout* textLayout = nullptr; for (const auto& element : node->elements) { - if (element->type() == ElementType::TextLayout) { + if (element->nodeType() == NodeType::TextLayout) { textLayout = static_cast(element.get()); break; } @@ -696,7 +696,7 @@ class LayerBuilderImpl { if (textLayout != nullptr) { for (const auto& element : node->elements) { - if (element->type() == ElementType::TextSpan) { + if (element->nodeType() == NodeType::TextSpan) { auto span = static_cast(element.get()); TextSpanInfo info; info.span = span; @@ -733,12 +733,12 @@ class LayerBuilderImpl { for (const auto& element : node->elements) { // Skip TextLayout modifier, it's handled by adjusting TextSpan positions. - if (element->type() == ElementType::TextLayout) { + if (element->nodeType() == NodeType::TextLayout) { continue; } // Handle TextSpan with layout adjustments. - if (element->type() == ElementType::TextSpan && textLayout != nullptr) { + if (element->nodeType() == NodeType::TextSpan && textLayout != nullptr) { auto span = static_cast(element.get()); auto tgfxTextSpan = std::make_shared(); @@ -756,21 +756,21 @@ class LayerBuilderImpl { // Calculate x offset based on textAlign. // This follows tgfx SVG text-anchor handling: xOffset = alignmentFactor * width - // where alignmentFactor is: Left=0, Center=-0.5, Right=-1.0 + // where alignmentFactor is: Start=0, Center=-0.5, End=-1.0 float xOffset = 0; float textWidth = info->bounds.width(); switch (textLayout->textAlign) { - case TextAlign::Left: + case TextAlign::Start: // No offset needed. break; case TextAlign::Center: xOffset = -0.5f * textWidth; break; - case TextAlign::Right: + case TextAlign::End: xOffset = -textWidth; break; case TextAlign::Justify: - // Justify requires more complex handling, treat as left for now. + // Justify requires more complex handling, treat as start for now. break; } diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 81c5d9af3c..fe21780e9a 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -205,29 +205,30 @@ PAG_TEST(PAGXTest, PAGXNodeBasic) { rect->size.height = 80; rect->roundness = 10; - EXPECT_EQ(rect->type(), pagx::ElementType::Rectangle); - EXPECT_STREQ(pagx::ElementTypeName(rect->type()), "Rectangle"); + EXPECT_EQ(rect->nodeType(), pagx::NodeType::Rectangle); EXPECT_FLOAT_EQ(rect->center.x, 50); EXPECT_FLOAT_EQ(rect->size.width, 100); // Test Path creation auto path = std::make_unique(); path->data = pagx::PathData::FromSVGString("M0 0 L100 100"); - EXPECT_EQ(path->type(), pagx::ElementType::Path); + EXPECT_EQ(path->nodeType(), pagx::NodeType::Path); EXPECT_GT(path->data.verbs().size(), 0u); // Test Fill creation auto fill = std::make_unique(); - fill->color = "#FF0000"; + auto solidColor = std::make_unique(); + solidColor->color = {1.0f, 0.0f, 0.0f, 1.0f}; // Red + fill->color = std::move(solidColor); fill->alpha = 0.8f; - EXPECT_EQ(fill->type(), pagx::ElementType::Fill); - EXPECT_EQ(fill->color, "#FF0000"); + EXPECT_EQ(fill->nodeType(), pagx::NodeType::Fill); + EXPECT_NE(fill->color, nullptr); // Test Group with children auto group = std::make_unique(); group->elements.push_back(std::move(rect)); group->elements.push_back(std::move(fill)); - EXPECT_EQ(group->type(), pagx::ElementType::Group); + EXPECT_EQ(group->nodeType(), pagx::NodeType::Group); EXPECT_EQ(group->elements.size(), 2u); } @@ -255,7 +256,9 @@ PAG_TEST(PAGXTest, PAGXDocumentXMLExport) { rect->size.height = 60; auto fill = std::make_unique(); - fill->color = "#0000FF"; + auto solidColor = std::make_unique(); + solidColor->color = {0.0f, 0.0f, 1.0f, 1.0f}; // Blue + fill->color = std::move(solidColor); group->elements.push_back(std::move(rect)); group->elements.push_back(std::move(fill)); @@ -293,7 +296,9 @@ PAG_TEST(PAGXTest, PAGXDocumentRoundTrip) { rect->size.height = 60; auto fill = std::make_unique(); - fill->color = "#00FF00"; + auto solidColor = std::make_unique(); + solidColor->color = {0.0f, 1.0f, 0.0f, 1.0f}; // Green + fill->color = std::move(solidColor); layer->contents.push_back(std::move(rect)); layer->contents.push_back(std::move(fill)); @@ -349,11 +354,12 @@ PAG_TEST(PAGXTest, PAGXTypesBasic) { EXPECT_FLOAT_EQ(c1.blue, 0.0f); EXPECT_FLOAT_EQ(c1.alpha, 1.0f); - // Test Color parsing - auto c2 = pagx::Color::Parse("#FF8000"); + // Test Color with wide gamut + pagx::Color c2 = {1.0f, 0.5f, 0.0f, 1.0f, pagx::ColorSpace::DisplayP3}; EXPECT_FLOAT_EQ(c2.red, 1.0f); - EXPECT_NEAR(c2.green, 0.5f, 0.01f); + EXPECT_FLOAT_EQ(c2.green, 0.5f); EXPECT_FLOAT_EQ(c2.blue, 0.0f); + EXPECT_EQ(c2.colorSpace, pagx::ColorSpace::DisplayP3); // Test Matrix (identity) pagx::Matrix m1 = {}; @@ -372,7 +378,7 @@ PAG_TEST(PAGXTest, PAGXTypesBasic) { PAG_TEST(PAGXTest, ColorSources) { // Test SolidColor auto solid = std::make_unique(); - solid->color = pagx::Color::FromRGBA(1.0f, 0.0f, 0.0f, 1.0f); + solid->color = {1.0f, 0.0f, 0.0f, 1.0f}; // Red EXPECT_EQ(solid->type(), pagx::ColorSourceType::SolidColor); EXPECT_FLOAT_EQ(solid->color.red, 1.0f); @@ -385,11 +391,11 @@ PAG_TEST(PAGXTest, ColorSources) { pagx::ColorStop stop1; stop1.offset = 0; - stop1.color = pagx::Color::FromRGBA(1.0f, 0.0f, 0.0f, 1.0f); + stop1.color = {1.0f, 0.0f, 0.0f, 1.0f}; // Red pagx::ColorStop stop2; stop2.offset = 1; - stop2.color = pagx::Color::FromRGBA(0.0f, 0.0f, 1.0f, 1.0f); + stop2.color = {0.0f, 0.0f, 1.0f, 1.0f}; // Blue linear->colorStops.push_back(stop1); linear->colorStops.push_back(stop2); @@ -422,7 +428,7 @@ PAG_TEST(PAGXTest, LayerStylesFilters) { dropShadow->offsetY = 5; dropShadow->blurrinessX = 10; dropShadow->blurrinessY = 10; - dropShadow->color = pagx::Color::FromRGBA(0, 0, 0, 0.5f); + dropShadow->color = {0.0f, 0.0f, 0.0f, 0.5f}; // Semi-transparent black layer->styles.push_back(std::move(dropShadow)); // Add blur filter From 60e3df000612c6b096b94f5f00a7d5eb63485775 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 22:42:54 +0800 Subject: [PATCH 111/678] Rename PAGXEnumUtils to PAGXStringUtils and PAGXXMLParser to PAGXImporter. --- pagx/include/pagx/model/Document.h | 32 ++--- pagx/src/PAGXDocument.cpp | 19 ++- pagx/src/PAGXExporter.cpp | 2 +- .../{PAGXXMLParser.cpp => PAGXImporter.cpp} | 116 +++++++++--------- pagx/src/{PAGXXMLParser.h => PAGXImporter.h} | 8 +- ...{PAGXEnumUtils.cpp => PAGXStringUtils.cpp} | 2 +- .../{PAGXEnumUtils.h => PAGXStringUtils.h} | 2 +- pagx/src/svg/SVGImporter.cpp | 28 ++--- 8 files changed, 96 insertions(+), 113 deletions(-) rename pagx/src/{PAGXXMLParser.cpp => PAGXImporter.cpp} (89%) rename pagx/src/{PAGXXMLParser.h => PAGXImporter.h} (96%) rename pagx/src/{PAGXEnumUtils.cpp => PAGXStringUtils.cpp} (99%) rename pagx/src/{PAGXEnumUtils.h => PAGXStringUtils.h} (98%) diff --git a/pagx/include/pagx/model/Document.h b/pagx/include/pagx/model/Document.h index b2aecf12a4..a397df3e7f 100644 --- a/pagx/include/pagx/model/Document.h +++ b/pagx/include/pagx/model/Document.h @@ -22,13 +22,12 @@ #include #include #include -#include "pagx/model/ColorSource.h" #include "pagx/model/Layer.h" #include "pagx/model/Node.h" namespace pagx { -class PAGXXMLParser; +class PAGXImporter; /** * Document is the root container for a PAGX document. @@ -53,17 +52,11 @@ class Document { float height = 0; /** - * Resources (images, compositions, etc.). - * These can be referenced by "#id" in the document. + * Resources (images, compositions, color sources, etc.). + * These can be referenced by "@id" in the document. */ std::vector> resources = {}; - /** - * Color sources (gradients, solid colors, patterns). - * These can be referenced by "#id" in fills and strokes. - */ - std::vector> colorSources = {}; - /** * Top-level layers. */ @@ -100,19 +93,13 @@ class Document { /** * Exports the document to XML format. */ - std::string toXML() const; + std::string toXML(); /** * Finds a resource by ID. * Returns nullptr if not found. */ - Node* findResource(const std::string& id) const; - - /** - * Finds a color source by ID. - * Returns nullptr if not found. - */ - ColorSource* findColorSource(const std::string& id) const; + Node* findResource(const std::string& id); /** * Finds a layer by ID (searches recursively). @@ -121,14 +108,13 @@ class Document { Layer* findLayer(const std::string& id) const; private: - friend class PAGXXMLParser; + friend class PAGXImporter; Document() = default; - mutable std::unordered_map resourceMap = {}; - mutable std::unordered_map colorSourceMap = {}; - mutable bool resourceMapDirty = true; + std::unordered_map resourceMap = {}; + bool resourceMapDirty = true; - void rebuildResourceMap() const; + void rebuildResourceMap(); static Layer* findLayerRecursive(const std::vector>& layers, const std::string& id); }; diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp index ea683b1f15..a6fa94c1d4 100644 --- a/pagx/src/PAGXDocument.cpp +++ b/pagx/src/PAGXDocument.cpp @@ -20,8 +20,8 @@ #include #include #include "pagx/model/Composition.h" -#include "PAGXXMLParser.h" -#include "PAGXXMLWriter.h" +#include "PAGXExporter.h" +#include "PAGXImporter.h" namespace pagx { @@ -54,14 +54,14 @@ std::shared_ptr Document::FromXML(const std::string& xmlContent) { } std::shared_ptr Document::FromXML(const uint8_t* data, size_t length) { - return PAGXXMLParser::Parse(data, length); + return PAGXImporter::Parse(data, length); } -std::string Document::toXML() const { - return PAGXXMLWriter::Write(*this); +std::string Document::toXML() { + return PAGXExporter::Export(*this); } -Node* Document::findResource(const std::string& id) const { +Node* Document::findResource(const std::string& id) { if (resourceMapDirty) { rebuildResourceMap(); } @@ -69,11 +69,6 @@ Node* Document::findResource(const std::string& id) const { return it != resourceMap.end() ? it->second : nullptr; } -ColorSource* Document::findColorSource(const std::string& id) const { - auto it = colorSourceMap.find(id); - return it != colorSourceMap.end() ? it->second : nullptr; -} - Layer* Document::findLayer(const std::string& id) const { // First search in top-level layers auto found = findLayerRecursive(layers, id); @@ -93,7 +88,7 @@ Layer* Document::findLayer(const std::string& id) const { return nullptr; } -void Document::rebuildResourceMap() const { +void Document::rebuildResourceMap() { resourceMap.clear(); for (const auto& resource : resources) { if (!resource->id.empty()) { diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index 7e61d82bad..654fce97a1 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -18,7 +18,7 @@ #include "PAGXExporter.h" #include -#include "PAGXEnumUtils.h" +#include "PAGXStringUtils.h" #include "pagx/model/BackgroundBlurStyle.h" #include "pagx/model/BlendFilter.h" #include "pagx/model/BlurFilter.h" diff --git a/pagx/src/PAGXXMLParser.cpp b/pagx/src/PAGXImporter.cpp similarity index 89% rename from pagx/src/PAGXXMLParser.cpp rename to pagx/src/PAGXImporter.cpp index dea2344307..94582cc3a1 100644 --- a/pagx/src/PAGXXMLParser.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -16,10 +16,10 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "PAGXXMLParser.h" +#include "PAGXImporter.h" #include #include -#include "PAGXEnumUtils.h" +#include "PAGXStringUtils.h" namespace pagx { @@ -260,7 +260,7 @@ class XMLTokenizer { // PAGXXMLParser implementation //============================================================================== -std::shared_ptr PAGXXMLParser::Parse(const uint8_t* data, size_t length) { +std::shared_ptr PAGXImporter::Parse(const uint8_t* data, size_t length) { auto root = parseXML(data, length); if (!root || root->tag != "pagx") { return nullptr; @@ -270,12 +270,12 @@ std::shared_ptr PAGXXMLParser::Parse(const uint8_t* data, size_t lengt return doc; } -std::unique_ptr PAGXXMLParser::parseXML(const uint8_t* data, size_t length) { +std::unique_ptr PAGXImporter::parseXML(const uint8_t* data, size_t length) { XMLTokenizer tokenizer(data, length); return tokenizer.parse(); } -void PAGXXMLParser::parseDocument(const XMLNode* root, Document* doc) { +void PAGXImporter::parseDocument(const XMLNode* root, Document* doc) { doc->version = getAttribute(root, "version", "1.0"); doc->width = getFloatAttribute(root, "width", 0); doc->height = getFloatAttribute(root, "height", 0); @@ -292,7 +292,7 @@ void PAGXXMLParser::parseDocument(const XMLNode* root, Document* doc) { } } -void PAGXXMLParser::parseResources(const XMLNode* node, Document* doc) { +void PAGXImporter::parseResources(const XMLNode* node, Document* doc) { for (const auto& child : node->children) { // Try to parse as a resource (including color sources) auto resource = parseResource(child.get()); @@ -308,7 +308,7 @@ void PAGXXMLParser::parseResources(const XMLNode* node, Document* doc) { } } -std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseResource(const XMLNode* node) { if (node->tag == "Image") { return parseImage(node); } @@ -321,7 +321,7 @@ std::unique_ptr PAGXXMLParser::parseResource(const XMLNode* node) { return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayer(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseLayer(const XMLNode* node) { auto layer = std::make_unique(); layer->id = getAttribute(node, "id"); layer->name = getAttribute(node, "name"); @@ -404,7 +404,7 @@ std::unique_ptr PAGXXMLParser::parseLayer(const XMLNode* node) { return layer; } -void PAGXXMLParser::parseContents(const XMLNode* node, Layer* layer) { +void PAGXImporter::parseContents(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { auto element = parseElement(child.get()); if (element) { @@ -413,7 +413,7 @@ void PAGXXMLParser::parseContents(const XMLNode* node, Layer* layer) { } } -void PAGXXMLParser::parseStyles(const XMLNode* node, Layer* layer) { +void PAGXImporter::parseStyles(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { auto style = parseLayerStyle(child.get()); if (style) { @@ -422,7 +422,7 @@ void PAGXXMLParser::parseStyles(const XMLNode* node, Layer* layer) { } } -void PAGXXMLParser::parseFilters(const XMLNode* node, Layer* layer) { +void PAGXImporter::parseFilters(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { auto filter = parseLayerFilter(child.get()); if (filter) { @@ -431,7 +431,7 @@ void PAGXXMLParser::parseFilters(const XMLNode* node, Layer* layer) { } } -std::unique_ptr PAGXXMLParser::parseElement(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseElement(const XMLNode* node) { if (node->tag == "Rectangle") { return parseRectangle(node); } @@ -480,7 +480,7 @@ std::unique_ptr PAGXXMLParser::parseElement(const XMLNode* node) { return nullptr; } -std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseColorSource(const XMLNode* node) { if (node->tag == "SolidColor") { return parseSolidColor(node); } @@ -502,7 +502,7 @@ std::unique_ptr PAGXXMLParser::parseColorSource(const XMLNode* node return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseLayerStyle(const XMLNode* node) { if (node->tag == "DropShadowStyle") { return parseDropShadowStyle(node); } @@ -515,7 +515,7 @@ std::unique_ptr PAGXXMLParser::parseLayerStyle(const XMLNode* node) return nullptr; } -std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseLayerFilter(const XMLNode* node) { if (node->tag == "BlurFilter") { return parseBlurFilter(node); } @@ -538,7 +538,7 @@ std::unique_ptr PAGXXMLParser::parseLayerFilter(const XMLNode* node // Geometry element parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseRectangle(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseRectangle(const XMLNode* node) { auto rect = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); rect->center = parsePoint(centerStr); @@ -549,7 +549,7 @@ std::unique_ptr PAGXXMLParser::parseRectangle(const XMLNode* node) { return rect; } -std::unique_ptr PAGXXMLParser::parseEllipse(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseEllipse(const XMLNode* node) { auto ellipse = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); ellipse->center = parsePoint(centerStr); @@ -559,7 +559,7 @@ std::unique_ptr PAGXXMLParser::parseEllipse(const XMLNode* node) { return ellipse; } -std::unique_ptr PAGXXMLParser::parsePolystar(const XMLNode* node) { +std::unique_ptr PAGXImporter::parsePolystar(const XMLNode* node) { auto polystar = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); polystar->center = parsePoint(centerStr); @@ -574,7 +574,7 @@ std::unique_ptr PAGXXMLParser::parsePolystar(const XMLNode* node) { return polystar; } -std::unique_ptr PAGXXMLParser::parsePath(const XMLNode* node) { +std::unique_ptr PAGXImporter::parsePath(const XMLNode* node) { auto path = std::make_unique(); auto dataAttr = getAttribute(node, "data"); if (!dataAttr.empty()) { @@ -584,7 +584,7 @@ std::unique_ptr PAGXXMLParser::parsePath(const XMLNode* node) { return path; } -std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseTextSpan(const XMLNode* node) { auto textSpan = std::make_unique(); auto positionStr = getAttribute(node, "position", "0,0"); textSpan->position = parsePoint(positionStr); @@ -602,7 +602,7 @@ std::unique_ptr PAGXXMLParser::parseTextSpan(const XMLNode* node) { // Painter parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseFill(const XMLNode* node) { auto fill = std::make_unique(); fill->colorRef = getAttribute(node, "color"); fill->alpha = getFloatAttribute(node, "alpha", 1); @@ -621,7 +621,7 @@ std::unique_ptr PAGXXMLParser::parseFill(const XMLNode* node) { return fill; } -std::unique_ptr PAGXXMLParser::parseStroke(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseStroke(const XMLNode* node) { auto stroke = std::make_unique(); stroke->colorRef = getAttribute(node, "color"); stroke->width = getFloatAttribute(node, "width", 1); @@ -653,7 +653,7 @@ std::unique_ptr PAGXXMLParser::parseStroke(const XMLNode* node) { // Modifier parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseTrimPath(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseTrimPath(const XMLNode* node) { auto trim = std::make_unique(); trim->start = getFloatAttribute(node, "start", 0); trim->end = getFloatAttribute(node, "end", 1); @@ -662,19 +662,19 @@ std::unique_ptr PAGXXMLParser::parseTrimPath(const XMLNode* node) { return trim; } -std::unique_ptr PAGXXMLParser::parseRoundCorner(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseRoundCorner(const XMLNode* node) { auto round = std::make_unique(); round->radius = getFloatAttribute(node, "radius", 0); return round; } -std::unique_ptr PAGXXMLParser::parseMergePath(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseMergePath(const XMLNode* node) { auto merge = std::make_unique(); merge->mode = MergePathModeFromString(getAttribute(node, "mode", "append")); return merge; } -std::unique_ptr PAGXXMLParser::parseTextModifier(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseTextModifier(const XMLNode* node) { auto modifier = std::make_unique(); auto anchorStr = getAttribute(node, "anchorPoint", "0.5,0.5"); modifier->anchorPoint = parsePoint(anchorStr); @@ -699,7 +699,7 @@ std::unique_ptr PAGXXMLParser::parseTextModifier(const XMLNode* no return modifier; } -std::unique_ptr PAGXXMLParser::parseTextPath(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseTextPath(const XMLNode* node) { auto textPath = std::make_unique(); textPath->path = getAttribute(node, "path"); textPath->textAlign = TextAlignFromString(getAttribute(node, "textAlign", "start")); @@ -710,7 +710,7 @@ std::unique_ptr PAGXXMLParser::parseTextPath(const XMLNode* node) { return textPath; } -std::unique_ptr PAGXXMLParser::parseTextLayout(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseTextLayout(const XMLNode* node) { auto layout = std::make_unique(); layout->x = getFloatAttribute(node, "x", 0); layout->y = getFloatAttribute(node, "y", 0); @@ -724,7 +724,7 @@ std::unique_ptr PAGXXMLParser::parseTextLayout(const XMLNode* node) return layout; } -std::unique_ptr PAGXXMLParser::parseRepeater(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseRepeater(const XMLNode* node) { auto repeater = std::make_unique(); repeater->copies = getFloatAttribute(node, "copies", 3); repeater->offset = getFloatAttribute(node, "offset", 0); @@ -741,7 +741,7 @@ std::unique_ptr PAGXXMLParser::parseRepeater(const XMLNode* node) { return repeater; } -std::unique_ptr PAGXXMLParser::parseGroup(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseGroup(const XMLNode* node) { auto group = std::make_unique(); // group->name (removed) = getAttribute(node, "name"); auto anchorStr = getAttribute(node, "anchorPoint", "0,0"); @@ -765,7 +765,7 @@ std::unique_ptr PAGXXMLParser::parseGroup(const XMLNode* node) { return group; } -RangeSelector PAGXXMLParser::parseRangeSelector(const XMLNode* node) { +RangeSelector PAGXImporter::parseRangeSelector(const XMLNode* node) { RangeSelector selector = {}; selector.start = getFloatAttribute(node, "start", 0); selector.end = getFloatAttribute(node, "end", 1); @@ -785,7 +785,7 @@ RangeSelector PAGXXMLParser::parseRangeSelector(const XMLNode* node) { // Color source parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseSolidColor(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseSolidColor(const XMLNode* node) { auto solid = std::make_unique(); solid->id = getAttribute(node, "id"); solid->color.red = getFloatAttribute(node, "red", 0); @@ -796,7 +796,7 @@ std::unique_ptr PAGXXMLParser::parseSolidColor(const XMLNode* node) return solid; } -std::unique_ptr PAGXXMLParser::parseLinearGradient(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseLinearGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto startPointStr = getAttribute(node, "startPoint", "0,0"); @@ -815,7 +815,7 @@ std::unique_ptr PAGXXMLParser::parseLinearGradient(const XMLNode return gradient; } -std::unique_ptr PAGXXMLParser::parseRadialGradient(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseRadialGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto centerStr = getAttribute(node, "center", "0,0"); @@ -833,7 +833,7 @@ std::unique_ptr PAGXXMLParser::parseRadialGradient(const XMLNode return gradient; } -std::unique_ptr PAGXXMLParser::parseConicGradient(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseConicGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto centerStr = getAttribute(node, "center", "0,0"); @@ -852,7 +852,7 @@ std::unique_ptr PAGXXMLParser::parseConicGradient(const XMLNode* return gradient; } -std::unique_ptr PAGXXMLParser::parseDiamondGradient(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseDiamondGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto centerStr = getAttribute(node, "center", "0,0"); @@ -870,7 +870,7 @@ std::unique_ptr PAGXXMLParser::parseDiamondGradient(const XMLNo return gradient; } -std::unique_ptr PAGXXMLParser::parseImagePattern(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseImagePattern(const XMLNode* node) { auto pattern = std::make_unique(); pattern->id = getAttribute(node, "id"); pattern->image = getAttribute(node, "image"); @@ -884,7 +884,7 @@ std::unique_ptr PAGXXMLParser::parseImagePattern(const XMLNode* no return pattern; } -ColorStop PAGXXMLParser::parseColorStop(const XMLNode* node) { +ColorStop PAGXImporter::parseColorStop(const XMLNode* node) { ColorStop stop = {}; stop.offset = getFloatAttribute(node, "offset", 0); auto colorStr = getAttribute(node, "color"); @@ -898,21 +898,21 @@ ColorStop PAGXXMLParser::parseColorStop(const XMLNode* node) { // Resource parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseImage(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseImage(const XMLNode* node) { auto image = std::make_unique(); image->id = getAttribute(node, "id"); image->source = getAttribute(node, "source"); return image; } -std::unique_ptr PAGXXMLParser::parsePathData(const XMLNode* node) { +std::unique_ptr PAGXImporter::parsePathData(const XMLNode* node) { auto data = getAttribute(node, "data"); auto pathData = std::make_unique(PathData::FromSVGString(data)); pathData->id = getAttribute(node, "id"); return pathData; } -std::unique_ptr PAGXXMLParser::parseComposition(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseComposition(const XMLNode* node) { auto comp = std::make_unique(); comp->id = getAttribute(node, "id"); comp->width = getFloatAttribute(node, "width", 0); @@ -932,7 +932,7 @@ std::unique_ptr PAGXXMLParser::parseComposition(const XMLNode* node // Layer style parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseDropShadowStyle(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseDropShadowStyle(const XMLNode* node) { auto style = std::make_unique(); style->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); style->offsetX = getFloatAttribute(node, "offsetX", 0); @@ -947,7 +947,7 @@ std::unique_ptr PAGXXMLParser::parseDropShadowStyle(const XMLNo return style; } -std::unique_ptr PAGXXMLParser::parseInnerShadowStyle(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseInnerShadowStyle(const XMLNode* node) { auto style = std::make_unique(); style->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); style->offsetX = getFloatAttribute(node, "offsetX", 0); @@ -961,7 +961,7 @@ std::unique_ptr PAGXXMLParser::parseInnerShadowStyle(const XML return style; } -std::unique_ptr PAGXXMLParser::parseBackgroundBlurStyle( +std::unique_ptr PAGXImporter::parseBackgroundBlurStyle( const XMLNode* node) { auto style = std::make_unique(); style->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); @@ -975,7 +975,7 @@ std::unique_ptr PAGXXMLParser::parseBackgroundBlurStyle( // Layer filter parsing //============================================================================== -std::unique_ptr PAGXXMLParser::parseBlurFilter(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseBlurFilter(const XMLNode* node) { auto filter = std::make_unique(); filter->blurrinessX = getFloatAttribute(node, "blurrinessX", 0); filter->blurrinessY = getFloatAttribute(node, "blurrinessY", 0); @@ -983,7 +983,7 @@ std::unique_ptr PAGXXMLParser::parseBlurFilter(const XMLNode* node) return filter; } -std::unique_ptr PAGXXMLParser::parseDropShadowFilter(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseDropShadowFilter(const XMLNode* node) { auto filter = std::make_unique(); filter->offsetX = getFloatAttribute(node, "offsetX", 0); filter->offsetY = getFloatAttribute(node, "offsetY", 0); @@ -997,7 +997,7 @@ std::unique_ptr PAGXXMLParser::parseDropShadowFilter(const XML return filter; } -std::unique_ptr PAGXXMLParser::parseInnerShadowFilter(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseInnerShadowFilter(const XMLNode* node) { auto filter = std::make_unique(); filter->offsetX = getFloatAttribute(node, "offsetX", 0); filter->offsetY = getFloatAttribute(node, "offsetY", 0); @@ -1011,7 +1011,7 @@ std::unique_ptr PAGXXMLParser::parseInnerShadowFilter(const X return filter; } -std::unique_ptr PAGXXMLParser::parseBlendFilter(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseBlendFilter(const XMLNode* node) { auto filter = std::make_unique(); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { @@ -1021,7 +1021,7 @@ std::unique_ptr PAGXXMLParser::parseBlendFilter(const XMLNode* node return filter; } -std::unique_ptr PAGXXMLParser::parseColorMatrixFilter(const XMLNode* node) { +std::unique_ptr PAGXImporter::parseColorMatrixFilter(const XMLNode* node) { auto filter = std::make_unique(); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { @@ -1037,13 +1037,13 @@ std::unique_ptr PAGXXMLParser::parseColorMatrixFilter(const X // Utility functions //============================================================================== -std::string PAGXXMLParser::getAttribute(const XMLNode* node, const std::string& name, +std::string PAGXImporter::getAttribute(const XMLNode* node, const std::string& name, const std::string& defaultValue) { auto it = node->attributes.find(name); return it != node->attributes.end() ? it->second : defaultValue; } -float PAGXXMLParser::getFloatAttribute(const XMLNode* node, const std::string& name, +float PAGXImporter::getFloatAttribute(const XMLNode* node, const std::string& name, float defaultValue) { auto str = getAttribute(node, name); if (str.empty()) { @@ -1056,7 +1056,7 @@ float PAGXXMLParser::getFloatAttribute(const XMLNode* node, const std::string& n } } -int PAGXXMLParser::getIntAttribute(const XMLNode* node, const std::string& name, int defaultValue) { +int PAGXImporter::getIntAttribute(const XMLNode* node, const std::string& name, int defaultValue) { auto str = getAttribute(node, name); if (str.empty()) { return defaultValue; @@ -1068,7 +1068,7 @@ int PAGXXMLParser::getIntAttribute(const XMLNode* node, const std::string& name, } } -bool PAGXXMLParser::getBoolAttribute(const XMLNode* node, const std::string& name, +bool PAGXImporter::getBoolAttribute(const XMLNode* node, const std::string& name, bool defaultValue) { auto str = getAttribute(node, name); if (str.empty()) { @@ -1077,7 +1077,7 @@ bool PAGXXMLParser::getBoolAttribute(const XMLNode* node, const std::string& nam return str == "true" || str == "1"; } -Point PAGXXMLParser::parsePoint(const std::string& str) { +Point PAGXImporter::parsePoint(const std::string& str) { Point point = {}; std::istringstream iss(str); std::string token = {}; @@ -1097,7 +1097,7 @@ Point PAGXXMLParser::parsePoint(const std::string& str) { return point; } -Size PAGXXMLParser::parseSize(const std::string& str) { +Size PAGXImporter::parseSize(const std::string& str) { Size size = {}; std::istringstream iss(str); std::string token = {}; @@ -1117,7 +1117,7 @@ Size PAGXXMLParser::parseSize(const std::string& str) { return size; } -Rect PAGXXMLParser::parseRect(const std::string& str) { +Rect PAGXImporter::parseRect(const std::string& str) { Rect rect = {}; std::istringstream iss(str); std::string token = {}; @@ -1154,7 +1154,7 @@ int parseHexDigit(char c) { } } // namespace -Color PAGXXMLParser::parseColor(const std::string& str) { +Color PAGXImporter::parseColor(const std::string& str) { if (str.empty()) { return {}; } @@ -1254,7 +1254,7 @@ Color PAGXXMLParser::parseColor(const std::string& str) { return {}; } -std::vector PAGXXMLParser::parseFloatList(const std::string& str) { +std::vector PAGXImporter::parseFloatList(const std::string& str) { std::vector values = {}; std::istringstream iss(str); std::string token = {}; diff --git a/pagx/src/PAGXXMLParser.h b/pagx/src/PAGXImporter.h similarity index 96% rename from pagx/src/PAGXXMLParser.h rename to pagx/src/PAGXImporter.h index 8d101ea744..c44262c088 100644 --- a/pagx/src/PAGXXMLParser.h +++ b/pagx/src/PAGXImporter.h @@ -42,7 +42,7 @@ #include "pagx/model/LinearGradient.h" #include "pagx/model/MergePath.h" #include "pagx/model/Path.h" -#include "pagx/model/PathDataResource.h" +#include "pagx/model/PathData.h" #include "pagx/model/Polystar.h" #include "pagx/model/RadialGradient.h" #include "pagx/model/RangeSelector.h" @@ -56,6 +56,7 @@ #include "pagx/model/TextPath.h" #include "pagx/model/TextSpan.h" #include "pagx/model/TrimPath.h" +#include "pagx/model/types/Color.h" namespace pagx { @@ -72,7 +73,7 @@ struct XMLNode { /** * Parser for PAGX XML format. */ -class PAGXXMLParser { +class PAGXImporter { public: /** * Parses XML data into a PAGXDocument. @@ -121,7 +122,7 @@ class PAGXXMLParser { static ColorStop parseColorStop(const XMLNode* node); static std::unique_ptr parseImage(const XMLNode* node); - static std::unique_ptr parsePathData(const XMLNode* node); + static std::unique_ptr parsePathData(const XMLNode* node); static std::unique_ptr parseComposition(const XMLNode* node); static std::unique_ptr parseDropShadowStyle(const XMLNode* node); @@ -144,6 +145,7 @@ class PAGXXMLParser { static Point parsePoint(const std::string& str); static Size parseSize(const std::string& str); static Rect parseRect(const std::string& str); + static Color parseColor(const std::string& str); static std::vector parseFloatList(const std::string& str); }; diff --git a/pagx/src/PAGXEnumUtils.cpp b/pagx/src/PAGXStringUtils.cpp similarity index 99% rename from pagx/src/PAGXEnumUtils.cpp rename to pagx/src/PAGXStringUtils.cpp index 0153dae790..6f5ef8ebf3 100644 --- a/pagx/src/PAGXEnumUtils.cpp +++ b/pagx/src/PAGXStringUtils.cpp @@ -16,7 +16,7 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "PAGXEnumUtils.h" +#include "PAGXStringUtils.h" #include #include #include diff --git a/pagx/src/PAGXEnumUtils.h b/pagx/src/PAGXStringUtils.h similarity index 98% rename from pagx/src/PAGXEnumUtils.h rename to pagx/src/PAGXStringUtils.h index 0f31e0f28a..0bb1715fce 100644 --- a/pagx/src/PAGXEnumUtils.h +++ b/pagx/src/PAGXStringUtils.h @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// Copyright (C) 2026 Tencent. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 9029ef2d79..fe204dd183 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -21,7 +21,7 @@ #include #include #include -#include "PAGXEnumUtils.h" +#include "PAGXStringUtils.h" #include "pagx/model/Document.h" #include "pagx/model/SolidColor.h" #include "SVGParserInternal.h" @@ -555,15 +555,15 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr // SVG text-anchor maps to PAGX TextLayout.textAlign: // start -> Left (default, no TextLayout needed) // middle -> Center - // end -> Right + // end -> End if (!anchor.empty() && anchor != "start") { auto textLayout = std::make_unique(); - textLayout->width = 0; // auto-width + textLayout->width = 0; // auto-width (Point Text mode) textLayout->height = 0; // auto-height if (anchor == "middle") { textLayout->textAlign = TextAlign::Center; } else if (anchor == "end") { - textLayout->textAlign = TextAlign::Right; + textLayout->textAlign = TextAlign::End; } group->elements.push_back(std::move(textLayout)); } @@ -1665,26 +1665,26 @@ std::string SVGParserImpl::registerImageResource(const std::string& imageSource) // Helper function to check if two VectorElement nodes are the same geometry. static bool isSameGeometry(const Element* a, const Element* b) { - if (!a || !b || a->type() != b->type()) { + if (!a || !b || a->nodeType() != b->nodeType()) { return false; } - switch (a->type()) { - case ElementType::Rectangle: { + switch (a->nodeType()) { + case NodeType::Rectangle: { auto rectA = static_cast(a); auto rectB = static_cast(b); return rectA->center.x == rectB->center.x && rectA->center.y == rectB->center.y && rectA->size.width == rectB->size.width && rectA->size.height == rectB->size.height && rectA->roundness == rectB->roundness; } - case ElementType::Ellipse: { + case NodeType::Ellipse: { auto ellipseA = static_cast(a); auto ellipseB = static_cast(b); return ellipseA->center.x == ellipseB->center.x && ellipseA->center.y == ellipseB->center.y && ellipseA->size.width == ellipseB->size.width && ellipseA->size.height == ellipseB->size.height; } - case ElementType::Path: { + case NodeType::Path: { auto pathA = static_cast(a); auto pathB = static_cast(b); return pathA->data.toSVGString() == pathB->data.toSVGString(); @@ -1711,10 +1711,10 @@ static bool isSimpleShapeLayer(const Layer* layer, const Element*& outGeometry, const auto* second = layer->contents[1].get(); // Check if first is geometry and second is painter. - bool firstIsGeometry = (first->type() == ElementType::Rectangle || - first->type() == ElementType::Ellipse || first->type() == ElementType::Path); + bool firstIsGeometry = (first->nodeType() == NodeType::Rectangle || + first->nodeType() == NodeType::Ellipse || first->nodeType() == NodeType::Path); bool secondIsPainter = - (second->type() == ElementType::Fill || second->type() == ElementType::Stroke); + (second->nodeType() == NodeType::Fill || second->nodeType() == NodeType::Stroke); if (firstIsGeometry && secondIsPainter) { outGeometry = first; @@ -1745,8 +1745,8 @@ void SVGParserImpl::mergeAdjacentLayers(std::vector>& lay if (isSimpleShapeLayer(layers[i + 1].get(), geomB, painterB) && isSameGeometry(geomA, geomB)) { // Merge: one has Fill, the other has Stroke. - bool aHasFill = (painterA->type() == ElementType::Fill); - bool bHasFill = (painterB->type() == ElementType::Fill); + bool aHasFill = (painterA->nodeType() == NodeType::Fill); + bool bHasFill = (painterB->nodeType() == NodeType::Fill); if (aHasFill != bHasFill) { // Create merged layer. From 406a07fa4b9e77b6d0704707a68493cd94f61bb7 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 23:10:00 +0800 Subject: [PATCH 112/678] Refactor PAGX module: rename model to nodes, decouple PAGXDocument from I/O. - Rename pagx/include/pagx/model/ to pagx/include/pagx/nodes/ - Flatten types/ subdirectory into nodes/ - Rename Document to PAGXDocument and move to pagx/include/pagx/ - Create independent PAGXImporter and PAGXExporter public APIs - Keep internal implementation in PAGXImporterImpl and PAGXExporterImpl - PAGXDocument is now a pure data structure without I/O logic - Update all include paths and dependent files --- DEPS | 2 +- pagx/docs/pagx_spec.md | 42 +- pagx/include/pagx/LayerBuilder.h | 4 +- .../pagx/{model/Document.h => PAGXDocument.h} | 50 +- pagx/include/pagx/PAGXExporter.h | 44 + .../{model/ImagePattern.h => PAGXImporter.h} | 48 +- pagx/include/pagx/SVGImporter.h | 8 +- pagx/include/pagx/model/Node.h | 72 - .../BackgroundBlurStyle.h} | 34 +- .../BlendFilter.h} | 25 +- .../pagx/{model/types => nodes}/BlendMode.h | 5 - .../DropShadowFilter.h => nodes/BlurFilter.h} | 28 +- .../pagx/{model/types => nodes}/Color.h | 2 +- .../ColorMatrixFilter.h} | 41 +- .../pagx/{model => nodes}/ColorSource.h | 12 +- .../PathDataResource.h => nodes/ColorSpace.h} | 22 +- .../include/pagx/{model => nodes}/ColorStop.h | 14 +- .../pagx/{model => nodes}/Composition.h | 2 +- .../pagx/{model => nodes}/ConicGradient.h | 35 +- .../pagx/{model => nodes}/DiamondGradient.h | 31 +- pagx/include/pagx/nodes/DropShadowFilter.h | 66 + pagx/include/pagx/nodes/DropShadowStyle.h | 72 + pagx/include/pagx/nodes/Element.h | 38 + pagx/include/pagx/{model => nodes}/Ellipse.h | 10 +- pagx/include/pagx/{model => nodes}/Fill.h | 8 +- .../FilterMode.h} | 23 +- pagx/include/pagx/{model => nodes}/Group.h | 8 +- pagx/include/pagx/{model => nodes}/Image.h | 2 +- pagx/include/pagx/nodes/ImagePattern.h | 79 + .../pagx/{model => nodes}/InnerShadowFilter.h | 33 +- pagx/include/pagx/nodes/InnerShadowStyle.h | 67 + pagx/include/pagx/{model => nodes}/Layer.h | 30 +- .../BlurFilter.h => nodes/LayerFilter.h} | 16 +- .../{model/types => nodes}/LayerPlacement.h | 0 .../pagx/nodes/LayerStyle.h} | 15 +- .../pagx/{model => nodes}/LinearGradient.h | 31 +- .../pagx/{model/types => nodes}/Matrix.h | 37 +- .../include/pagx/{model => nodes}/MergePath.h | 10 +- pagx/include/pagx/nodes/MipmapMode.h | 41 + .../pagx/{model/Element.h => nodes/Node.h} | 130 +- pagx/include/pagx/{model => nodes}/Path.h | 14 +- pagx/include/pagx/{model => nodes}/PathData.h | 18 +- .../pagx/{model/types => nodes}/Point.h | 7 + pagx/include/pagx/{model => nodes}/Polystar.h | 14 +- .../pagx/{model => nodes}/RadialGradient.h | 31 +- .../pagx/{model => nodes}/RangeSelector.h | 65 +- .../pagx/{model/types => nodes}/Rect.h | 15 + .../include/pagx/{model => nodes}/Rectangle.h | 10 +- pagx/include/pagx/{model => nodes}/Repeater.h | 12 +- .../pagx/{model => nodes}/RoundCorner.h | 6 +- .../pagx/{model/types => nodes}/Size.h | 7 + .../pagx/{model => nodes}/SolidColor.h | 15 +- pagx/include/pagx/{model => nodes}/Stroke.h | 8 +- .../{model/LayerStyle.h => nodes/TextAlign.h} | 46 +- .../pagx/{model => nodes}/TextLayout.h | 76 +- .../pagx/{model => nodes}/TextModifier.h | 10 +- pagx/include/pagx/{model => nodes}/TextPath.h | 35 +- .../pagx/{model => nodes}/TextSelector.h | 14 +- pagx/include/pagx/{model => nodes}/TextSpan.h | 8 +- .../pagx/{model/types => nodes}/TileMode.h | 5 - pagx/include/pagx/{model => nodes}/TrimPath.h | 19 +- pagx/src/PAGXDocument.cpp | 51 +- pagx/src/PAGXElement.cpp | 126 -- pagx/src/PAGXExporter.cpp | 92 +- .../PAGXExporterAPI.cpp} | 31 +- .../BlendFilter.h => src/PAGXExporterImpl.h} | 21 +- pagx/src/PAGXImporter.cpp | 116 +- pagx/src/PAGXImporterAPI.cpp | 51 + .../{PAGXImporter.h => PAGXImporterImpl.h} | 80 +- pagx/src/PAGXStringUtils.cpp | 50 +- pagx/src/PAGXStringUtils.h | 52 +- pagx/src/PAGXTypes.cpp | 182 --- pagx/src/PAGXXMLWriter.cpp | 1445 ----------------- pagx/src/PathData.cpp | 4 +- pagx/src/svg/SVGImporter.cpp | 18 +- pagx/src/svg/SVGParserInternal.h | 61 +- pagx/src/tgfx/LayerBuilder.cpp | 79 +- test/src/PAGXTest.cpp | 42 +- 78 files changed, 1487 insertions(+), 2686 deletions(-) rename pagx/include/pagx/{model/Document.h => PAGXDocument.h} (69%) create mode 100644 pagx/include/pagx/PAGXExporter.h rename pagx/include/pagx/{model/ImagePattern.h => PAGXImporter.h} (59%) delete mode 100644 pagx/include/pagx/model/Node.h rename pagx/include/pagx/{model/DropShadowStyle.h => nodes/BackgroundBlurStyle.h} (61%) rename pagx/include/pagx/{model/InnerShadowStyle.h => nodes/BlendFilter.h} (69%) rename pagx/include/pagx/{model/types => nodes}/BlendMode.h (91%) rename pagx/include/pagx/{model/DropShadowFilter.h => nodes/BlurFilter.h} (66%) rename pagx/include/pagx/{model/types => nodes}/Color.h (98%) rename pagx/include/pagx/{model/LayerFilter.h => nodes/ColorMatrixFilter.h} (55%) rename pagx/include/pagx/{model => nodes}/ColorSource.h (89%) rename pagx/include/pagx/{model/PathDataResource.h => nodes/ColorSpace.h} (70%) rename pagx/include/pagx/{model => nodes}/ColorStop.h (79%) rename pagx/include/pagx/{model => nodes}/Composition.h (98%) rename pagx/include/pagx/{model => nodes}/ConicGradient.h (64%) rename pagx/include/pagx/{model => nodes}/DiamondGradient.h (69%) create mode 100644 pagx/include/pagx/nodes/DropShadowFilter.h create mode 100644 pagx/include/pagx/nodes/DropShadowStyle.h create mode 100644 pagx/include/pagx/nodes/Element.h rename pagx/include/pagx/{model => nodes}/Ellipse.h (88%) rename pagx/include/pagx/{model => nodes}/Fill.h (94%) rename pagx/include/pagx/{model/ColorMatrixFilter.h => nodes/FilterMode.h} (69%) rename pagx/include/pagx/{model => nodes}/Group.h (93%) rename pagx/include/pagx/{model => nodes}/Image.h (97%) create mode 100644 pagx/include/pagx/nodes/ImagePattern.h rename pagx/include/pagx/{model => nodes}/InnerShadowFilter.h (62%) create mode 100644 pagx/include/pagx/nodes/InnerShadowStyle.h rename pagx/include/pagx/{model => nodes}/Layer.h (87%) rename pagx/include/pagx/{model/BlurFilter.h => nodes/LayerFilter.h} (76%) rename pagx/include/pagx/{model/types => nodes}/LayerPlacement.h (100%) rename pagx/{src/PAGXXMLWriter.h => include/pagx/nodes/LayerStyle.h} (81%) rename pagx/include/pagx/{model => nodes}/LinearGradient.h (69%) rename pagx/include/pagx/{model/types => nodes}/Matrix.h (89%) rename pagx/include/pagx/{model => nodes}/MergePath.h (86%) create mode 100644 pagx/include/pagx/nodes/MipmapMode.h rename pagx/include/pagx/{model/Element.h => nodes/Node.h} (50%) rename pagx/include/pagx/{model => nodes}/Path.h (81%) rename pagx/include/pagx/{model => nodes}/PathData.h (92%) rename pagx/include/pagx/{model/types => nodes}/Point.h (94%) rename pagx/include/pagx/{model => nodes}/Polystar.h (86%) rename pagx/include/pagx/{model => nodes}/RadialGradient.h (69%) rename pagx/include/pagx/{model => nodes}/RangeSelector.h (54%) rename pagx/include/pagx/{model/types => nodes}/Rect.h (90%) rename pagx/include/pagx/{model => nodes}/Rectangle.h (89%) rename pagx/include/pagx/{model => nodes}/Repeater.h (89%) rename pagx/include/pagx/{model => nodes}/RoundCorner.h (92%) rename pagx/include/pagx/{model/types => nodes}/Size.h (94%) rename pagx/include/pagx/{model => nodes}/SolidColor.h (81%) rename pagx/include/pagx/{model => nodes}/Stroke.h (95%) rename pagx/include/pagx/{model/LayerStyle.h => nodes/TextAlign.h} (60%) rename pagx/include/pagx/{model => nodes}/TextLayout.h (51%) rename pagx/include/pagx/{model => nodes}/TextModifier.h (93%) rename pagx/include/pagx/{model => nodes}/TextPath.h (71%) rename pagx/include/pagx/{model => nodes}/TextSelector.h (87%) rename pagx/include/pagx/{model => nodes}/TextSpan.h (93%) rename pagx/include/pagx/{model/types => nodes}/TileMode.h (90%) rename pagx/include/pagx/{model => nodes}/TrimPath.h (78%) delete mode 100644 pagx/src/PAGXElement.cpp rename pagx/{include/pagx/model/BackgroundBlurStyle.h => src/PAGXExporterAPI.cpp} (68%) rename pagx/{include/pagx/model/BlendFilter.h => src/PAGXExporterImpl.h} (73%) create mode 100644 pagx/src/PAGXImporterAPI.cpp rename pagx/src/{PAGXImporter.h => PAGXImporterImpl.h} (78%) delete mode 100644 pagx/src/PAGXTypes.cpp delete mode 100644 pagx/src/PAGXXMLWriter.cpp diff --git a/DEPS b/DEPS index dbca07a4fb..94acd5ad88 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "f96be663690a908ec7a59481dd65b5cedd99854d", + "commit": "9b732d8a3810d71b5a69ee735635baeb79a09cbe", "dir": "third_party/tgfx" }, { diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index c6b9b0d051..43f5552ba2 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -311,18 +311,12 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 ##### 纯色(SolidColor) ```xml - - - + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `red` | float | 0 | 红色分量,sRGB 为 0.0~1.0,广色域可超出 | -| `green` | float | 0 | 绿色分量 | -| `blue` | float | 0 | 蓝色分量 | -| `alpha` | float | 1 | 透明度,0.0~1.0 | -| `colorSpace` | ColorSpace | sRGB | 色域:`sRGB` 或 `displayP3` | +| `color` | color | (必填) | 颜色值 | ##### 线性渐变(LinearGradient) @@ -1058,9 +1052,7 @@ y = center.y + outerRadius * sin(angle) ```xml - - - + @@ -1081,7 +1073,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | idref | - | 颜色源引用(如 `@gradientId`) | +| `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | | `fillRule` | FillRule | winding | 填充规则(见下方) | @@ -1107,14 +1099,10 @@ y = center.y + outerRadius * sin(angle) ```xml - - - + - - - + @@ -1127,7 +1115,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | idref | - | 颜色源引用(如 `@gradientId`) | +| `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | | `width` | float | 1 | 描边宽度 | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | @@ -1196,7 +1184,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: |------|------|--------|------| | `start` | float | 0 | 起始位置 0~1 | | `end` | float | 1 | 结束位置 0~1 | -| `offset` | float | 0 | 偏移量(度) | +| `offset` | float | 0 | 偏移量(度),360 度表示完整路径长度的一个周期。例如,180 度将裁剪范围偏移半个路径长度 | | `type` | TrimType | separate | 裁剪类型(见下方) | **TrimType(裁剪类型)**: @@ -2052,11 +2040,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `red` | float | 0 | -| `green` | float | 0 | -| `blue` | float | 0 | -| `alpha` | float | 1 | -| `colorSpace` | ColorSpace | sRGB | +| `color` | color | (必填) | #### LinearGradient @@ -2281,19 +2265,17 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `color` | idref | - | +| `color` | color/idref | #000000 | | `alpha` | float | 1 | | `blendMode` | BlendMode | normal | | `fillRule` | FillRule | winding | | `placement` | LayerPlacement | background | -子元素:ColorSource(SolidColor、LinearGradient 等) - #### Stroke | 属性 | 类型 | 默认值 | |------|------|--------| -| `color` | idref | - | +| `color` | color/idref | #000000 | | `width` | float | 1 | | `alpha` | float | 1 | | `blendMode` | BlendMode | normal | @@ -2305,8 +2287,6 @@ Layer / Group | `align` | StrokeAlign | center | | `placement` | LayerPlacement | background | -子元素:ColorSource(SolidColor、LinearGradient 等) - ### C.8 形状修改器节点 #### TrimPath diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index 3b605aa743..c84f09ecf2 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -21,7 +21,7 @@ #include #include #include -#include "pagx/model/Document.h" +#include "pagx/PAGXDocument.h" #include "tgfx/core/Typeface.h" #include "tgfx/layers/Layer.h" @@ -81,7 +81,7 @@ class LayerBuilder { /** * Builds a layer tree from a PAGXDocument. */ - static PAGXContent Build(const Document& document, const Options& options = Options()); + static PAGXContent Build(const PAGXDocument& document, const Options& options = Options()); /** * Builds a layer tree from a PAGX file. diff --git a/pagx/include/pagx/model/Document.h b/pagx/include/pagx/PAGXDocument.h similarity index 69% rename from pagx/include/pagx/model/Document.h rename to pagx/include/pagx/PAGXDocument.h index a397df3e7f..b32af3444d 100644 --- a/pagx/include/pagx/model/Document.h +++ b/pagx/include/pagx/PAGXDocument.h @@ -22,20 +22,23 @@ #include #include #include -#include "pagx/model/Layer.h" -#include "pagx/model/Node.h" +#include "pagx/nodes/Layer.h" +#include "pagx/nodes/Node.h" namespace pagx { -class PAGXImporter; - /** - * Document is the root container for a PAGX document. - * It contains resources and layers, and provides methods for loading, saving, and manipulating - * the document. + * PAGXDocument is the root container for a PAGX document. + * It contains resources and layers. This is a pure data structure class. + * Use PAGXImporter to load documents and PAGXExporter to save documents. */ -class Document { +class PAGXDocument { public: + /** + * Creates an empty document with the specified size. + */ + static std::shared_ptr Make(float width, float height); + /** * Format version. */ @@ -67,34 +70,6 @@ class Document { */ std::string basePath = {}; - /** - * Creates an empty document with the specified size. - */ - static std::shared_ptr Make(float width, float height); - - /** - * Loads a document from a file. - * Returns nullptr if the file cannot be loaded. - */ - static std::shared_ptr FromFile(const std::string& filePath); - - /** - * Parses a document from XML content. - * Returns nullptr if parsing fails. - */ - static std::shared_ptr FromXML(const std::string& xmlContent); - - /** - * Parses a document from XML data. - * Returns nullptr if parsing fails. - */ - static std::shared_ptr FromXML(const uint8_t* data, size_t length); - - /** - * Exports the document to XML format. - */ - std::string toXML(); - /** * Finds a resource by ID. * Returns nullptr if not found. @@ -108,9 +83,6 @@ class Document { Layer* findLayer(const std::string& id) const; private: - friend class PAGXImporter; - Document() = default; - std::unordered_map resourceMap = {}; bool resourceMapDirty = true; diff --git a/pagx/include/pagx/PAGXExporter.h b/pagx/include/pagx/PAGXExporter.h new file mode 100644 index 0000000000..9c6c56110c --- /dev/null +++ b/pagx/include/pagx/PAGXExporter.h @@ -0,0 +1,44 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/PAGXDocument.h" + +namespace pagx { + +/** + * PAGXExporter exports PAGXDocument to PAGX XML format. + */ +class PAGXExporter { + public: + /** + * Exports a PAGXDocument to XML string. + * The output faithfully reflects the structure of the input document. + */ + static std::string ToXML(const PAGXDocument& document); + + /** + * Exports a PAGXDocument to a file. + * Returns true if successful, false otherwise. + */ + static bool ToFile(const PAGXDocument& document, const std::string& filePath); +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/ImagePattern.h b/pagx/include/pagx/PAGXImporter.h similarity index 59% rename from pagx/include/pagx/model/ImagePattern.h rename to pagx/include/pagx/PAGXImporter.h index e2cce77eb0..bc8f634984 100644 --- a/pagx/include/pagx/model/ImagePattern.h +++ b/pagx/include/pagx/PAGXImporter.h @@ -18,40 +18,34 @@ #pragma once +#include #include -#include "pagx/model/ColorSource.h" -#include "pagx/model/types/Matrix.h" -#include "pagx/model/types/TileMode.h" +#include "pagx/PAGXDocument.h" namespace pagx { /** - * Sampling modes for images. + * PAGXImporter parses PAGX XML format into PAGXDocument. */ -enum class SamplingMode { - Nearest, - Linear, - Mipmap -}; - -std::string SamplingModeToString(SamplingMode mode); -SamplingMode SamplingModeFromString(const std::string& str); - -/** - * An image pattern. - */ -class ImagePattern : public ColorSource { +class PAGXImporter { public: - std::string id = {}; - std::string image = {}; - TileMode tileModeX = TileMode::Clamp; - TileMode tileModeY = TileMode::Clamp; - SamplingMode sampling = SamplingMode::Linear; - Matrix matrix = {}; - - ColorSourceType type() const override { - return ColorSourceType::ImagePattern; - } + /** + * Parses a PAGX file and returns a PAGXDocument. + * Returns nullptr if the file cannot be loaded or parsing fails. + */ + static std::shared_ptr FromFile(const std::string& filePath); + + /** + * Parses PAGX XML content and returns a PAGXDocument. + * Returns nullptr if parsing fails. + */ + static std::shared_ptr FromXML(const std::string& xmlContent); + + /** + * Parses PAGX XML data and returns a PAGXDocument. + * Returns nullptr if parsing fails. + */ + static std::shared_ptr FromXML(const uint8_t* data, size_t length); }; } // namespace pagx diff --git a/pagx/include/pagx/SVGImporter.h b/pagx/include/pagx/SVGImporter.h index 111d97ae66..bfd299bee7 100644 --- a/pagx/include/pagx/SVGImporter.h +++ b/pagx/include/pagx/SVGImporter.h @@ -20,7 +20,7 @@ #include #include -#include "pagx/model/Document.h" +#include "pagx/PAGXDocument.h" namespace pagx { @@ -53,19 +53,19 @@ class SVGImporter { /** * Parses an SVG file and creates a PAGX Document. */ - static std::shared_ptr Parse(const std::string& filePath, + static std::shared_ptr Parse(const std::string& filePath, const Options& options = Options()); /** * Parses SVG data and creates a PAGX Document. */ - static std::shared_ptr Parse(const uint8_t* data, size_t length, + static std::shared_ptr Parse(const uint8_t* data, size_t length, const Options& options = Options()); /** * Parses an SVG string and creates a PAGX Document. */ - static std::shared_ptr ParseString(const std::string& svgContent, + static std::shared_ptr ParseString(const std::string& svgContent, const Options& options = Options()); }; diff --git a/pagx/include/pagx/model/Node.h b/pagx/include/pagx/model/Node.h deleted file mode 100644 index 5ed1fd8b32..0000000000 --- a/pagx/include/pagx/model/Node.h +++ /dev/null @@ -1,72 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include - -namespace pagx { - -/** - * NodeType enumerates all types of nodes that can be stored in a PAGX document. - * This includes resources (Image, Composition) and other shared definitions. - */ -enum class NodeType { - /** - * An image resource. - */ - Image, - /** - * A reusable path data resource. - */ - PathData, - /** - * A composition resource containing layers. - */ - Composition -}; - -/** - * Returns the string name of a node type. - */ -const char* NodeTypeName(NodeType type); - -/** - * Node is the base class for all shareable nodes in a PAGX document. Nodes can be stored in the - * document's resources list and referenced by ID (e.g., "#imageId"). Each node has a unique - * identifier and a type. - */ -class Node { - public: - /** - * The unique identifier of this node. Used for referencing the node by ID (e.g., "#id"). - */ - std::string id = {}; - - virtual ~Node() = default; - - /** - * Returns the node type of this node. - */ - virtual NodeType nodeType() const = 0; - - protected: - Node() = default; -}; - -} // namespace pagx diff --git a/pagx/include/pagx/model/DropShadowStyle.h b/pagx/include/pagx/nodes/BackgroundBlurStyle.h similarity index 61% rename from pagx/include/pagx/model/DropShadowStyle.h rename to pagx/include/pagx/nodes/BackgroundBlurStyle.h index b6c3426ccd..1c584ca803 100644 --- a/pagx/include/pagx/model/DropShadowStyle.h +++ b/pagx/include/pagx/nodes/BackgroundBlurStyle.h @@ -18,27 +18,39 @@ #pragma once -#include "pagx/model/LayerStyle.h" -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Color.h" +#include "pagx/nodes/LayerStyle.h" +#include "pagx/nodes/BlendMode.h" +#include "pagx/nodes/TileMode.h" namespace pagx { /** - * Drop shadow style. + * A background blur layer style that blurs the content behind the layer. */ -class DropShadowStyle : public LayerStyle { +class BackgroundBlurStyle : public LayerStyle { public: - float offsetX = 0; - float offsetY = 0; + /** + * The horizontal blur radius in pixels. The default value is 0. + */ float blurrinessX = 0; + + /** + * The vertical blur radius in pixels. The default value is 0. + */ float blurrinessY = 0; - Color color = {}; - bool showBehindLayer = true; + + /** + * The tile mode for handling blur edges. The default value is Mirror. + */ + TileMode tileMode = TileMode::Mirror; + + /** + * The blend mode used when compositing the blur. The default value is Normal. + */ BlendMode blendMode = BlendMode::Normal; - LayerStyleType type() const override { - return LayerStyleType::DropShadowStyle; + NodeType nodeType() const override { + return NodeType::BackgroundBlurStyle; } }; diff --git a/pagx/include/pagx/model/InnerShadowStyle.h b/pagx/include/pagx/nodes/BlendFilter.h similarity index 69% rename from pagx/include/pagx/model/InnerShadowStyle.h rename to pagx/include/pagx/nodes/BlendFilter.h index 1cc3bb7624..03ca739883 100644 --- a/pagx/include/pagx/model/InnerShadowStyle.h +++ b/pagx/include/pagx/nodes/BlendFilter.h @@ -18,26 +18,29 @@ #pragma once -#include "pagx/model/LayerStyle.h" -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Color.h" +#include "pagx/nodes/LayerFilter.h" +#include "pagx/nodes/BlendMode.h" +#include "pagx/nodes/Color.h" namespace pagx { /** - * Inner shadow style. + * A blend filter that blends a color with the layer content using a specified blend mode. */ -class InnerShadowStyle : public LayerStyle { +class BlendFilter : public LayerFilter { public: - float offsetX = 0; - float offsetY = 0; - float blurrinessX = 0; - float blurrinessY = 0; + /** + * The color to blend with the layer content. + */ Color color = {}; + + /** + * The blend mode used for combining the color with the layer. The default value is Normal. + */ BlendMode blendMode = BlendMode::Normal; - LayerStyleType type() const override { - return LayerStyleType::InnerShadowStyle; + NodeType nodeType() const override { + return NodeType::BlendFilter; } }; diff --git a/pagx/include/pagx/model/types/BlendMode.h b/pagx/include/pagx/nodes/BlendMode.h similarity index 91% rename from pagx/include/pagx/model/types/BlendMode.h rename to pagx/include/pagx/nodes/BlendMode.h index 10b90cbdcb..dc900be275 100644 --- a/pagx/include/pagx/model/types/BlendMode.h +++ b/pagx/include/pagx/nodes/BlendMode.h @@ -18,8 +18,6 @@ #pragma once -#include - namespace pagx { /** @@ -46,7 +44,4 @@ enum class BlendMode { PlusDarker }; -std::string BlendModeToString(BlendMode mode); -BlendMode BlendModeFromString(const std::string& str); - } // namespace pagx diff --git a/pagx/include/pagx/model/DropShadowFilter.h b/pagx/include/pagx/nodes/BlurFilter.h similarity index 66% rename from pagx/include/pagx/model/DropShadowFilter.h rename to pagx/include/pagx/nodes/BlurFilter.h index fc97c755ae..7b98c59a91 100644 --- a/pagx/include/pagx/model/DropShadowFilter.h +++ b/pagx/include/pagx/nodes/BlurFilter.h @@ -18,25 +18,33 @@ #pragma once -#include "pagx/model/LayerFilter.h" -#include "pagx/model/types/Color.h" +#include "pagx/nodes/LayerFilter.h" +#include "pagx/nodes/TileMode.h" namespace pagx { /** - * Drop shadow filter. + * A blur filter that applies a Gaussian blur effect to the layer. */ -class DropShadowFilter : public LayerFilter { +class BlurFilter : public LayerFilter { public: - float offsetX = 0; - float offsetY = 0; + /** + * The horizontal blur radius in pixels. The default value is 0. + */ float blurrinessX = 0; + + /** + * The vertical blur radius in pixels. The default value is 0. + */ float blurrinessY = 0; - Color color = {}; - bool shadowOnly = false; - LayerFilterType type() const override { - return LayerFilterType::DropShadowFilter; + /** + * The tile mode for handling blur edges. The default value is Decal. + */ + TileMode tileMode = TileMode::Decal; + + NodeType nodeType() const override { + return NodeType::BlurFilter; } }; diff --git a/pagx/include/pagx/model/types/Color.h b/pagx/include/pagx/nodes/Color.h similarity index 98% rename from pagx/include/pagx/model/types/Color.h rename to pagx/include/pagx/nodes/Color.h index af9c3a9e11..04d7a7543b 100644 --- a/pagx/include/pagx/model/types/Color.h +++ b/pagx/include/pagx/nodes/Color.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/model/types/ColorSpace.h" +#include "pagx/nodes/ColorSpace.h" namespace pagx { diff --git a/pagx/include/pagx/model/LayerFilter.h b/pagx/include/pagx/nodes/ColorMatrixFilter.h similarity index 55% rename from pagx/include/pagx/model/LayerFilter.h rename to pagx/include/pagx/nodes/ColorMatrixFilter.h index 4681bf5d8a..820944545d 100644 --- a/pagx/include/pagx/model/LayerFilter.h +++ b/pagx/include/pagx/nodes/ColorMatrixFilter.h @@ -18,38 +18,31 @@ #pragma once -namespace pagx { - -/** - * Layer filter types. - */ -enum class LayerFilterType { - BlurFilter, - DropShadowFilter, - InnerShadowFilter, - BlendFilter, - ColorMatrixFilter -}; +#include +#include "pagx/nodes/LayerFilter.h" -/** - * Returns the string name of a layer filter type. - */ -const char* LayerFilterTypeName(LayerFilterType type); +namespace pagx { /** - * Base class for layer filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, ColorMatrixFilter). + * A color matrix filter that transforms the layer colors using a 4x5 matrix. + * The matrix is applied as: [R' G' B' A'] = [R G B A 1] * matrix */ -class LayerFilter { +class ColorMatrixFilter : public LayerFilter { public: - virtual ~LayerFilter() = default; - /** - * Returns the layer filter type of this layer filter. + * The 4x5 color transformation matrix stored as a 20-element array in row-major order. + * Elements 0-4: red channel transformation + * Elements 5-9: green channel transformation + * Elements 10-14: blue channel transformation + * Elements 15-19: alpha channel transformation + * The last element of each row is an additive offset (bias). + * The default value is the identity matrix. */ - virtual LayerFilterType type() const = 0; + std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; - protected: - LayerFilter() = default; + NodeType nodeType() const override { + return NodeType::ColorMatrixFilter; + } }; } // namespace pagx diff --git a/pagx/include/pagx/model/ColorSource.h b/pagx/include/pagx/nodes/ColorSource.h similarity index 89% rename from pagx/include/pagx/model/ColorSource.h rename to pagx/include/pagx/nodes/ColorSource.h index ac0bff06f7..5015b6abcd 100644 --- a/pagx/include/pagx/model/ColorSource.h +++ b/pagx/include/pagx/nodes/ColorSource.h @@ -18,6 +18,8 @@ #pragma once +#include "pagx/nodes/Node.h" + namespace pagx { /** @@ -32,19 +34,13 @@ enum class ColorSourceType { ImagePattern }; -/** - * Returns the string name of a color source type. - */ -const char* ColorSourceTypeName(ColorSourceType type); - /** * Base class for color sources (SolidColor, gradients, ImagePattern). * ColorSource can be used both inline in painters and as standalone resources in defs. + * Inherits from Node so it can be stored in resources and referenced by ID. */ -class ColorSource { +class ColorSource : public Node { public: - virtual ~ColorSource() = default; - /** * Returns the color source type of this color source. */ diff --git a/pagx/include/pagx/model/PathDataResource.h b/pagx/include/pagx/nodes/ColorSpace.h similarity index 70% rename from pagx/include/pagx/model/PathDataResource.h rename to pagx/include/pagx/nodes/ColorSpace.h index 97c08e8471..d986c90626 100644 --- a/pagx/include/pagx/model/PathDataResource.h +++ b/pagx/include/pagx/nodes/ColorSpace.h @@ -18,25 +18,23 @@ #pragma once -#include -#include "pagx/model/Node.h" - namespace pagx { /** - * PathDataResource is a reusable path data resource that can be referenced by Path elements. It - * stores path data as an SVG path string for efficient serialization. + * Color space enumeration for color values. */ -class PathDataResource : public Node { - public: +enum class ColorSpace { /** - * The SVG path data string (d attribute format). + * Standard RGB color space (sRGB). The most common color space for web and displays. + * Component values are typically in [0, 1] range. */ - std::string data = {}; + SRGB, - NodeType nodeType() const override { - return NodeType::PathData; - } + /** + * Display P3 color space, a wide gamut color space used by Apple devices. + * Component values may exceed [0, 1] range to represent colors outside sRGB gamut. + */ + DisplayP3 }; } // namespace pagx diff --git a/pagx/include/pagx/model/ColorStop.h b/pagx/include/pagx/nodes/ColorStop.h similarity index 79% rename from pagx/include/pagx/model/ColorStop.h rename to pagx/include/pagx/nodes/ColorStop.h index 8e0fb8398e..e7f9051a34 100644 --- a/pagx/include/pagx/model/ColorStop.h +++ b/pagx/include/pagx/nodes/ColorStop.h @@ -18,16 +18,22 @@ #pragma once -#include "pagx/model/types/Color.h" +#include "pagx/nodes/Color.h" namespace pagx { /** - * A color stop in a gradient. + * A color stop defines a color at a specific position in a gradient. */ -class ColorStop { - public: +struct ColorStop { + /** + * The position of this color stop along the gradient, ranging from 0 to 1. + */ float offset = 0; + + /** + * The color value at this stop position. + */ Color color = {}; }; diff --git a/pagx/include/pagx/model/Composition.h b/pagx/include/pagx/nodes/Composition.h similarity index 98% rename from pagx/include/pagx/model/Composition.h rename to pagx/include/pagx/nodes/Composition.h index 24a54f2840..0590eafad3 100644 --- a/pagx/include/pagx/model/Composition.h +++ b/pagx/include/pagx/nodes/Composition.h @@ -21,7 +21,7 @@ #include #include #include -#include "pagx/model/Node.h" +#include "pagx/nodes/Node.h" namespace pagx { diff --git a/pagx/include/pagx/model/ConicGradient.h b/pagx/include/pagx/nodes/ConicGradient.h similarity index 64% rename from pagx/include/pagx/model/ConicGradient.h rename to pagx/include/pagx/nodes/ConicGradient.h index 3353317c62..68ad825082 100644 --- a/pagx/include/pagx/model/ConicGradient.h +++ b/pagx/include/pagx/nodes/ConicGradient.h @@ -18,30 +18,51 @@ #pragma once -#include #include -#include "pagx/model/ColorSource.h" -#include "pagx/model/ColorStop.h" -#include "pagx/model/types/Matrix.h" -#include "pagx/model/types/Point.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/ColorStop.h" +#include "pagx/nodes/Matrix.h" +#include "pagx/nodes/Point.h" namespace pagx { /** - * A conic (sweep) gradient. + * A conic (sweep) gradient color source that produces a gradient sweeping around a center point. */ class ConicGradient : public ColorSource { public: - std::string id = {}; + /** + * The center point of the gradient. + */ Point center = {}; + + /** + * The starting angle of the gradient sweep in degrees. The default value is 0. + */ float startAngle = 0; + + /** + * The ending angle of the gradient sweep in degrees. The default value is 360. + */ float endAngle = 360; + + /** + * The transformation matrix applied to the gradient. + */ Matrix matrix = {}; + + /** + * The color stops defining the gradient colors and positions. + */ std::vector colorStops = {}; ColorSourceType type() const override { return ColorSourceType::ConicGradient; } + + NodeType nodeType() const override { + return NodeType::ConicGradient; + } }; } // namespace pagx diff --git a/pagx/include/pagx/model/DiamondGradient.h b/pagx/include/pagx/nodes/DiamondGradient.h similarity index 69% rename from pagx/include/pagx/model/DiamondGradient.h rename to pagx/include/pagx/nodes/DiamondGradient.h index 3eefe08300..05eb353c84 100644 --- a/pagx/include/pagx/model/DiamondGradient.h +++ b/pagx/include/pagx/nodes/DiamondGradient.h @@ -18,29 +18,46 @@ #pragma once -#include #include -#include "pagx/model/ColorSource.h" -#include "pagx/model/ColorStop.h" -#include "pagx/model/types/Matrix.h" -#include "pagx/model/types/Point.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/ColorStop.h" +#include "pagx/nodes/Matrix.h" +#include "pagx/nodes/Point.h" namespace pagx { /** - * A diamond gradient. + * A diamond gradient color source that produces a gradient in a diamond shape from the center. */ class DiamondGradient : public ColorSource { public: - std::string id = {}; + /** + * The center point of the gradient. + */ Point center = {}; + + /** + * Half the diagonal length of the diamond shape. + */ float halfDiagonal = 0; + + /** + * The transformation matrix applied to the gradient. + */ Matrix matrix = {}; + + /** + * The color stops defining the gradient colors and positions. + */ std::vector colorStops = {}; ColorSourceType type() const override { return ColorSourceType::DiamondGradient; } + + NodeType nodeType() const override { + return NodeType::DiamondGradient; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/DropShadowFilter.h b/pagx/include/pagx/nodes/DropShadowFilter.h new file mode 100644 index 0000000000..db4e07ff2f --- /dev/null +++ b/pagx/include/pagx/nodes/DropShadowFilter.h @@ -0,0 +1,66 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/LayerFilter.h" +#include "pagx/nodes/Color.h" + +namespace pagx { + +/** + * A drop shadow filter that renders a shadow behind the layer content. + */ +class DropShadowFilter : public LayerFilter { + public: + /** + * The horizontal offset of the shadow in pixels. The default value is 0. + */ + float offsetX = 0; + + /** + * The vertical offset of the shadow in pixels. The default value is 0. + */ + float offsetY = 0; + + /** + * The horizontal blur radius of the shadow in pixels. The default value is 0. + */ + float blurrinessX = 0; + + /** + * The vertical blur radius of the shadow in pixels. The default value is 0. + */ + float blurrinessY = 0; + + /** + * The color of the shadow. + */ + Color color = {}; + + /** + * Whether to render only the shadow without the original content. The default value is false. + */ + bool shadowOnly = false; + + NodeType nodeType() const override { + return NodeType::DropShadowFilter; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/DropShadowStyle.h b/pagx/include/pagx/nodes/DropShadowStyle.h new file mode 100644 index 0000000000..e884a50f3a --- /dev/null +++ b/pagx/include/pagx/nodes/DropShadowStyle.h @@ -0,0 +1,72 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/LayerStyle.h" +#include "pagx/nodes/BlendMode.h" +#include "pagx/nodes/Color.h" + +namespace pagx { + +/** + * A drop shadow layer style that renders a shadow behind the layer content. + */ +class DropShadowStyle : public LayerStyle { + public: + /** + * The horizontal offset of the shadow in pixels. The default value is 0. + */ + float offsetX = 0; + + /** + * The vertical offset of the shadow in pixels. The default value is 0. + */ + float offsetY = 0; + + /** + * The horizontal blur radius of the shadow in pixels. The default value is 0. + */ + float blurrinessX = 0; + + /** + * The vertical blur radius of the shadow in pixels. The default value is 0. + */ + float blurrinessY = 0; + + /** + * The color of the shadow. + */ + Color color = {}; + + /** + * Whether the shadow is shown behind the layer. The default value is true. + */ + bool showBehindLayer = true; + + /** + * The blend mode used when compositing the shadow. The default value is Normal. + */ + BlendMode blendMode = BlendMode::Normal; + + NodeType nodeType() const override { + return NodeType::DropShadowStyle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/nodes/Element.h b/pagx/include/pagx/nodes/Element.h new file mode 100644 index 0000000000..3f74589f0c --- /dev/null +++ b/pagx/include/pagx/nodes/Element.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/Node.h" + +namespace pagx { + +/** + * Element is the base class for all elements in a shape layer. It includes shapes (Rectangle, + * Ellipse, Polystar, Path, TextSpan), painters (Fill, Stroke), modifiers (TrimPath, RoundCorner, + * MergePath), text elements (TextModifier, TextPath, TextLayout), and containers (Group, Repeater). + */ +class Element : public Node { + public: + ~Element() override = default; + + protected: + Element() = default; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Ellipse.h b/pagx/include/pagx/nodes/Ellipse.h similarity index 88% rename from pagx/include/pagx/model/Ellipse.h rename to pagx/include/pagx/nodes/Ellipse.h index d9b8fc31d4..7869f05227 100644 --- a/pagx/include/pagx/model/Ellipse.h +++ b/pagx/include/pagx/nodes/Ellipse.h @@ -18,9 +18,9 @@ #pragma once -#include "pagx/model/Element.h" -#include "pagx/model/types/Point.h" -#include "pagx/model/types/Size.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/Point.h" +#include "pagx/nodes/Size.h" namespace pagx { @@ -44,8 +44,8 @@ class Ellipse : public Element { */ bool reversed = false; - ElementType type() const override { - return ElementType::Ellipse; + NodeType nodeType() const override { + return NodeType::Ellipse; } }; diff --git a/pagx/include/pagx/model/Fill.h b/pagx/include/pagx/nodes/Fill.h similarity index 94% rename from pagx/include/pagx/model/Fill.h rename to pagx/include/pagx/nodes/Fill.h index 7265d8b4c7..5db840c7ba 100644 --- a/pagx/include/pagx/model/Fill.h +++ b/pagx/include/pagx/nodes/Fill.h @@ -20,10 +20,10 @@ #include #include -#include "pagx/model/ColorSource.h" -#include "pagx/model/Element.h" -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/LayerPlacement.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/BlendMode.h" +#include "pagx/nodes/LayerPlacement.h" namespace pagx { diff --git a/pagx/include/pagx/model/ColorMatrixFilter.h b/pagx/include/pagx/nodes/FilterMode.h similarity index 69% rename from pagx/include/pagx/model/ColorMatrixFilter.h rename to pagx/include/pagx/nodes/FilterMode.h index 98ae503a74..77511149e1 100644 --- a/pagx/include/pagx/model/ColorMatrixFilter.h +++ b/pagx/include/pagx/nodes/FilterMode.h @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// Copyright (C) 2026 Tencent. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -18,21 +18,20 @@ #pragma once -#include -#include "pagx/model/LayerFilter.h" - namespace pagx { /** - * Color matrix filter. + * FilterMode defines how texture sampling is performed when a texture is minified or magnified. */ -class ColorMatrixFilter : public LayerFilter { - public: - std::array matrix = {1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}; - - LayerFilterType type() const override { - return LayerFilterType::ColorMatrixFilter; - } +enum class FilterMode { + /** + * Single sample point (the nearest neighbor). + */ + Nearest, + /** + * Interpolate between 2x2 sample points (bi-linear interpolation). + */ + Linear }; } // namespace pagx diff --git a/pagx/include/pagx/model/Group.h b/pagx/include/pagx/nodes/Group.h similarity index 93% rename from pagx/include/pagx/model/Group.h rename to pagx/include/pagx/nodes/Group.h index 93fa6c7d46..9ddf078b10 100644 --- a/pagx/include/pagx/model/Group.h +++ b/pagx/include/pagx/nodes/Group.h @@ -20,8 +20,8 @@ #include #include -#include "pagx/model/Element.h" -#include "pagx/model/types/Point.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/Point.h" namespace pagx { @@ -72,8 +72,8 @@ class Group : public Element { */ std::vector> elements = {}; - ElementType type() const override { - return ElementType::Group; + NodeType nodeType() const override { + return NodeType::Group; } }; diff --git a/pagx/include/pagx/model/Image.h b/pagx/include/pagx/nodes/Image.h similarity index 97% rename from pagx/include/pagx/model/Image.h rename to pagx/include/pagx/nodes/Image.h index e19c192509..ecc79b1e0b 100644 --- a/pagx/include/pagx/model/Image.h +++ b/pagx/include/pagx/nodes/Image.h @@ -19,7 +19,7 @@ #pragma once #include -#include "pagx/model/Node.h" +#include "pagx/nodes/Node.h" namespace pagx { diff --git a/pagx/include/pagx/nodes/ImagePattern.h b/pagx/include/pagx/nodes/ImagePattern.h new file mode 100644 index 0000000000..5b5a951390 --- /dev/null +++ b/pagx/include/pagx/nodes/ImagePattern.h @@ -0,0 +1,79 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/FilterMode.h" +#include "pagx/nodes/Matrix.h" +#include "pagx/nodes/MipmapMode.h" +#include "pagx/nodes/TileMode.h" + +namespace pagx { + +/** + * An image pattern color source that tiles an image to fill shapes. + */ +class ImagePattern : public ColorSource { + public: + /** + * A reference to an image resource (e.g., "@imageId"). + */ + std::string image = {}; + + /** + * The tile mode for the horizontal direction. The default value is Clamp. + */ + TileMode tileModeX = TileMode::Clamp; + + /** + * The tile mode for the vertical direction. The default value is Clamp. + */ + TileMode tileModeY = TileMode::Clamp; + + /** + * The filter mode for minification (when the image is scaled down). The default value is Linear. + */ + FilterMode minFilterMode = FilterMode::Linear; + + /** + * The filter mode for magnification (when the image is scaled up). The default value is Linear. + */ + FilterMode magFilterMode = FilterMode::Linear; + + /** + * The mipmap mode for texture sampling. The default value is Linear. + */ + MipmapMode mipmapMode = MipmapMode::Linear; + + /** + * The transformation matrix applied to the pattern. + */ + Matrix matrix = {}; + + ColorSourceType type() const override { + return ColorSourceType::ImagePattern; + } + + NodeType nodeType() const override { + return NodeType::ImagePattern; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/InnerShadowFilter.h b/pagx/include/pagx/nodes/InnerShadowFilter.h similarity index 62% rename from pagx/include/pagx/model/InnerShadowFilter.h rename to pagx/include/pagx/nodes/InnerShadowFilter.h index d119a01a23..c857af684a 100644 --- a/pagx/include/pagx/model/InnerShadowFilter.h +++ b/pagx/include/pagx/nodes/InnerShadowFilter.h @@ -18,25 +18,48 @@ #pragma once -#include "pagx/model/LayerFilter.h" -#include "pagx/model/types/Color.h" +#include "pagx/nodes/LayerFilter.h" +#include "pagx/nodes/Color.h" namespace pagx { /** - * Inner shadow filter. + * An inner shadow filter that renders a shadow inside the layer content. */ class InnerShadowFilter : public LayerFilter { public: + /** + * The horizontal offset of the shadow in pixels. The default value is 0. + */ float offsetX = 0; + + /** + * The vertical offset of the shadow in pixels. The default value is 0. + */ float offsetY = 0; + + /** + * The horizontal blur radius of the shadow in pixels. The default value is 0. + */ float blurrinessX = 0; + + /** + * The vertical blur radius of the shadow in pixels. The default value is 0. + */ float blurrinessY = 0; + + /** + * The color of the shadow. + */ Color color = {}; + + /** + * Whether to render only the shadow without the original content. The default value is false. + */ bool shadowOnly = false; - LayerFilterType type() const override { - return LayerFilterType::InnerShadowFilter; + NodeType nodeType() const override { + return NodeType::InnerShadowFilter; } }; diff --git a/pagx/include/pagx/nodes/InnerShadowStyle.h b/pagx/include/pagx/nodes/InnerShadowStyle.h new file mode 100644 index 0000000000..d99b02676a --- /dev/null +++ b/pagx/include/pagx/nodes/InnerShadowStyle.h @@ -0,0 +1,67 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/LayerStyle.h" +#include "pagx/nodes/BlendMode.h" +#include "pagx/nodes/Color.h" + +namespace pagx { + +/** + * An inner shadow layer style that renders a shadow inside the layer content. + */ +class InnerShadowStyle : public LayerStyle { + public: + /** + * The horizontal offset of the shadow in pixels. The default value is 0. + */ + float offsetX = 0; + + /** + * The vertical offset of the shadow in pixels. The default value is 0. + */ + float offsetY = 0; + + /** + * The horizontal blur radius of the shadow in pixels. The default value is 0. + */ + float blurrinessX = 0; + + /** + * The vertical blur radius of the shadow in pixels. The default value is 0. + */ + float blurrinessY = 0; + + /** + * The color of the shadow. + */ + Color color = {}; + + /** + * The blend mode used when compositing the shadow. The default value is Normal. + */ + BlendMode blendMode = BlendMode::Normal; + + NodeType nodeType() const override { + return NodeType::InnerShadowStyle; + } +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Layer.h b/pagx/include/pagx/nodes/Layer.h similarity index 87% rename from pagx/include/pagx/model/Layer.h rename to pagx/include/pagx/nodes/Layer.h index e0e17de53a..faea8a3e11 100644 --- a/pagx/include/pagx/model/Layer.h +++ b/pagx/include/pagx/nodes/Layer.h @@ -22,12 +22,13 @@ #include #include #include -#include "pagx/model/Element.h" -#include "pagx/model/LayerFilter.h" -#include "pagx/model/LayerStyle.h" -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Matrix.h" -#include "pagx/model/types/Rect.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/LayerFilter.h" +#include "pagx/nodes/LayerStyle.h" +#include "pagx/nodes/Node.h" +#include "pagx/nodes/BlendMode.h" +#include "pagx/nodes/Matrix.h" +#include "pagx/nodes/Rect.h" namespace pagx { @@ -40,20 +41,12 @@ enum class MaskType { Contour }; -std::string MaskTypeToString(MaskType type); -MaskType MaskTypeFromString(const std::string& str); - /** * Layer represents a layer node that can contain vector elements, layer styles, filters, and child * layers. It is the main building block for composing visual content in a PAGX document. */ -class Layer { +class Layer : public Node { public: - /** - * The unique identifier of the layer. - */ - std::string id = {}; - /** * The display name of the layer. */ @@ -165,10 +158,9 @@ class Layer { */ std::vector> children = {}; - /** - * Custom data from SVG data-* attributes. The keys are stored without the "data-" prefix. - */ - std::unordered_map customData = {}; + NodeType nodeType() const override { + return NodeType::Layer; + } }; } // namespace pagx diff --git a/pagx/include/pagx/model/BlurFilter.h b/pagx/include/pagx/nodes/LayerFilter.h similarity index 76% rename from pagx/include/pagx/model/BlurFilter.h rename to pagx/include/pagx/nodes/LayerFilter.h index 9940c58ee8..7a06cc9b8a 100644 --- a/pagx/include/pagx/model/BlurFilter.h +++ b/pagx/include/pagx/nodes/LayerFilter.h @@ -18,23 +18,19 @@ #pragma once -#include "pagx/model/LayerFilter.h" -#include "pagx/model/types/TileMode.h" +#include "pagx/nodes/Node.h" namespace pagx { /** - * Blur filter. + * Base class for layer filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, ColorMatrixFilter). */ -class BlurFilter : public LayerFilter { +class LayerFilter : public Node { public: - float blurrinessX = 0; - float blurrinessY = 0; - TileMode tileMode = TileMode::Decal; + ~LayerFilter() override = default; - LayerFilterType type() const override { - return LayerFilterType::BlurFilter; - } + protected: + LayerFilter() = default; }; } // namespace pagx diff --git a/pagx/include/pagx/model/types/LayerPlacement.h b/pagx/include/pagx/nodes/LayerPlacement.h similarity index 100% rename from pagx/include/pagx/model/types/LayerPlacement.h rename to pagx/include/pagx/nodes/LayerPlacement.h diff --git a/pagx/src/PAGXXMLWriter.h b/pagx/include/pagx/nodes/LayerStyle.h similarity index 81% rename from pagx/src/PAGXXMLWriter.h rename to pagx/include/pagx/nodes/LayerStyle.h index 9248705ce6..ce7c6ecf42 100644 --- a/pagx/src/PAGXXMLWriter.h +++ b/pagx/include/pagx/nodes/LayerStyle.h @@ -18,20 +18,19 @@ #pragma once -#include -#include "pagx/model/Document.h" +#include "pagx/nodes/Node.h" namespace pagx { /** - * Writer for PAGX XML format. + * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle). */ -class PAGXXMLWriter { +class LayerStyle : public Node { public: - /** - * Writes a PAGXDocument to XML string. - */ - static std::string Write(const Document& doc); + ~LayerStyle() override = default; + + protected: + LayerStyle() = default; }; } // namespace pagx diff --git a/pagx/include/pagx/model/LinearGradient.h b/pagx/include/pagx/nodes/LinearGradient.h similarity index 69% rename from pagx/include/pagx/model/LinearGradient.h rename to pagx/include/pagx/nodes/LinearGradient.h index 8d3915c012..c2b60239fb 100644 --- a/pagx/include/pagx/model/LinearGradient.h +++ b/pagx/include/pagx/nodes/LinearGradient.h @@ -18,29 +18,46 @@ #pragma once -#include #include -#include "pagx/model/ColorSource.h" -#include "pagx/model/ColorStop.h" -#include "pagx/model/types/Matrix.h" -#include "pagx/model/types/Point.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/ColorStop.h" +#include "pagx/nodes/Matrix.h" +#include "pagx/nodes/Point.h" namespace pagx { /** - * A linear gradient. + * A linear gradient color source that produces a gradient along a line between two points. */ class LinearGradient : public ColorSource { public: - std::string id = {}; + /** + * The starting point of the gradient line. + */ Point startPoint = {}; + + /** + * The ending point of the gradient line. + */ Point endPoint = {}; + + /** + * The transformation matrix applied to the gradient. + */ Matrix matrix = {}; + + /** + * The color stops defining the gradient colors and positions. + */ std::vector colorStops = {}; ColorSourceType type() const override { return ColorSourceType::LinearGradient; } + + NodeType nodeType() const override { + return NodeType::LinearGradient; + } }; } // namespace pagx diff --git a/pagx/include/pagx/model/types/Matrix.h b/pagx/include/pagx/nodes/Matrix.h similarity index 89% rename from pagx/include/pagx/model/types/Matrix.h rename to pagx/include/pagx/nodes/Matrix.h index 0f3fd49c39..65f9882ccf 100644 --- a/pagx/include/pagx/model/types/Matrix.h +++ b/pagx/include/pagx/nodes/Matrix.h @@ -22,7 +22,7 @@ #include #include #include -#include "pagx/model/types/Point.h" +#include "pagx/nodes/Point.h" namespace pagx { @@ -34,12 +34,35 @@ namespace pagx { * | 0 0 1 | */ struct Matrix { - float a = 1; // scaleX - float b = 0; // skewY - float c = 0; // skewX - float d = 1; // scaleY - float tx = 0; // transX - float ty = 0; // transY + /** + * The horizontal scale factor (scaleX). + */ + float a = 1; + + /** + * The vertical skew factor (skewY). + */ + float b = 0; + + /** + * The horizontal skew factor (skewX). + */ + float c = 0; + + /** + * The vertical scale factor (scaleY). + */ + float d = 1; + + /** + * The horizontal translation (transX). + */ + float tx = 0; + + /** + * The vertical translation (transY). + */ + float ty = 0; /** * Returns the identity matrix. diff --git a/pagx/include/pagx/model/MergePath.h b/pagx/include/pagx/nodes/MergePath.h similarity index 86% rename from pagx/include/pagx/model/MergePath.h rename to pagx/include/pagx/nodes/MergePath.h index d048a5120e..2c3b6b8e90 100644 --- a/pagx/include/pagx/model/MergePath.h +++ b/pagx/include/pagx/nodes/MergePath.h @@ -18,8 +18,7 @@ #pragma once -#include -#include "pagx/model/Element.h" +#include "pagx/nodes/Element.h" namespace pagx { @@ -34,9 +33,6 @@ enum class MergePathMode { Difference }; -std::string MergePathModeToString(MergePathMode mode); -MergePathMode MergePathModeFromString(const std::string& str); - /** * MergePath is a path modifier that merges multiple paths using boolean operations. It can append, * add, subtract, intersect, or exclude paths from each other. @@ -48,8 +44,8 @@ class MergePath : public Element { */ MergePathMode mode = MergePathMode::Append; - ElementType type() const override { - return ElementType::MergePath; + NodeType nodeType() const override { + return NodeType::MergePath; } }; diff --git a/pagx/include/pagx/nodes/MipmapMode.h b/pagx/include/pagx/nodes/MipmapMode.h new file mode 100644 index 0000000000..cb965d2c8f --- /dev/null +++ b/pagx/include/pagx/nodes/MipmapMode.h @@ -0,0 +1,41 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * MipmapMode defines how mipmap levels are selected during texture sampling. + */ +enum class MipmapMode { + /** + * Ignore mipmap levels, sample from the "base". + */ + None, + /** + * Sample from the nearest level. + */ + Nearest, + /** + * Interpolate between the two nearest levels. + */ + Linear +}; + +} // namespace pagx diff --git a/pagx/include/pagx/model/Element.h b/pagx/include/pagx/nodes/Node.h similarity index 50% rename from pagx/include/pagx/model/Element.h rename to pagx/include/pagx/nodes/Node.h index 6f27ad15c6..df7f8a4080 100644 --- a/pagx/include/pagx/model/Element.h +++ b/pagx/include/pagx/nodes/Node.h @@ -18,13 +18,98 @@ #pragma once +#include +#include + namespace pagx { /** - * ElementType enumerates all types of elements that can be placed in Layer.contents or - * Group.elements. + * NodeType enumerates all types of nodes that can be stored in a PAGX document. + * This includes resources (Image, Composition, ColorSources) and elements (shapes, painters, + * modifiers, etc.). */ -enum class ElementType { +enum class NodeType { + // Resources + /** + * A reusable path data resource. + */ + PathData, + /** + * An image resource. + */ + Image, + /** + * A composition resource containing layers. + */ + Composition, + /** + * A solid color source. + */ + SolidColor, + /** + * A linear gradient color source. + */ + LinearGradient, + /** + * A radial gradient color source. + */ + RadialGradient, + /** + * A conic (sweep) gradient color source. + */ + ConicGradient, + /** + * A diamond gradient color source. + */ + DiamondGradient, + /** + * An image pattern color source. + */ + ImagePattern, + + // Layer + /** + * A layer node that contains vector elements and child layers. + */ + Layer, + + // Layer Styles + /** + * A drop shadow layer style. + */ + DropShadowStyle, + /** + * An inner shadow layer style. + */ + InnerShadowStyle, + /** + * A background blur layer style. + */ + BackgroundBlurStyle, + + // Layer Filters + /** + * A blur filter. + */ + BlurFilter, + /** + * A drop shadow filter. + */ + DropShadowFilter, + /** + * An inner shadow filter. + */ + InnerShadowFilter, + /** + * A blend filter. + */ + BlendFilter, + /** + * A color matrix filter. + */ + ColorMatrixFilter, + + // Elements (shapes, painters, modifiers, containers) /** * A rectangle shape with optional rounded corners. */ @@ -84,30 +169,41 @@ enum class ElementType { /** * A modifier that creates multiple copies of preceding elements. */ - Repeater -}; + Repeater, -/** - * Returns the string name of an element type. - */ -const char* ElementTypeName(ElementType type); + // Text Selectors + /** + * A range selector for text modifiers. + */ + RangeSelector +}; /** - * Element is the base class for all elements in a shape layer. It includes shapes (Rectangle, - * Ellipse, Polystar, Path, TextSpan), painters (Fill, Stroke), modifiers (TrimPath, RoundCorner, - * MergePath), text elements (TextModifier, TextPath, TextLayout), and containers (Group, Repeater). + * Node is the base class for all shareable nodes in a PAGX document. Nodes can be stored in the + * document's resources list and referenced by ID (e.g., "@resourceId"). Each node has a unique + * identifier and a type. */ -class Element { +class Node { public: - virtual ~Element() = default; + /** + * The unique identifier of this node. Used for referencing the node by ID (e.g., "@id"). + */ + std::string id = {}; + + /** + * Custom data attributes. The keys are stored without the "data-" prefix. + */ + std::unordered_map customData = {}; + + virtual ~Node() = default; /** - * Returns the element type of this element. + * Returns the node type of this node. */ - virtual ElementType type() const = 0; + virtual NodeType nodeType() const = 0; protected: - Element() = default; + Node() = default; }; } // namespace pagx diff --git a/pagx/include/pagx/model/Path.h b/pagx/include/pagx/nodes/Path.h similarity index 81% rename from pagx/include/pagx/model/Path.h rename to pagx/include/pagx/nodes/Path.h index 7839fa6e4e..ef0bbf582f 100644 --- a/pagx/include/pagx/model/Path.h +++ b/pagx/include/pagx/nodes/Path.h @@ -18,8 +18,8 @@ #pragma once -#include "pagx/model/PathData.h" -#include "pagx/model/Element.h" +#include "pagx/nodes/PathData.h" +#include "pagx/nodes/Element.h" namespace pagx { @@ -34,13 +34,19 @@ class Path : public Element { */ PathData data = {}; + /** + * Reference to a PathData resource (e.g., "#path1"). If non-empty, this takes precedence + * over the inline `data` field when exporting. + */ + std::string dataRef = {}; + /** * Whether the path direction is reversed. The default value is false. */ bool reversed = false; - ElementType type() const override { - return ElementType::Path; + NodeType nodeType() const override { + return NodeType::Path; } }; diff --git a/pagx/include/pagx/model/PathData.h b/pagx/include/pagx/nodes/PathData.h similarity index 92% rename from pagx/include/pagx/model/PathData.h rename to pagx/include/pagx/nodes/PathData.h index 17079d22b5..3692ee2e29 100644 --- a/pagx/include/pagx/model/PathData.h +++ b/pagx/include/pagx/nodes/PathData.h @@ -20,8 +20,9 @@ #include #include -#include "pagx/model/types/Matrix.h" -#include "pagx/model/types/Rect.h" +#include "pagx/nodes/Node.h" +#include "pagx/nodes/Matrix.h" +#include "pagx/nodes/Rect.h" namespace pagx { @@ -39,8 +40,9 @@ enum class PathVerb : uint8_t { /** * PathData stores path commands in a format optimized for fast iteration * and serialization. Unlike tgfx::Path, it exposes raw data arrays directly. + * PathData can be stored in document resources and referenced by ID. */ -class PathData { +class PathData : public Node { public: PathData() = default; @@ -143,7 +145,7 @@ class PathData { /** * Returns the bounding rectangle of the path. */ - Rect getBounds() const; + Rect getBounds(); /** * Returns true if the path contains no commands. @@ -167,11 +169,15 @@ class PathData { */ static int PointsPerVerb(PathVerb verb); + NodeType nodeType() const override { + return NodeType::PathData; + } + private: std::vector _verbs = {}; std::vector _points = {}; - mutable Rect _cachedBounds = {}; - mutable bool _boundsDirty = true; + Rect _cachedBounds = {}; + bool _boundsDirty = true; }; } // namespace pagx diff --git a/pagx/include/pagx/model/types/Point.h b/pagx/include/pagx/nodes/Point.h similarity index 94% rename from pagx/include/pagx/model/types/Point.h rename to pagx/include/pagx/nodes/Point.h index e4996dcf21..bc3135b580 100644 --- a/pagx/include/pagx/model/types/Point.h +++ b/pagx/include/pagx/nodes/Point.h @@ -24,7 +24,14 @@ namespace pagx { * A point with x and y coordinates. */ struct Point { + /** + * The x coordinate. + */ float x = 0; + + /** + * The y coordinate. + */ float y = 0; bool operator==(const Point& other) const { diff --git a/pagx/include/pagx/model/Polystar.h b/pagx/include/pagx/nodes/Polystar.h similarity index 86% rename from pagx/include/pagx/model/Polystar.h rename to pagx/include/pagx/nodes/Polystar.h index c0e47e64d7..ad0c1cdf84 100644 --- a/pagx/include/pagx/model/Polystar.h +++ b/pagx/include/pagx/nodes/Polystar.h @@ -18,8 +18,8 @@ #pragma once -#include "pagx/model/Element.h" -#include "pagx/model/types/Point.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/Point.h" namespace pagx { @@ -44,7 +44,7 @@ class Polystar : public Element { /** * The type of polystar shape, either Star or Polygon. The default value is Star. */ - PolystarType polystarType = PolystarType::Star; + PolystarType type = PolystarType::Star; /** * The number of points in the polystar. The default value is 5. @@ -57,7 +57,7 @@ class Polystar : public Element { float outerRadius = 100; /** - * The inner radius of the polystar. Only applies when polystarType is Star. The default value + * The inner radius of the polystar. Only applies when type is Star. The default value * is 50. */ float innerRadius = 50; @@ -73,7 +73,7 @@ class Polystar : public Element { float outerRoundness = 0; /** - * The roundness of the inner points, ranging from 0 to 100. Only applies when polystarType is + * The roundness of the inner points, ranging from 0 to 100. Only applies when type is * Star. The default value is 0. */ float innerRoundness = 0; @@ -83,8 +83,8 @@ class Polystar : public Element { */ bool reversed = false; - ElementType type() const override { - return ElementType::Polystar; + NodeType nodeType() const override { + return NodeType::Polystar; } }; diff --git a/pagx/include/pagx/model/RadialGradient.h b/pagx/include/pagx/nodes/RadialGradient.h similarity index 69% rename from pagx/include/pagx/model/RadialGradient.h rename to pagx/include/pagx/nodes/RadialGradient.h index 4a43976111..dc0fe15440 100644 --- a/pagx/include/pagx/model/RadialGradient.h +++ b/pagx/include/pagx/nodes/RadialGradient.h @@ -18,29 +18,46 @@ #pragma once -#include #include -#include "pagx/model/ColorSource.h" -#include "pagx/model/ColorStop.h" -#include "pagx/model/types/Matrix.h" -#include "pagx/model/types/Point.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/ColorStop.h" +#include "pagx/nodes/Matrix.h" +#include "pagx/nodes/Point.h" namespace pagx { /** - * A radial gradient. + * A radial gradient color source that produces a gradient radiating from a center point. */ class RadialGradient : public ColorSource { public: - std::string id = {}; + /** + * The center point of the gradient. + */ Point center = {}; + + /** + * The radius of the gradient circle. + */ float radius = 0; + + /** + * The transformation matrix applied to the gradient. + */ Matrix matrix = {}; + + /** + * The color stops defining the gradient colors and positions. + */ std::vector colorStops = {}; ColorSourceType type() const override { return ColorSourceType::RadialGradient; } + + NodeType nodeType() const override { + return NodeType::RadialGradient; + } }; } // namespace pagx diff --git a/pagx/include/pagx/model/RangeSelector.h b/pagx/include/pagx/nodes/RangeSelector.h similarity index 54% rename from pagx/include/pagx/model/RangeSelector.h rename to pagx/include/pagx/nodes/RangeSelector.h index 0e8184c4b8..b25cdcb6b3 100644 --- a/pagx/include/pagx/model/RangeSelector.h +++ b/pagx/include/pagx/nodes/RangeSelector.h @@ -18,8 +18,7 @@ #pragma once -#include -#include "pagx/model/TextSelector.h" +#include "pagx/nodes/TextSelector.h" namespace pagx { @@ -31,9 +30,6 @@ enum class SelectorUnit { Percentage }; -std::string SelectorUnitToString(SelectorUnit unit); -SelectorUnit SelectorUnitFromString(const std::string& str); - /** * Range selector shape. */ @@ -46,9 +42,6 @@ enum class SelectorShape { Smooth }; -std::string SelectorShapeToString(SelectorShape shape); -SelectorShape SelectorShapeFromString(const std::string& str); - /** * Range selector combination mode. */ @@ -61,28 +54,72 @@ enum class SelectorMode { Difference }; -std::string SelectorModeToString(SelectorMode mode); -SelectorMode SelectorModeFromString(const std::string& str); - /** - * Range selector for text modifier. + * A range selector that defines which characters in a text are affected by a text modifier. + * It provides flexible control over character selection through start/end positions, shapes, + * and randomization. */ class RangeSelector : public TextSelector { public: + /** + * The starting position of the selection range, in units defined by the unit property. The + * default value is 0. + */ float start = 0; + + /** + * The ending position of the selection range, in units defined by the unit property. The default + * value is 1. + */ float end = 1; + + /** + * The offset to shift the selection range. The default value is 0. + */ float offset = 0; + + /** + * The unit used for start, end, and offset values. The default value is Percentage. + */ SelectorUnit unit = SelectorUnit::Percentage; + + /** + * The shape of the selection falloff curve. The default value is Square. + */ SelectorShape shape = SelectorShape::Square; + + /** + * The ease-in amount for the selection shape, ranging from 0 to 1. The default value is 0. + */ float easeIn = 0; + + /** + * The ease-out amount for the selection shape, ranging from 0 to 1. The default value is 0. + */ float easeOut = 0; + + /** + * The mode for combining multiple selectors. The default value is Add. + */ SelectorMode mode = SelectorMode::Add; + + /** + * The weight of this selector's influence, ranging from 0 to 1. The default value is 1. + */ float weight = 1; + + /** + * Whether to randomize the order of character selection. The default value is false. + */ bool randomizeOrder = false; + + /** + * The seed for random order generation. The default value is 0. + */ int randomSeed = 0; - TextSelectorType type() const override { - return TextSelectorType::RangeSelector; + NodeType nodeType() const override { + return NodeType::RangeSelector; } }; diff --git a/pagx/include/pagx/model/types/Rect.h b/pagx/include/pagx/nodes/Rect.h similarity index 90% rename from pagx/include/pagx/model/types/Rect.h rename to pagx/include/pagx/nodes/Rect.h index 5adea6206d..4e2a1f435c 100644 --- a/pagx/include/pagx/model/types/Rect.h +++ b/pagx/include/pagx/nodes/Rect.h @@ -24,9 +24,24 @@ namespace pagx { * A rectangle defined by position and size. */ struct Rect { + /** + * The x coordinate of the rectangle origin. + */ float x = 0; + + /** + * The y coordinate of the rectangle origin. + */ float y = 0; + + /** + * The width of the rectangle. + */ float width = 0; + + /** + * The height of the rectangle. + */ float height = 0; /** diff --git a/pagx/include/pagx/model/Rectangle.h b/pagx/include/pagx/nodes/Rectangle.h similarity index 89% rename from pagx/include/pagx/model/Rectangle.h rename to pagx/include/pagx/nodes/Rectangle.h index d70ecb5fb4..7de34c417b 100644 --- a/pagx/include/pagx/model/Rectangle.h +++ b/pagx/include/pagx/nodes/Rectangle.h @@ -18,9 +18,9 @@ #pragma once -#include "pagx/model/Element.h" -#include "pagx/model/types/Point.h" -#include "pagx/model/types/Size.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/Point.h" +#include "pagx/nodes/Size.h" namespace pagx { @@ -49,8 +49,8 @@ class Rectangle : public Element { */ bool reversed = false; - ElementType type() const override { - return ElementType::Rectangle; + NodeType nodeType() const override { + return NodeType::Rectangle; } }; diff --git a/pagx/include/pagx/model/Repeater.h b/pagx/include/pagx/nodes/Repeater.h similarity index 89% rename from pagx/include/pagx/model/Repeater.h rename to pagx/include/pagx/nodes/Repeater.h index d174bab030..617d3f1f01 100644 --- a/pagx/include/pagx/model/Repeater.h +++ b/pagx/include/pagx/nodes/Repeater.h @@ -18,9 +18,8 @@ #pragma once -#include -#include "pagx/model/Element.h" -#include "pagx/model/types/Point.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/Point.h" namespace pagx { @@ -32,9 +31,6 @@ enum class RepeaterOrder { AboveOriginal }; -std::string RepeaterOrderToString(RepeaterOrder order); -RepeaterOrder RepeaterOrderFromString(const std::string& str); - /** * Repeater is a modifier that creates multiple copies of preceding elements with progressive * transformations. Each copy can have an incremental offset in position, rotation, scale, and @@ -88,8 +84,8 @@ class Repeater : public Element { */ float endAlpha = 1; - ElementType type() const override { - return ElementType::Repeater; + NodeType nodeType() const override { + return NodeType::Repeater; } }; diff --git a/pagx/include/pagx/model/RoundCorner.h b/pagx/include/pagx/nodes/RoundCorner.h similarity index 92% rename from pagx/include/pagx/model/RoundCorner.h rename to pagx/include/pagx/nodes/RoundCorner.h index 9aedc8ab8a..fc8a123fdf 100644 --- a/pagx/include/pagx/model/RoundCorner.h +++ b/pagx/include/pagx/nodes/RoundCorner.h @@ -18,7 +18,7 @@ #pragma once -#include "pagx/model/Element.h" +#include "pagx/nodes/Element.h" namespace pagx { @@ -33,8 +33,8 @@ class RoundCorner : public Element { */ float radius = 10; - ElementType type() const override { - return ElementType::RoundCorner; + NodeType nodeType() const override { + return NodeType::RoundCorner; } }; diff --git a/pagx/include/pagx/model/types/Size.h b/pagx/include/pagx/nodes/Size.h similarity index 94% rename from pagx/include/pagx/model/types/Size.h rename to pagx/include/pagx/nodes/Size.h index bb0ea3e110..c9d57d0e42 100644 --- a/pagx/include/pagx/model/types/Size.h +++ b/pagx/include/pagx/nodes/Size.h @@ -24,7 +24,14 @@ namespace pagx { * A size with width and height. */ struct Size { + /** + * The width dimension. + */ float width = 0; + + /** + * The height dimension. + */ float height = 0; bool operator==(const Size& other) const { diff --git a/pagx/include/pagx/model/SolidColor.h b/pagx/include/pagx/nodes/SolidColor.h similarity index 81% rename from pagx/include/pagx/model/SolidColor.h rename to pagx/include/pagx/nodes/SolidColor.h index fb7aba8a40..848bf0008c 100644 --- a/pagx/include/pagx/model/SolidColor.h +++ b/pagx/include/pagx/nodes/SolidColor.h @@ -18,23 +18,28 @@ #pragma once -#include -#include "pagx/model/ColorSource.h" -#include "pagx/model/types/Color.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/Color.h" namespace pagx { /** - * A solid color. + * A solid color source for fills and strokes. */ class SolidColor : public ColorSource { public: - std::string id = {}; + /** + * The color value with RGBA components and color space. + */ Color color = {}; ColorSourceType type() const override { return ColorSourceType::SolidColor; } + + NodeType nodeType() const override { + return NodeType::SolidColor; + } }; } // namespace pagx diff --git a/pagx/include/pagx/model/Stroke.h b/pagx/include/pagx/nodes/Stroke.h similarity index 95% rename from pagx/include/pagx/model/Stroke.h rename to pagx/include/pagx/nodes/Stroke.h index 0a4fb2b12f..afb557408a 100644 --- a/pagx/include/pagx/model/Stroke.h +++ b/pagx/include/pagx/nodes/Stroke.h @@ -21,10 +21,10 @@ #include #include #include -#include "pagx/model/ColorSource.h" -#include "pagx/model/Element.h" -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/LayerPlacement.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/BlendMode.h" +#include "pagx/nodes/LayerPlacement.h" namespace pagx { diff --git a/pagx/include/pagx/model/LayerStyle.h b/pagx/include/pagx/nodes/TextAlign.h similarity index 60% rename from pagx/include/pagx/model/LayerStyle.h rename to pagx/include/pagx/nodes/TextAlign.h index ccbf8e2cbf..2175af4edd 100644 --- a/pagx/include/pagx/model/LayerStyle.h +++ b/pagx/include/pagx/nodes/TextAlign.h @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// Copyright (C) 2026 Tencent. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -21,33 +21,29 @@ namespace pagx { /** - * Layer style types. + * Text horizontal alignment. */ -enum class LayerStyleType { - DropShadowStyle, - InnerShadowStyle, - BackgroundBlurStyle -}; - -/** - * Returns the string name of a layer style type. - */ -const char* LayerStyleTypeName(LayerStyleType type); - -/** - * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle). - */ -class LayerStyle { - public: - virtual ~LayerStyle() = default; - +enum class TextAlign { /** - * Returns the layer style type of this layer style. + * Align text to the start (left for LTR, right for RTL). + * For TextPath, align text to the path start. */ - virtual LayerStyleType type() const = 0; - - protected: - LayerStyle() = default; + Start, + /** + * Align text to the center. + * For TextPath, center text along the path. + */ + Center, + /** + * Align text to the end (right for LTR, left for RTL). + * For TextPath, align text to the path end. + */ + End, + /** + * Justify text (stretch to fill the available width). + * For TextPath, force text to fill the path by adjusting letter spacing. + */ + Justify }; } // namespace pagx diff --git a/pagx/include/pagx/model/TextLayout.h b/pagx/include/pagx/nodes/TextLayout.h similarity index 51% rename from pagx/include/pagx/model/TextLayout.h rename to pagx/include/pagx/nodes/TextLayout.h index 494cae5c0a..54a41aee1c 100644 --- a/pagx/include/pagx/model/TextLayout.h +++ b/pagx/include/pagx/nodes/TextLayout.h @@ -18,24 +18,11 @@ #pragma once -#include -#include "pagx/model/Element.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/TextAlign.h" namespace pagx { -/** - * Text horizontal alignment. - */ -enum class TextAlign { - Left, - Center, - Right, - Justify -}; - -std::string TextAlignToString(TextAlign align); -TextAlign TextAlignFromString(const std::string& str); - /** * Text vertical alignment. */ @@ -45,29 +32,35 @@ enum class VerticalAlign { Bottom }; -std::string VerticalAlignToString(VerticalAlign align); -VerticalAlign VerticalAlignFromString(const std::string& str); - /** - * Text overflow handling. + * Text direction (horizontal or vertical writing mode). */ -enum class Overflow { - Clip, - Visible, - Ellipsis +enum class TextDirection { + Horizontal, + Vertical }; -std::string OverflowToString(Overflow overflow); -Overflow OverflowFromString(const std::string& str); - /** - * TextLayout is a text animator that controls text layout within a bounding box. It provides - * options for text alignment, line height, indentation, and overflow handling. + * TextLayout is a text modifier that controls text layout and alignment. It supports two modes: + * - Point Text mode (width=0): Single-line text with anchor-based alignment at position (x, y). + * - Box Text mode (width>0): Multi-line text within a bounding box with word wrapping. */ class TextLayout : public Element { public: /** - * The width of the text box in pixels. A value of 0 means auto-width. The default value is 0. + * The x position of the text layout origin. The default value is 0. + */ + float x = 0; + + /** + * The y position of the text layout origin. The default value is 0. + */ + float y = 0; + + /** + * The width of the text box in pixels. A value of 0 enables Point Text mode (single-line, + * anchor-based alignment). A value greater than 0 enables Box Text mode (multi-line with word + * wrapping). The default value is 0. */ float width = 0; @@ -77,33 +70,32 @@ class TextLayout : public Element { float height = 0; /** - * The horizontal text alignment (Left, Center, Right, or Justify). The default value is Left. + * The horizontal text alignment. The default value is Start. */ - TextAlign textAlign = TextAlign::Left; + TextAlign textAlign = TextAlign::Start; /** - * The vertical text alignment (Top, Middle, or Bottom). The default value is Top. + * The alignment of the last line when textAlign is Justify. The default value is Start. */ - VerticalAlign verticalAlign = VerticalAlign::Top; + TextAlign textAlignLast = TextAlign::Start; /** - * The line height multiplier. The default value is 1.2. + * The vertical text alignment (only effective when height > 0). The default value is Top. */ - float lineHeight = 1.2f; + VerticalAlign verticalAlign = VerticalAlign::Top; /** - * The first-line indent in pixels. The default value is 0. + * The line height multiplier. The default value is 1.2. */ - float indent = 0; + float lineHeight = 1.2f; /** - * The overflow behavior when text exceeds the bounding box (Clip, Visible, or Scroll). The - * default value is Clip. + * The text direction (horizontal or vertical writing mode). The default value is Horizontal. */ - Overflow overflow = Overflow::Clip; + TextDirection direction = TextDirection::Horizontal; - ElementType type() const override { - return ElementType::TextLayout; + NodeType nodeType() const override { + return NodeType::TextLayout; } }; diff --git a/pagx/include/pagx/model/TextModifier.h b/pagx/include/pagx/nodes/TextModifier.h similarity index 93% rename from pagx/include/pagx/model/TextModifier.h rename to pagx/include/pagx/nodes/TextModifier.h index a6ad2e29ea..490ecc352f 100644 --- a/pagx/include/pagx/model/TextModifier.h +++ b/pagx/include/pagx/nodes/TextModifier.h @@ -21,9 +21,9 @@ #include #include #include -#include "pagx/model/Element.h" -#include "pagx/model/TextSelector.h" -#include "pagx/model/types/Point.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/TextSelector.h" +#include "pagx/nodes/Point.h" namespace pagx { @@ -90,8 +90,8 @@ class TextModifier : public Element { */ std::vector> selectors = {}; - ElementType type() const override { - return ElementType::TextModifier; + NodeType nodeType() const override { + return NodeType::TextModifier; } }; diff --git a/pagx/include/pagx/model/TextPath.h b/pagx/include/pagx/nodes/TextPath.h similarity index 71% rename from pagx/include/pagx/model/TextPath.h rename to pagx/include/pagx/nodes/TextPath.h index e1af9f90eb..e0d53eb352 100644 --- a/pagx/include/pagx/model/TextPath.h +++ b/pagx/include/pagx/nodes/TextPath.h @@ -19,24 +19,13 @@ #pragma once #include -#include "pagx/model/Element.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/TextAlign.h" namespace pagx { /** - * Text path alignment. - */ -enum class TextPathAlign { - Start, - Center, - End -}; - -std::string TextPathAlignToString(TextPathAlign align); -TextPathAlign TextPathAlignFromString(const std::string& str); - -/** - * TextPath is a text animator that places text along a path. It allows text to follow the contour + * TextPath is a text modifier that places text along a path. It allows text to follow the contour * of a referenced path shape. */ class TextPath : public Element { @@ -47,9 +36,13 @@ class TextPath : public Element { std::string path = {}; /** - * The alignment of text along the path (Start, Center, or Justify). The default value is Start. + * The alignment of text along the path. The default value is Start. + * - Start: Align text to the path start. + * - Center: Center text along the path. + * - End: Align text to the path end. + * - Justify: Force text to fill the path by adjusting letter spacing. */ - TextPathAlign pathAlign = TextPathAlign::Start; + TextAlign textAlign = TextAlign::Start; /** * The margin from the start of the path in pixels. The default value is 0. @@ -71,14 +64,8 @@ class TextPath : public Element { */ bool reversed = false; - /** - * Whether to force text alignment to the path even when it exceeds the path length. The default - * value is false. - */ - bool forceAlignment = false; - - ElementType type() const override { - return ElementType::TextPath; + NodeType nodeType() const override { + return NodeType::TextPath; } }; diff --git a/pagx/include/pagx/model/TextSelector.h b/pagx/include/pagx/nodes/TextSelector.h similarity index 87% rename from pagx/include/pagx/model/TextSelector.h rename to pagx/include/pagx/nodes/TextSelector.h index 27a3f6fb6e..1988ecdb7c 100644 --- a/pagx/include/pagx/model/TextSelector.h +++ b/pagx/include/pagx/nodes/TextSelector.h @@ -18,16 +18,16 @@ #pragma once -namespace pagx { +#include "pagx/nodes/Node.h" -enum class TextSelectorType { - RangeSelector -}; +namespace pagx { -class TextSelector { +/** + * Base class for text selectors. + */ +class TextSelector : public Node { public: - virtual ~TextSelector() = default; - virtual TextSelectorType type() const = 0; + ~TextSelector() override = default; protected: TextSelector() = default; diff --git a/pagx/include/pagx/model/TextSpan.h b/pagx/include/pagx/nodes/TextSpan.h similarity index 93% rename from pagx/include/pagx/model/TextSpan.h rename to pagx/include/pagx/nodes/TextSpan.h index f4643e956c..844527f67d 100644 --- a/pagx/include/pagx/model/TextSpan.h +++ b/pagx/include/pagx/nodes/TextSpan.h @@ -19,8 +19,8 @@ #pragma once #include -#include "pagx/model/Element.h" -#include "pagx/model/types/Point.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/Point.h" namespace pagx { @@ -70,8 +70,8 @@ class TextSpan : public Element { */ std::string text = {}; - ElementType type() const override { - return ElementType::TextSpan; + NodeType nodeType() const override { + return NodeType::TextSpan; } }; diff --git a/pagx/include/pagx/model/types/TileMode.h b/pagx/include/pagx/nodes/TileMode.h similarity index 90% rename from pagx/include/pagx/model/types/TileMode.h rename to pagx/include/pagx/nodes/TileMode.h index 758865336c..502ec64349 100644 --- a/pagx/include/pagx/model/types/TileMode.h +++ b/pagx/include/pagx/nodes/TileMode.h @@ -18,8 +18,6 @@ #pragma once -#include - namespace pagx { /** @@ -32,7 +30,4 @@ enum class TileMode { Decal }; -std::string TileModeToString(TileMode mode); -TileMode TileModeFromString(const std::string& str); - } // namespace pagx diff --git a/pagx/include/pagx/model/TrimPath.h b/pagx/include/pagx/nodes/TrimPath.h similarity index 78% rename from pagx/include/pagx/model/TrimPath.h rename to pagx/include/pagx/nodes/TrimPath.h index 3da8c47fc3..3fe55a5e5b 100644 --- a/pagx/include/pagx/model/TrimPath.h +++ b/pagx/include/pagx/nodes/TrimPath.h @@ -18,8 +18,7 @@ #pragma once -#include -#include "pagx/model/Element.h" +#include "pagx/nodes/Element.h" namespace pagx { @@ -31,9 +30,6 @@ enum class TrimType { Continuous }; -std::string TrimTypeToString(TrimType type); -TrimType TrimTypeFromString(const std::string& str); - /** * TrimPath is a path modifier that trims paths to a specified range. It can be used to animate * path drawing or reveal effects by adjusting the start and end values. @@ -53,20 +49,21 @@ class TrimPath : public Element { float end = 1; /** - * The offset to shift the trim range along the path, where 1 represents a full path length. The - * default value is 0. + * The offset to shift the trim range along the path. The value is in degrees, where 360 degrees + * equals a full cycle of the path length. For example, 180 degrees shifts the trim range by half + * the path. The default value is 0. */ float offset = 0; /** * The trim type that determines how multiple paths are trimmed. Separate trims each path - * individually, while Simultaneous trims all paths as one continuous path. The default value is + * individually, while Continuous trims all paths as one continuous path. The default value is * Separate. */ - TrimType trimType = TrimType::Separate; + TrimType type = TrimType::Separate; - ElementType type() const override { - return ElementType::TrimPath; + NodeType nodeType() const override { + return NodeType::TrimPath; } }; diff --git a/pagx/src/PAGXDocument.cpp b/pagx/src/PAGXDocument.cpp index a6fa94c1d4..4807358ac6 100644 --- a/pagx/src/PAGXDocument.cpp +++ b/pagx/src/PAGXDocument.cpp @@ -16,52 +16,19 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "pagx/model/Document.h" -#include -#include -#include "pagx/model/Composition.h" -#include "PAGXExporter.h" -#include "PAGXImporter.h" +#include "pagx/PAGXDocument.h" +#include "pagx/nodes/Composition.h" namespace pagx { -std::shared_ptr Document::Make(float docWidth, float docHeight) { - auto doc = std::shared_ptr(new Document()); +std::shared_ptr PAGXDocument::Make(float docWidth, float docHeight) { + auto doc = std::shared_ptr(new PAGXDocument()); doc->width = docWidth; doc->height = docHeight; return doc; } -std::shared_ptr Document::FromFile(const std::string& filePath) { - std::ifstream file(filePath, std::ios::binary); - if (!file.is_open()) { - return nullptr; - } - std::stringstream buffer = {}; - buffer << file.rdbuf(); - auto doc = FromXML(buffer.str()); - if (doc) { - auto lastSlash = filePath.find_last_of("/\\"); - if (lastSlash != std::string::npos) { - doc->basePath = filePath.substr(0, lastSlash + 1); - } - } - return doc; -} - -std::shared_ptr Document::FromXML(const std::string& xmlContent) { - return FromXML(reinterpret_cast(xmlContent.data()), xmlContent.size()); -} - -std::shared_ptr Document::FromXML(const uint8_t* data, size_t length) { - return PAGXImporter::Parse(data, length); -} - -std::string Document::toXML() { - return PAGXExporter::Export(*this); -} - -Node* Document::findResource(const std::string& id) { +Node* PAGXDocument::findResource(const std::string& id) { if (resourceMapDirty) { rebuildResourceMap(); } @@ -69,7 +36,7 @@ Node* Document::findResource(const std::string& id) { return it != resourceMap.end() ? it->second : nullptr; } -Layer* Document::findLayer(const std::string& id) const { +Layer* PAGXDocument::findLayer(const std::string& id) const { // First search in top-level layers auto found = findLayerRecursive(layers, id); if (found) { @@ -88,7 +55,7 @@ Layer* Document::findLayer(const std::string& id) const { return nullptr; } -void Document::rebuildResourceMap() { +void PAGXDocument::rebuildResourceMap() { resourceMap.clear(); for (const auto& resource : resources) { if (!resource->id.empty()) { @@ -98,8 +65,8 @@ void Document::rebuildResourceMap() { resourceMapDirty = false; } -Layer* Document::findLayerRecursive(const std::vector>& layers, - const std::string& id) { +Layer* PAGXDocument::findLayerRecursive(const std::vector>& layers, + const std::string& id) { for (const auto& layer : layers) { if (layer->id == id) { return layer.get(); diff --git a/pagx/src/PAGXElement.cpp b/pagx/src/PAGXElement.cpp deleted file mode 100644 index 39cfbebd73..0000000000 --- a/pagx/src/PAGXElement.cpp +++ /dev/null @@ -1,126 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "pagx/model/ColorSource.h" -#include "pagx/model/Element.h" -#include "pagx/model/LayerFilter.h" -#include "pagx/model/LayerStyle.h" -#include "pagx/model/Node.h" - -namespace pagx { - -const char* NodeTypeName(NodeType type) { - switch (type) { - case NodeType::Image: - return "Image"; - case NodeType::PathData: - return "PathData"; - case NodeType::Composition: - return "Composition"; - default: - return "Unknown"; - } -} - -const char* ElementTypeName(ElementType type) { - switch (type) { - case ElementType::Rectangle: - return "Rectangle"; - case ElementType::Ellipse: - return "Ellipse"; - case ElementType::Polystar: - return "Polystar"; - case ElementType::Path: - return "Path"; - case ElementType::TextSpan: - return "TextSpan"; - case ElementType::Fill: - return "Fill"; - case ElementType::Stroke: - return "Stroke"; - case ElementType::TrimPath: - return "TrimPath"; - case ElementType::RoundCorner: - return "RoundCorner"; - case ElementType::MergePath: - return "MergePath"; - case ElementType::TextModifier: - return "TextModifier"; - case ElementType::TextPath: - return "TextPath"; - case ElementType::TextLayout: - return "TextLayout"; - case ElementType::Group: - return "Group"; - case ElementType::Repeater: - return "Repeater"; - default: - return "Unknown"; - } -} - -const char* ColorSourceTypeName(ColorSourceType type) { - switch (type) { - case ColorSourceType::SolidColor: - return "SolidColor"; - case ColorSourceType::LinearGradient: - return "LinearGradient"; - case ColorSourceType::RadialGradient: - return "RadialGradient"; - case ColorSourceType::ConicGradient: - return "ConicGradient"; - case ColorSourceType::DiamondGradient: - return "DiamondGradient"; - case ColorSourceType::ImagePattern: - return "ImagePattern"; - default: - return "Unknown"; - } -} - -const char* LayerStyleTypeName(LayerStyleType type) { - switch (type) { - case LayerStyleType::DropShadowStyle: - return "DropShadowStyle"; - case LayerStyleType::InnerShadowStyle: - return "InnerShadowStyle"; - case LayerStyleType::BackgroundBlurStyle: - return "BackgroundBlurStyle"; - default: - return "Unknown"; - } -} - -const char* LayerFilterTypeName(LayerFilterType type) { - switch (type) { - case LayerFilterType::BlurFilter: - return "BlurFilter"; - case LayerFilterType::DropShadowFilter: - return "DropShadowFilter"; - case LayerFilterType::InnerShadowFilter: - return "InnerShadowFilter"; - case LayerFilterType::BlendFilter: - return "BlendFilter"; - case LayerFilterType::ColorMatrixFilter: - return "ColorMatrixFilter"; - default: - return "Unknown"; - } -} - -} // namespace pagx diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index 654fce97a1..55db9ba4af 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -16,42 +16,42 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "PAGXExporter.h" +#include "PAGXExporterImpl.h" #include #include "PAGXStringUtils.h" -#include "pagx/model/BackgroundBlurStyle.h" -#include "pagx/model/BlendFilter.h" -#include "pagx/model/BlurFilter.h" -#include "pagx/model/ColorMatrixFilter.h" -#include "pagx/model/Composition.h" -#include "pagx/model/ConicGradient.h" -#include "pagx/model/DiamondGradient.h" -#include "pagx/model/Document.h" -#include "pagx/model/DropShadowFilter.h" -#include "pagx/model/DropShadowStyle.h" -#include "pagx/model/Ellipse.h" -#include "pagx/model/Fill.h" -#include "pagx/model/Group.h" -#include "pagx/model/Image.h" -#include "pagx/model/ImagePattern.h" -#include "pagx/model/InnerShadowFilter.h" -#include "pagx/model/InnerShadowStyle.h" -#include "pagx/model/LinearGradient.h" -#include "pagx/model/MergePath.h" -#include "pagx/model/Path.h" -#include "pagx/model/Polystar.h" -#include "pagx/model/RadialGradient.h" -#include "pagx/model/RangeSelector.h" -#include "pagx/model/Rectangle.h" -#include "pagx/model/Repeater.h" -#include "pagx/model/RoundCorner.h" -#include "pagx/model/SolidColor.h" -#include "pagx/model/Stroke.h" -#include "pagx/model/TextLayout.h" -#include "pagx/model/TextModifier.h" -#include "pagx/model/TextPath.h" -#include "pagx/model/TextSpan.h" -#include "pagx/model/TrimPath.h" +#include "pagx/nodes/BackgroundBlurStyle.h" +#include "pagx/nodes/BlendFilter.h" +#include "pagx/nodes/BlurFilter.h" +#include "pagx/nodes/ColorMatrixFilter.h" +#include "pagx/nodes/Composition.h" +#include "pagx/nodes/ConicGradient.h" +#include "pagx/nodes/DiamondGradient.h" +#include "pagx/PAGXDocument.h" +#include "pagx/nodes/DropShadowFilter.h" +#include "pagx/nodes/DropShadowStyle.h" +#include "pagx/nodes/Ellipse.h" +#include "pagx/nodes/Fill.h" +#include "pagx/nodes/Group.h" +#include "pagx/nodes/Image.h" +#include "pagx/nodes/ImagePattern.h" +#include "pagx/nodes/InnerShadowFilter.h" +#include "pagx/nodes/InnerShadowStyle.h" +#include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/MergePath.h" +#include "pagx/nodes/Path.h" +#include "pagx/nodes/Polystar.h" +#include "pagx/nodes/RadialGradient.h" +#include "pagx/nodes/RangeSelector.h" +#include "pagx/nodes/Rectangle.h" +#include "pagx/nodes/Repeater.h" +#include "pagx/nodes/RoundCorner.h" +#include "pagx/nodes/SolidColor.h" +#include "pagx/nodes/Stroke.h" +#include "pagx/nodes/TextLayout.h" +#include "pagx/nodes/TextModifier.h" +#include "pagx/nodes/TextPath.h" +#include "pagx/nodes/TextSpan.h" +#include "pagx/nodes/TrimPath.h" namespace pagx { @@ -560,7 +560,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { } else { xml.closeElementStart(); for (const auto& selector : modifier->selectors) { - if (selector->type() != TextSelectorType::RangeSelector) { + if (selector->nodeType() != NodeType::RangeSelector) { continue; } auto rangeSelector = static_cast(selector.get()); @@ -685,8 +685,8 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { //============================================================================== static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { - switch (node->type()) { - case LayerStyleType::DropShadowStyle: { + switch (node->nodeType()) { + case NodeType::DropShadowStyle: { auto style = static_cast(node); xml.openElement("DropShadowStyle"); if (style->blendMode != BlendMode::Normal) { @@ -701,7 +701,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.closeElementSelfClosing(); break; } - case LayerStyleType::InnerShadowStyle: { + case NodeType::InnerShadowStyle: { auto style = static_cast(node); xml.openElement("InnerShadowStyle"); if (style->blendMode != BlendMode::Normal) { @@ -715,7 +715,7 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { xml.closeElementSelfClosing(); break; } - case LayerStyleType::BackgroundBlurStyle: { + case NodeType::BackgroundBlurStyle: { auto style = static_cast(node); xml.openElement("BackgroundBlurStyle"); if (style->blendMode != BlendMode::Normal) { @@ -739,8 +739,8 @@ static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { //============================================================================== static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { - switch (node->type()) { - case LayerFilterType::BlurFilter: { + switch (node->nodeType()) { + case NodeType::BlurFilter: { auto filter = static_cast(node); xml.openElement("BlurFilter"); xml.addRequiredAttribute("blurrinessX", filter->blurrinessX); @@ -751,7 +751,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.closeElementSelfClosing(); break; } - case LayerFilterType::DropShadowFilter: { + case NodeType::DropShadowFilter: { auto filter = static_cast(node); xml.openElement("DropShadowFilter"); xml.addAttribute("offsetX", filter->offsetX); @@ -763,7 +763,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.closeElementSelfClosing(); break; } - case LayerFilterType::InnerShadowFilter: { + case NodeType::InnerShadowFilter: { auto filter = static_cast(node); xml.openElement("InnerShadowFilter"); xml.addAttribute("offsetX", filter->offsetX); @@ -775,7 +775,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.closeElementSelfClosing(); break; } - case LayerFilterType::BlendFilter: { + case NodeType::BlendFilter: { auto filter = static_cast(node); xml.openElement("BlendFilter"); xml.addAttribute("color", ColorToHexString(filter->color, filter->color.alpha < 1.0f)); @@ -785,7 +785,7 @@ static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { xml.closeElementSelfClosing(); break; } - case LayerFilterType::ColorMatrixFilter: { + case NodeType::ColorMatrixFilter: { auto filter = static_cast(node); xml.openElement("ColorMatrixFilter"); std::vector values(filter->matrix.begin(), filter->matrix.end()); @@ -926,7 +926,7 @@ static void writeLayer(XMLBuilder& xml, const Layer* node) { // Main Export function //============================================================================== -std::string PAGXExporter::Export(const Document& doc) { +std::string PAGXExporterImpl::Export(const PAGXDocument& doc) { XMLBuilder xml = {}; xml.appendDeclaration(); diff --git a/pagx/include/pagx/model/BackgroundBlurStyle.h b/pagx/src/PAGXExporterAPI.cpp similarity index 68% rename from pagx/include/pagx/model/BackgroundBlurStyle.h rename to pagx/src/PAGXExporterAPI.cpp index ae0288d2b7..b2ed578729 100644 --- a/pagx/include/pagx/model/BackgroundBlurStyle.h +++ b/pagx/src/PAGXExporterAPI.cpp @@ -16,27 +16,24 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#pragma once - -#include "pagx/model/LayerStyle.h" -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/TileMode.h" +#include "pagx/PAGXExporter.h" +#include +#include "PAGXExporterImpl.h" namespace pagx { -/** - * Background blur style. - */ -class BackgroundBlurStyle : public LayerStyle { - public: - float blurrinessX = 0; - float blurrinessY = 0; - TileMode tileMode = TileMode::Mirror; - BlendMode blendMode = BlendMode::Normal; +std::string PAGXExporter::ToXML(const PAGXDocument& document) { + return PAGXExporterImpl::Export(document); +} - LayerStyleType type() const override { - return LayerStyleType::BackgroundBlurStyle; +bool PAGXExporter::ToFile(const PAGXDocument& document, const std::string& filePath) { + std::string xml = ToXML(document); + std::ofstream file(filePath, std::ios::binary); + if (!file.is_open()) { + return false; } -}; + file << xml; + return file.good(); +} } // namespace pagx diff --git a/pagx/include/pagx/model/BlendFilter.h b/pagx/src/PAGXExporterImpl.h similarity index 73% rename from pagx/include/pagx/model/BlendFilter.h rename to pagx/src/PAGXExporterImpl.h index 9335c684cd..665cd66811 100644 --- a/pagx/include/pagx/model/BlendFilter.h +++ b/pagx/src/PAGXExporterImpl.h @@ -18,23 +18,22 @@ #pragma once -#include "pagx/model/LayerFilter.h" -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Color.h" +#include +#include "pagx/PAGXDocument.h" namespace pagx { /** - * Blend filter. + * Internal implementation of PAGX XML exporter. + * Exports a PAGXDocument to XML string without any optimization. + * The output faithfully reflects the structure of the input Document. */ -class BlendFilter : public LayerFilter { +class PAGXExporterImpl { public: - Color color = {}; - BlendMode blendMode = BlendMode::Normal; - - LayerFilterType type() const override { - return LayerFilterType::BlendFilter; - } + /** + * Exports a PAGXDocument to XML string. + */ + static std::string Export(const PAGXDocument& doc); }; } // namespace pagx diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 94582cc3a1..a08aed7afe 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -16,7 +16,7 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "PAGXImporter.h" +#include "PAGXImporterImpl.h" #include #include #include "PAGXStringUtils.h" @@ -260,22 +260,22 @@ class XMLTokenizer { // PAGXXMLParser implementation //============================================================================== -std::shared_ptr PAGXImporter::Parse(const uint8_t* data, size_t length) { +std::shared_ptr PAGXImporterImpl::Parse(const uint8_t* data, size_t length) { auto root = parseXML(data, length); if (!root || root->tag != "pagx") { return nullptr; } - auto doc = std::shared_ptr(new Document()); + auto doc = std::shared_ptr(new PAGXDocument()); parseDocument(root.get(), doc.get()); return doc; } -std::unique_ptr PAGXImporter::parseXML(const uint8_t* data, size_t length) { +std::unique_ptr PAGXImporterImpl::parseXML(const uint8_t* data, size_t length) { XMLTokenizer tokenizer(data, length); return tokenizer.parse(); } -void PAGXImporter::parseDocument(const XMLNode* root, Document* doc) { +void PAGXImporterImpl::parseDocument(const XMLNode* root, PAGXDocument* doc) { doc->version = getAttribute(root, "version", "1.0"); doc->width = getFloatAttribute(root, "width", 0); doc->height = getFloatAttribute(root, "height", 0); @@ -292,7 +292,7 @@ void PAGXImporter::parseDocument(const XMLNode* root, Document* doc) { } } -void PAGXImporter::parseResources(const XMLNode* node, Document* doc) { +void PAGXImporterImpl::parseResources(const XMLNode* node, PAGXDocument* doc) { for (const auto& child : node->children) { // Try to parse as a resource (including color sources) auto resource = parseResource(child.get()); @@ -308,7 +308,7 @@ void PAGXImporter::parseResources(const XMLNode* node, Document* doc) { } } -std::unique_ptr PAGXImporter::parseResource(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseResource(const XMLNode* node) { if (node->tag == "Image") { return parseImage(node); } @@ -321,7 +321,7 @@ std::unique_ptr PAGXImporter::parseResource(const XMLNode* node) { return nullptr; } -std::unique_ptr PAGXImporter::parseLayer(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseLayer(const XMLNode* node) { auto layer = std::make_unique(); layer->id = getAttribute(node, "id"); layer->name = getAttribute(node, "name"); @@ -404,7 +404,7 @@ std::unique_ptr PAGXImporter::parseLayer(const XMLNode* node) { return layer; } -void PAGXImporter::parseContents(const XMLNode* node, Layer* layer) { +void PAGXImporterImpl::parseContents(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { auto element = parseElement(child.get()); if (element) { @@ -413,7 +413,7 @@ void PAGXImporter::parseContents(const XMLNode* node, Layer* layer) { } } -void PAGXImporter::parseStyles(const XMLNode* node, Layer* layer) { +void PAGXImporterImpl::parseStyles(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { auto style = parseLayerStyle(child.get()); if (style) { @@ -422,7 +422,7 @@ void PAGXImporter::parseStyles(const XMLNode* node, Layer* layer) { } } -void PAGXImporter::parseFilters(const XMLNode* node, Layer* layer) { +void PAGXImporterImpl::parseFilters(const XMLNode* node, Layer* layer) { for (const auto& child : node->children) { auto filter = parseLayerFilter(child.get()); if (filter) { @@ -431,7 +431,7 @@ void PAGXImporter::parseFilters(const XMLNode* node, Layer* layer) { } } -std::unique_ptr PAGXImporter::parseElement(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseElement(const XMLNode* node) { if (node->tag == "Rectangle") { return parseRectangle(node); } @@ -480,7 +480,7 @@ std::unique_ptr PAGXImporter::parseElement(const XMLNode* node) { return nullptr; } -std::unique_ptr PAGXImporter::parseColorSource(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseColorSource(const XMLNode* node) { if (node->tag == "SolidColor") { return parseSolidColor(node); } @@ -502,7 +502,7 @@ std::unique_ptr PAGXImporter::parseColorSource(const XMLNode* node) return nullptr; } -std::unique_ptr PAGXImporter::parseLayerStyle(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseLayerStyle(const XMLNode* node) { if (node->tag == "DropShadowStyle") { return parseDropShadowStyle(node); } @@ -515,7 +515,7 @@ std::unique_ptr PAGXImporter::parseLayerStyle(const XMLNode* node) { return nullptr; } -std::unique_ptr PAGXImporter::parseLayerFilter(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseLayerFilter(const XMLNode* node) { if (node->tag == "BlurFilter") { return parseBlurFilter(node); } @@ -538,7 +538,7 @@ std::unique_ptr PAGXImporter::parseLayerFilter(const XMLNode* node) // Geometry element parsing //============================================================================== -std::unique_ptr PAGXImporter::parseRectangle(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseRectangle(const XMLNode* node) { auto rect = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); rect->center = parsePoint(centerStr); @@ -549,7 +549,7 @@ std::unique_ptr PAGXImporter::parseRectangle(const XMLNode* node) { return rect; } -std::unique_ptr PAGXImporter::parseEllipse(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseEllipse(const XMLNode* node) { auto ellipse = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); ellipse->center = parsePoint(centerStr); @@ -559,7 +559,7 @@ std::unique_ptr PAGXImporter::parseEllipse(const XMLNode* node) { return ellipse; } -std::unique_ptr PAGXImporter::parsePolystar(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parsePolystar(const XMLNode* node) { auto polystar = std::make_unique(); auto centerStr = getAttribute(node, "center", "0,0"); polystar->center = parsePoint(centerStr); @@ -574,7 +574,7 @@ std::unique_ptr PAGXImporter::parsePolystar(const XMLNode* node) { return polystar; } -std::unique_ptr PAGXImporter::parsePath(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parsePath(const XMLNode* node) { auto path = std::make_unique(); auto dataAttr = getAttribute(node, "data"); if (!dataAttr.empty()) { @@ -584,7 +584,7 @@ std::unique_ptr PAGXImporter::parsePath(const XMLNode* node) { return path; } -std::unique_ptr PAGXImporter::parseTextSpan(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseTextSpan(const XMLNode* node) { auto textSpan = std::make_unique(); auto positionStr = getAttribute(node, "position", "0,0"); textSpan->position = parsePoint(positionStr); @@ -602,7 +602,7 @@ std::unique_ptr PAGXImporter::parseTextSpan(const XMLNode* node) { // Painter parsing //============================================================================== -std::unique_ptr PAGXImporter::parseFill(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseFill(const XMLNode* node) { auto fill = std::make_unique(); fill->colorRef = getAttribute(node, "color"); fill->alpha = getFloatAttribute(node, "alpha", 1); @@ -621,7 +621,7 @@ std::unique_ptr PAGXImporter::parseFill(const XMLNode* node) { return fill; } -std::unique_ptr PAGXImporter::parseStroke(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseStroke(const XMLNode* node) { auto stroke = std::make_unique(); stroke->colorRef = getAttribute(node, "color"); stroke->width = getFloatAttribute(node, "width", 1); @@ -653,7 +653,7 @@ std::unique_ptr PAGXImporter::parseStroke(const XMLNode* node) { // Modifier parsing //============================================================================== -std::unique_ptr PAGXImporter::parseTrimPath(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseTrimPath(const XMLNode* node) { auto trim = std::make_unique(); trim->start = getFloatAttribute(node, "start", 0); trim->end = getFloatAttribute(node, "end", 1); @@ -662,19 +662,19 @@ std::unique_ptr PAGXImporter::parseTrimPath(const XMLNode* node) { return trim; } -std::unique_ptr PAGXImporter::parseRoundCorner(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseRoundCorner(const XMLNode* node) { auto round = std::make_unique(); round->radius = getFloatAttribute(node, "radius", 0); return round; } -std::unique_ptr PAGXImporter::parseMergePath(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseMergePath(const XMLNode* node) { auto merge = std::make_unique(); merge->mode = MergePathModeFromString(getAttribute(node, "mode", "append")); return merge; } -std::unique_ptr PAGXImporter::parseTextModifier(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseTextModifier(const XMLNode* node) { auto modifier = std::make_unique(); auto anchorStr = getAttribute(node, "anchorPoint", "0.5,0.5"); modifier->anchorPoint = parsePoint(anchorStr); @@ -699,7 +699,7 @@ std::unique_ptr PAGXImporter::parseTextModifier(const XMLNode* nod return modifier; } -std::unique_ptr PAGXImporter::parseTextPath(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseTextPath(const XMLNode* node) { auto textPath = std::make_unique(); textPath->path = getAttribute(node, "path"); textPath->textAlign = TextAlignFromString(getAttribute(node, "textAlign", "start")); @@ -710,7 +710,7 @@ std::unique_ptr PAGXImporter::parseTextPath(const XMLNode* node) { return textPath; } -std::unique_ptr PAGXImporter::parseTextLayout(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseTextLayout(const XMLNode* node) { auto layout = std::make_unique(); layout->x = getFloatAttribute(node, "x", 0); layout->y = getFloatAttribute(node, "y", 0); @@ -724,7 +724,7 @@ std::unique_ptr PAGXImporter::parseTextLayout(const XMLNode* node) { return layout; } -std::unique_ptr PAGXImporter::parseRepeater(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseRepeater(const XMLNode* node) { auto repeater = std::make_unique(); repeater->copies = getFloatAttribute(node, "copies", 3); repeater->offset = getFloatAttribute(node, "offset", 0); @@ -741,7 +741,7 @@ std::unique_ptr PAGXImporter::parseRepeater(const XMLNode* node) { return repeater; } -std::unique_ptr PAGXImporter::parseGroup(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseGroup(const XMLNode* node) { auto group = std::make_unique(); // group->name (removed) = getAttribute(node, "name"); auto anchorStr = getAttribute(node, "anchorPoint", "0,0"); @@ -765,7 +765,7 @@ std::unique_ptr PAGXImporter::parseGroup(const XMLNode* node) { return group; } -RangeSelector PAGXImporter::parseRangeSelector(const XMLNode* node) { +RangeSelector PAGXImporterImpl::parseRangeSelector(const XMLNode* node) { RangeSelector selector = {}; selector.start = getFloatAttribute(node, "start", 0); selector.end = getFloatAttribute(node, "end", 1); @@ -785,7 +785,7 @@ RangeSelector PAGXImporter::parseRangeSelector(const XMLNode* node) { // Color source parsing //============================================================================== -std::unique_ptr PAGXImporter::parseSolidColor(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseSolidColor(const XMLNode* node) { auto solid = std::make_unique(); solid->id = getAttribute(node, "id"); solid->color.red = getFloatAttribute(node, "red", 0); @@ -796,7 +796,7 @@ std::unique_ptr PAGXImporter::parseSolidColor(const XMLNode* node) { return solid; } -std::unique_ptr PAGXImporter::parseLinearGradient(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseLinearGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto startPointStr = getAttribute(node, "startPoint", "0,0"); @@ -815,7 +815,7 @@ std::unique_ptr PAGXImporter::parseLinearGradient(const XMLNode* return gradient; } -std::unique_ptr PAGXImporter::parseRadialGradient(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseRadialGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto centerStr = getAttribute(node, "center", "0,0"); @@ -833,7 +833,7 @@ std::unique_ptr PAGXImporter::parseRadialGradient(const XMLNode* return gradient; } -std::unique_ptr PAGXImporter::parseConicGradient(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseConicGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto centerStr = getAttribute(node, "center", "0,0"); @@ -852,7 +852,7 @@ std::unique_ptr PAGXImporter::parseConicGradient(const XMLNode* n return gradient; } -std::unique_ptr PAGXImporter::parseDiamondGradient(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseDiamondGradient(const XMLNode* node) { auto gradient = std::make_unique(); gradient->id = getAttribute(node, "id"); auto centerStr = getAttribute(node, "center", "0,0"); @@ -870,7 +870,7 @@ std::unique_ptr PAGXImporter::parseDiamondGradient(const XMLNod return gradient; } -std::unique_ptr PAGXImporter::parseImagePattern(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseImagePattern(const XMLNode* node) { auto pattern = std::make_unique(); pattern->id = getAttribute(node, "id"); pattern->image = getAttribute(node, "image"); @@ -884,7 +884,7 @@ std::unique_ptr PAGXImporter::parseImagePattern(const XMLNode* nod return pattern; } -ColorStop PAGXImporter::parseColorStop(const XMLNode* node) { +ColorStop PAGXImporterImpl::parseColorStop(const XMLNode* node) { ColorStop stop = {}; stop.offset = getFloatAttribute(node, "offset", 0); auto colorStr = getAttribute(node, "color"); @@ -898,21 +898,21 @@ ColorStop PAGXImporter::parseColorStop(const XMLNode* node) { // Resource parsing //============================================================================== -std::unique_ptr PAGXImporter::parseImage(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseImage(const XMLNode* node) { auto image = std::make_unique(); image->id = getAttribute(node, "id"); image->source = getAttribute(node, "source"); return image; } -std::unique_ptr PAGXImporter::parsePathData(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parsePathData(const XMLNode* node) { auto data = getAttribute(node, "data"); auto pathData = std::make_unique(PathData::FromSVGString(data)); pathData->id = getAttribute(node, "id"); return pathData; } -std::unique_ptr PAGXImporter::parseComposition(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseComposition(const XMLNode* node) { auto comp = std::make_unique(); comp->id = getAttribute(node, "id"); comp->width = getFloatAttribute(node, "width", 0); @@ -932,7 +932,7 @@ std::unique_ptr PAGXImporter::parseComposition(const XMLNode* node) // Layer style parsing //============================================================================== -std::unique_ptr PAGXImporter::parseDropShadowStyle(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseDropShadowStyle(const XMLNode* node) { auto style = std::make_unique(); style->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); style->offsetX = getFloatAttribute(node, "offsetX", 0); @@ -947,7 +947,7 @@ std::unique_ptr PAGXImporter::parseDropShadowStyle(const XMLNod return style; } -std::unique_ptr PAGXImporter::parseInnerShadowStyle(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseInnerShadowStyle(const XMLNode* node) { auto style = std::make_unique(); style->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); style->offsetX = getFloatAttribute(node, "offsetX", 0); @@ -961,7 +961,7 @@ std::unique_ptr PAGXImporter::parseInnerShadowStyle(const XMLN return style; } -std::unique_ptr PAGXImporter::parseBackgroundBlurStyle( +std::unique_ptr PAGXImporterImpl::parseBackgroundBlurStyle( const XMLNode* node) { auto style = std::make_unique(); style->blendMode = BlendModeFromString(getAttribute(node, "blendMode", "normal")); @@ -975,7 +975,7 @@ std::unique_ptr PAGXImporter::parseBackgroundBlurStyle( // Layer filter parsing //============================================================================== -std::unique_ptr PAGXImporter::parseBlurFilter(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseBlurFilter(const XMLNode* node) { auto filter = std::make_unique(); filter->blurrinessX = getFloatAttribute(node, "blurrinessX", 0); filter->blurrinessY = getFloatAttribute(node, "blurrinessY", 0); @@ -983,7 +983,7 @@ std::unique_ptr PAGXImporter::parseBlurFilter(const XMLNode* node) { return filter; } -std::unique_ptr PAGXImporter::parseDropShadowFilter(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseDropShadowFilter(const XMLNode* node) { auto filter = std::make_unique(); filter->offsetX = getFloatAttribute(node, "offsetX", 0); filter->offsetY = getFloatAttribute(node, "offsetY", 0); @@ -997,7 +997,7 @@ std::unique_ptr PAGXImporter::parseDropShadowFilter(const XMLN return filter; } -std::unique_ptr PAGXImporter::parseInnerShadowFilter(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseInnerShadowFilter(const XMLNode* node) { auto filter = std::make_unique(); filter->offsetX = getFloatAttribute(node, "offsetX", 0); filter->offsetY = getFloatAttribute(node, "offsetY", 0); @@ -1011,7 +1011,7 @@ std::unique_ptr PAGXImporter::parseInnerShadowFilter(const XM return filter; } -std::unique_ptr PAGXImporter::parseBlendFilter(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseBlendFilter(const XMLNode* node) { auto filter = std::make_unique(); auto colorStr = getAttribute(node, "color"); if (!colorStr.empty()) { @@ -1021,7 +1021,7 @@ std::unique_ptr PAGXImporter::parseBlendFilter(const XMLNode* node) return filter; } -std::unique_ptr PAGXImporter::parseColorMatrixFilter(const XMLNode* node) { +std::unique_ptr PAGXImporterImpl::parseColorMatrixFilter(const XMLNode* node) { auto filter = std::make_unique(); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { @@ -1037,13 +1037,13 @@ std::unique_ptr PAGXImporter::parseColorMatrixFilter(const XM // Utility functions //============================================================================== -std::string PAGXImporter::getAttribute(const XMLNode* node, const std::string& name, +std::string PAGXImporterImpl::getAttribute(const XMLNode* node, const std::string& name, const std::string& defaultValue) { auto it = node->attributes.find(name); return it != node->attributes.end() ? it->second : defaultValue; } -float PAGXImporter::getFloatAttribute(const XMLNode* node, const std::string& name, +float PAGXImporterImpl::getFloatAttribute(const XMLNode* node, const std::string& name, float defaultValue) { auto str = getAttribute(node, name); if (str.empty()) { @@ -1056,7 +1056,7 @@ float PAGXImporter::getFloatAttribute(const XMLNode* node, const std::string& na } } -int PAGXImporter::getIntAttribute(const XMLNode* node, const std::string& name, int defaultValue) { +int PAGXImporterImpl::getIntAttribute(const XMLNode* node, const std::string& name, int defaultValue) { auto str = getAttribute(node, name); if (str.empty()) { return defaultValue; @@ -1068,7 +1068,7 @@ int PAGXImporter::getIntAttribute(const XMLNode* node, const std::string& name, } } -bool PAGXImporter::getBoolAttribute(const XMLNode* node, const std::string& name, +bool PAGXImporterImpl::getBoolAttribute(const XMLNode* node, const std::string& name, bool defaultValue) { auto str = getAttribute(node, name); if (str.empty()) { @@ -1077,7 +1077,7 @@ bool PAGXImporter::getBoolAttribute(const XMLNode* node, const std::string& name return str == "true" || str == "1"; } -Point PAGXImporter::parsePoint(const std::string& str) { +Point PAGXImporterImpl::parsePoint(const std::string& str) { Point point = {}; std::istringstream iss(str); std::string token = {}; @@ -1097,7 +1097,7 @@ Point PAGXImporter::parsePoint(const std::string& str) { return point; } -Size PAGXImporter::parseSize(const std::string& str) { +Size PAGXImporterImpl::parseSize(const std::string& str) { Size size = {}; std::istringstream iss(str); std::string token = {}; @@ -1117,7 +1117,7 @@ Size PAGXImporter::parseSize(const std::string& str) { return size; } -Rect PAGXImporter::parseRect(const std::string& str) { +Rect PAGXImporterImpl::parseRect(const std::string& str) { Rect rect = {}; std::istringstream iss(str); std::string token = {}; @@ -1154,7 +1154,7 @@ int parseHexDigit(char c) { } } // namespace -Color PAGXImporter::parseColor(const std::string& str) { +Color PAGXImporterImpl::parseColor(const std::string& str) { if (str.empty()) { return {}; } @@ -1254,7 +1254,7 @@ Color PAGXImporter::parseColor(const std::string& str) { return {}; } -std::vector PAGXImporter::parseFloatList(const std::string& str) { +std::vector PAGXImporterImpl::parseFloatList(const std::string& str) { std::vector values = {}; std::istringstream iss(str); std::string token = {}; diff --git a/pagx/src/PAGXImporterAPI.cpp b/pagx/src/PAGXImporterAPI.cpp new file mode 100644 index 0000000000..362e9b7e91 --- /dev/null +++ b/pagx/src/PAGXImporterAPI.cpp @@ -0,0 +1,51 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/PAGXImporter.h" +#include +#include +#include "PAGXImporterImpl.h" + +namespace pagx { + +std::shared_ptr PAGXImporter::FromFile(const std::string& filePath) { + std::ifstream file(filePath, std::ios::binary); + if (!file.is_open()) { + return nullptr; + } + std::stringstream buffer = {}; + buffer << file.rdbuf(); + auto doc = FromXML(buffer.str()); + if (doc) { + auto lastSlash = filePath.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + doc->basePath = filePath.substr(0, lastSlash + 1); + } + } + return doc; +} + +std::shared_ptr PAGXImporter::FromXML(const std::string& xmlContent) { + return FromXML(reinterpret_cast(xmlContent.data()), xmlContent.size()); +} + +std::shared_ptr PAGXImporter::FromXML(const uint8_t* data, size_t length) { + return PAGXImporterImpl::Parse(data, length); +} + +} // namespace pagx diff --git a/pagx/src/PAGXImporter.h b/pagx/src/PAGXImporterImpl.h similarity index 78% rename from pagx/src/PAGXImporter.h rename to pagx/src/PAGXImporterImpl.h index c44262c088..724c6fd0bb 100644 --- a/pagx/src/PAGXImporter.h +++ b/pagx/src/PAGXImporterImpl.h @@ -22,41 +22,41 @@ #include #include #include -#include "pagx/model/BackgroundBlurStyle.h" -#include "pagx/model/BlendFilter.h" -#include "pagx/model/BlurFilter.h" -#include "pagx/model/ColorMatrixFilter.h" -#include "pagx/model/Composition.h" -#include "pagx/model/ConicGradient.h" -#include "pagx/model/DiamondGradient.h" -#include "pagx/model/Document.h" -#include "pagx/model/DropShadowFilter.h" -#include "pagx/model/DropShadowStyle.h" -#include "pagx/model/Ellipse.h" -#include "pagx/model/Fill.h" -#include "pagx/model/Group.h" -#include "pagx/model/Image.h" -#include "pagx/model/ImagePattern.h" -#include "pagx/model/InnerShadowFilter.h" -#include "pagx/model/InnerShadowStyle.h" -#include "pagx/model/LinearGradient.h" -#include "pagx/model/MergePath.h" -#include "pagx/model/Path.h" -#include "pagx/model/PathData.h" -#include "pagx/model/Polystar.h" -#include "pagx/model/RadialGradient.h" -#include "pagx/model/RangeSelector.h" -#include "pagx/model/Rectangle.h" -#include "pagx/model/Repeater.h" -#include "pagx/model/RoundCorner.h" -#include "pagx/model/SolidColor.h" -#include "pagx/model/Stroke.h" -#include "pagx/model/TextLayout.h" -#include "pagx/model/TextModifier.h" -#include "pagx/model/TextPath.h" -#include "pagx/model/TextSpan.h" -#include "pagx/model/TrimPath.h" -#include "pagx/model/types/Color.h" +#include "pagx/nodes/BackgroundBlurStyle.h" +#include "pagx/nodes/BlendFilter.h" +#include "pagx/nodes/BlurFilter.h" +#include "pagx/nodes/ColorMatrixFilter.h" +#include "pagx/nodes/Composition.h" +#include "pagx/nodes/ConicGradient.h" +#include "pagx/nodes/DiamondGradient.h" +#include "pagx/PAGXDocument.h" +#include "pagx/nodes/DropShadowFilter.h" +#include "pagx/nodes/DropShadowStyle.h" +#include "pagx/nodes/Ellipse.h" +#include "pagx/nodes/Fill.h" +#include "pagx/nodes/Group.h" +#include "pagx/nodes/Image.h" +#include "pagx/nodes/ImagePattern.h" +#include "pagx/nodes/InnerShadowFilter.h" +#include "pagx/nodes/InnerShadowStyle.h" +#include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/MergePath.h" +#include "pagx/nodes/Path.h" +#include "pagx/nodes/PathData.h" +#include "pagx/nodes/Polystar.h" +#include "pagx/nodes/RadialGradient.h" +#include "pagx/nodes/RangeSelector.h" +#include "pagx/nodes/Rectangle.h" +#include "pagx/nodes/Repeater.h" +#include "pagx/nodes/RoundCorner.h" +#include "pagx/nodes/SolidColor.h" +#include "pagx/nodes/Stroke.h" +#include "pagx/nodes/TextLayout.h" +#include "pagx/nodes/TextModifier.h" +#include "pagx/nodes/TextPath.h" +#include "pagx/nodes/TextSpan.h" +#include "pagx/nodes/TrimPath.h" +#include "pagx/nodes/Color.h" namespace pagx { @@ -71,20 +71,20 @@ struct XMLNode { }; /** - * Parser for PAGX XML format. + * Internal implementation of PAGX XML parser. */ -class PAGXImporter { +class PAGXImporterImpl { public: /** * Parses XML data into a PAGXDocument. */ - static std::shared_ptr Parse(const uint8_t* data, size_t length); + static std::shared_ptr Parse(const uint8_t* data, size_t length); private: static std::unique_ptr parseXML(const uint8_t* data, size_t length); - static void parseDocument(const XMLNode* root, Document* doc); - static void parseResources(const XMLNode* node, Document* doc); + static void parseDocument(const XMLNode* root, PAGXDocument* doc); + static void parseResources(const XMLNode* node, PAGXDocument* doc); static std::unique_ptr parseResource(const XMLNode* node); static std::unique_ptr parseLayer(const XMLNode* node); static void parseContents(const XMLNode* node, Layer* layer); diff --git a/pagx/src/PAGXStringUtils.cpp b/pagx/src/PAGXStringUtils.cpp index 6f5ef8ebf3..44a3ab66b4 100644 --- a/pagx/src/PAGXStringUtils.cpp +++ b/pagx/src/PAGXStringUtils.cpp @@ -70,6 +70,24 @@ const char* NodeTypeName(NodeType type) { return "DiamondGradient"; case NodeType::ImagePattern: return "ImagePattern"; + case NodeType::Layer: + return "Layer"; + case NodeType::DropShadowStyle: + return "DropShadowStyle"; + case NodeType::InnerShadowStyle: + return "InnerShadowStyle"; + case NodeType::BackgroundBlurStyle: + return "BackgroundBlurStyle"; + case NodeType::BlurFilter: + return "BlurFilter"; + case NodeType::DropShadowFilter: + return "DropShadowFilter"; + case NodeType::InnerShadowFilter: + return "InnerShadowFilter"; + case NodeType::BlendFilter: + return "BlendFilter"; + case NodeType::ColorMatrixFilter: + return "ColorMatrixFilter"; case NodeType::Rectangle: return "Rectangle"; case NodeType::Ellipse: @@ -100,6 +118,8 @@ const char* NodeTypeName(NodeType type) { return "Group"; case NodeType::Repeater: return "Repeater"; + case NodeType::RangeSelector: + return "RangeSelector"; default: return "Unknown"; } @@ -124,36 +144,6 @@ const char* ColorSourceTypeName(ColorSourceType type) { } } -const char* LayerStyleTypeName(LayerStyleType type) { - switch (type) { - case LayerStyleType::DropShadowStyle: - return "DropShadowStyle"; - case LayerStyleType::InnerShadowStyle: - return "InnerShadowStyle"; - case LayerStyleType::BackgroundBlurStyle: - return "BackgroundBlurStyle"; - default: - return "Unknown"; - } -} - -const char* LayerFilterTypeName(LayerFilterType type) { - switch (type) { - case LayerFilterType::BlurFilter: - return "BlurFilter"; - case LayerFilterType::DropShadowFilter: - return "DropShadowFilter"; - case LayerFilterType::InnerShadowFilter: - return "InnerShadowFilter"; - case LayerFilterType::BlendFilter: - return "BlendFilter"; - case LayerFilterType::ColorMatrixFilter: - return "ColorMatrixFilter"; - default: - return "Unknown"; - } -} - //============================================================================== // Enum string conversions //============================================================================== diff --git a/pagx/src/PAGXStringUtils.h b/pagx/src/PAGXStringUtils.h index 0bb1715fce..ce7d0fb121 100644 --- a/pagx/src/PAGXStringUtils.h +++ b/pagx/src/PAGXStringUtils.h @@ -19,27 +19,27 @@ #pragma once #include -#include "pagx/model/ColorSource.h" -#include "pagx/model/Element.h" -#include "pagx/model/Fill.h" -#include "pagx/model/ImagePattern.h" -#include "pagx/model/Layer.h" -#include "pagx/model/LayerFilter.h" -#include "pagx/model/LayerStyle.h" -#include "pagx/model/MergePath.h" -#include "pagx/model/Node.h" -#include "pagx/model/Polystar.h" -#include "pagx/model/RangeSelector.h" -#include "pagx/model/Repeater.h" -#include "pagx/model/Stroke.h" -#include "pagx/model/TextLayout.h" -#include "pagx/model/TextPath.h" -#include "pagx/model/TrimPath.h" -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Color.h" -#include "pagx/model/types/ColorSpace.h" -#include "pagx/model/types/LayerPlacement.h" -#include "pagx/model/types/TileMode.h" +#include "pagx/nodes/ColorSource.h" +#include "pagx/nodes/Element.h" +#include "pagx/nodes/Fill.h" +#include "pagx/nodes/ImagePattern.h" +#include "pagx/nodes/Layer.h" +#include "pagx/nodes/LayerFilter.h" +#include "pagx/nodes/LayerStyle.h" +#include "pagx/nodes/MergePath.h" +#include "pagx/nodes/Node.h" +#include "pagx/nodes/Polystar.h" +#include "pagx/nodes/RangeSelector.h" +#include "pagx/nodes/Repeater.h" +#include "pagx/nodes/Stroke.h" +#include "pagx/nodes/TextLayout.h" +#include "pagx/nodes/TextPath.h" +#include "pagx/nodes/TrimPath.h" +#include "pagx/nodes/BlendMode.h" +#include "pagx/nodes/Color.h" +#include "pagx/nodes/ColorSpace.h" +#include "pagx/nodes/LayerPlacement.h" +#include "pagx/nodes/TileMode.h" namespace pagx { @@ -53,16 +53,6 @@ const char* NodeTypeName(NodeType type); //============================================================================== const char* ColorSourceTypeName(ColorSourceType type); -//============================================================================== -// LayerStyle types -//============================================================================== -const char* LayerStyleTypeName(LayerStyleType type); - -//============================================================================== -// LayerFilter types -//============================================================================== -const char* LayerFilterTypeName(LayerFilterType type); - //============================================================================== // BlendMode //============================================================================== diff --git a/pagx/src/PAGXTypes.cpp b/pagx/src/PAGXTypes.cpp deleted file mode 100644 index 895840e064..0000000000 --- a/pagx/src/PAGXTypes.cpp +++ /dev/null @@ -1,182 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include -#include -#include "pagx/model/Fill.h" -#include "pagx/model/ImagePattern.h" -#include "pagx/model/Layer.h" -#include "pagx/model/MergePath.h" -#include "pagx/model/Polystar.h" -#include "pagx/model/RangeSelector.h" -#include "pagx/model/Repeater.h" -#include "pagx/model/Stroke.h" -#include "pagx/model/TextLayout.h" -#include "pagx/model/TextPath.h" -#include "pagx/model/TrimPath.h" -#include "pagx/model/types/BlendMode.h" -#include "pagx/model/types/Placement.h" -#include "pagx/model/types/TileMode.h" - -namespace pagx { - -//============================================================================== -// Enum string conversions -//============================================================================== - -#define DEFINE_ENUM_CONVERSION(EnumType, ...) \ - static const std::unordered_map EnumType##ToStringMap = {__VA_ARGS__}; \ - static const std::unordered_map StringTo##EnumType##Map = [] { \ - std::unordered_map map = {}; \ - for (const auto& pair : EnumType##ToStringMap) { \ - map[pair.second] = pair.first; \ - } \ - return map; \ - }(); \ - std::string EnumType##ToString(EnumType value) { \ - auto it = EnumType##ToStringMap.find(value); \ - return it != EnumType##ToStringMap.end() ? it->second : ""; \ - } \ - EnumType EnumType##FromString(const std::string& str) { \ - auto it = StringTo##EnumType##Map.find(str); \ - return it != StringTo##EnumType##Map.end() ? it->second \ - : EnumType##ToStringMap.begin()->first; \ - } - -DEFINE_ENUM_CONVERSION(BlendMode, - {BlendMode::Normal, "normal"}, - {BlendMode::Multiply, "multiply"}, - {BlendMode::Screen, "screen"}, - {BlendMode::Overlay, "overlay"}, - {BlendMode::Darken, "darken"}, - {BlendMode::Lighten, "lighten"}, - {BlendMode::ColorDodge, "colorDodge"}, - {BlendMode::ColorBurn, "colorBurn"}, - {BlendMode::HardLight, "hardLight"}, - {BlendMode::SoftLight, "softLight"}, - {BlendMode::Difference, "difference"}, - {BlendMode::Exclusion, "exclusion"}, - {BlendMode::Hue, "hue"}, - {BlendMode::Saturation, "saturation"}, - {BlendMode::Color, "color"}, - {BlendMode::Luminosity, "luminosity"}, - {BlendMode::PlusLighter, "plusLighter"}, - {BlendMode::PlusDarker, "plusDarker"}) - -DEFINE_ENUM_CONVERSION(LineCap, - {LineCap::Butt, "butt"}, - {LineCap::Round, "round"}, - {LineCap::Square, "square"}) - -DEFINE_ENUM_CONVERSION(LineJoin, - {LineJoin::Miter, "miter"}, - {LineJoin::Round, "round"}, - {LineJoin::Bevel, "bevel"}) - -DEFINE_ENUM_CONVERSION(FillRule, - {FillRule::Winding, "winding"}, - {FillRule::EvenOdd, "evenOdd"}) - -DEFINE_ENUM_CONVERSION(StrokeAlign, - {StrokeAlign::Center, "center"}, - {StrokeAlign::Inside, "inside"}, - {StrokeAlign::Outside, "outside"}) - -DEFINE_ENUM_CONVERSION(LayerPlacement, - {LayerPlacement::Background, "background"}, - {LayerPlacement::Foreground, "foreground"}) - -DEFINE_ENUM_CONVERSION(TileMode, - {TileMode::Clamp, "clamp"}, - {TileMode::Repeat, "repeat"}, - {TileMode::Mirror, "mirror"}, - {TileMode::Decal, "decal"}) - -DEFINE_ENUM_CONVERSION(SamplingMode, - {SamplingMode::Nearest, "nearest"}, - {SamplingMode::Linear, "linear"}, - {SamplingMode::Mipmap, "mipmap"}) - -DEFINE_ENUM_CONVERSION(MaskType, - {MaskType::Alpha, "alpha"}, - {MaskType::Luminance, "luminance"}, - {MaskType::Contour, "contour"}) - -DEFINE_ENUM_CONVERSION(PolystarType, - {PolystarType::Polygon, "polygon"}, - {PolystarType::Star, "star"}) - -DEFINE_ENUM_CONVERSION(TrimType, - {TrimType::Separate, "separate"}, - {TrimType::Continuous, "continuous"}) - -DEFINE_ENUM_CONVERSION(MergePathMode, - {MergePathMode::Append, "append"}, - {MergePathMode::Union, "union"}, - {MergePathMode::Intersect, "intersect"}, - {MergePathMode::Xor, "xor"}, - {MergePathMode::Difference, "difference"}) - -DEFINE_ENUM_CONVERSION(TextAlign, - {TextAlign::Left, "left"}, - {TextAlign::Center, "center"}, - {TextAlign::Right, "right"}, - {TextAlign::Justify, "justify"}) - -DEFINE_ENUM_CONVERSION(VerticalAlign, - {VerticalAlign::Top, "top"}, - {VerticalAlign::Center, "center"}, - {VerticalAlign::Bottom, "bottom"}) - -DEFINE_ENUM_CONVERSION(Overflow, - {Overflow::Clip, "clip"}, - {Overflow::Visible, "visible"}, - {Overflow::Ellipsis, "ellipsis"}) - -DEFINE_ENUM_CONVERSION(TextPathAlign, - {TextPathAlign::Start, "start"}, - {TextPathAlign::Center, "center"}, - {TextPathAlign::End, "end"}) - -DEFINE_ENUM_CONVERSION(SelectorUnit, - {SelectorUnit::Index, "index"}, - {SelectorUnit::Percentage, "percentage"}) - -DEFINE_ENUM_CONVERSION(SelectorShape, - {SelectorShape::Square, "square"}, - {SelectorShape::RampUp, "rampUp"}, - {SelectorShape::RampDown, "rampDown"}, - {SelectorShape::Triangle, "triangle"}, - {SelectorShape::Round, "round"}, - {SelectorShape::Smooth, "smooth"}) - -DEFINE_ENUM_CONVERSION(SelectorMode, - {SelectorMode::Add, "add"}, - {SelectorMode::Subtract, "subtract"}, - {SelectorMode::Intersect, "intersect"}, - {SelectorMode::Min, "min"}, - {SelectorMode::Max, "max"}, - {SelectorMode::Difference, "difference"}) - -DEFINE_ENUM_CONVERSION(RepeaterOrder, - {RepeaterOrder::BelowOriginal, "belowOriginal"}, - {RepeaterOrder::AboveOriginal, "aboveOriginal"}) - -#undef DEFINE_ENUM_CONVERSION - -} // namespace pagx diff --git a/pagx/src/PAGXXMLWriter.cpp b/pagx/src/PAGXXMLWriter.cpp deleted file mode 100644 index c8d0e12dc2..0000000000 --- a/pagx/src/PAGXXMLWriter.cpp +++ /dev/null @@ -1,1445 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "PAGXXMLWriter.h" -#include -#include -#include -#include "pagx/model/BackgroundBlurStyle.h" -#include "pagx/model/BlendFilter.h" -#include "pagx/model/BlurFilter.h" -#include "pagx/model/ColorMatrixFilter.h" -#include "pagx/model/Composition.h" -#include "pagx/model/ConicGradient.h" -#include "pagx/model/DiamondGradient.h" -#include "pagx/model/Document.h" -#include "pagx/model/DropShadowFilter.h" -#include "pagx/model/DropShadowStyle.h" -#include "pagx/model/Ellipse.h" -#include "pagx/model/Fill.h" -#include "pagx/model/Group.h" -#include "pagx/model/Image.h" -#include "pagx/model/ImagePattern.h" -#include "pagx/model/InnerShadowFilter.h" -#include "pagx/model/InnerShadowStyle.h" -#include "pagx/model/LinearGradient.h" -#include "pagx/model/MergePath.h" -#include "pagx/model/Path.h" -#include "pagx/model/PathDataResource.h" -#include "pagx/model/Polystar.h" -#include "pagx/model/RadialGradient.h" -#include "pagx/model/RangeSelector.h" -#include "pagx/model/Rectangle.h" -#include "pagx/model/Repeater.h" -#include "pagx/model/RoundCorner.h" -#include "pagx/model/SolidColor.h" -#include "pagx/model/Stroke.h" -#include "pagx/model/TextLayout.h" -#include "pagx/model/TextModifier.h" -#include "pagx/model/TextPath.h" -#include "pagx/model/TextSpan.h" -#include "pagx/model/TrimPath.h" - -namespace pagx { - -//============================================================================== -// XMLBuilder - XML generation helper -//============================================================================== - -class XMLBuilder { - public: - void appendDeclaration() { - buffer << "\n"; - } - - void openElement(const std::string& tag) { - writeIndent(); - buffer << "<" << tag; - tagStack.push_back(tag); - } - - void addAttribute(const std::string& name, const std::string& value) { - if (!value.empty()) { - buffer << " " << name << "=\"" << escapeXML(value) << "\""; - } - } - - void addAttribute(const std::string& name, float value, float defaultValue = 0) { - if (value != defaultValue) { - buffer << " " << name << "=\"" << formatFloat(value) << "\""; - } - } - - void addRequiredAttribute(const std::string& name, float value) { - buffer << " " << name << "=\"" << formatFloat(value) << "\""; - } - - void addRequiredAttribute(const std::string& name, const std::string& value) { - buffer << " " << name << "=\"" << escapeXML(value) << "\""; - } - - void addAttribute(const std::string& name, int value, int defaultValue = 0) { - if (value != defaultValue) { - buffer << " " << name << "=\"" << value << "\""; - } - } - - void addAttribute(const std::string& name, bool value, bool defaultValue = false) { - if (value != defaultValue) { - buffer << " " << name << "=\"" << (value ? "true" : "false") << "\""; - } - } - - void closeElementStart() { - buffer << ">\n"; - indentLevel++; - } - - void closeElementSelfClosing() { - buffer << "/>\n"; - tagStack.pop_back(); - } - - void closeElement() { - indentLevel--; - writeIndent(); - buffer << "\n"; - tagStack.pop_back(); - } - - void addTextContent(const std::string& text) { - buffer << ""; - } - - std::string str() const { - return buffer.str(); - } - - private: - std::ostringstream buffer = {}; - std::vector tagStack = {}; - int indentLevel = 0; - - void writeIndent() { - for (int i = 0; i < indentLevel; i++) { - buffer << " "; - } - } - - static std::string escapeXML(const std::string& str) { - std::string result = {}; - for (char c : str) { - switch (c) { - case '<': - result += "<"; - break; - case '>': - result += ">"; - break; - case '&': - result += "&"; - break; - case '"': - result += """; - break; - case '\'': - result += "'"; - break; - default: - result += c; - break; - } - } - return result; - } - - static std::string formatFloat(float value) { - std::ostringstream oss = {}; - oss << value; - auto str = oss.str(); - if (str.find('.') != std::string::npos) { - while (str.back() == '0') { - str.pop_back(); - } - if (str.back() == '.') { - str.pop_back(); - } - } - return str; - } -}; - -//============================================================================== -// Helper functions for converting types to strings -//============================================================================== - -static std::string pointToString(const Point& p) { - std::ostringstream oss = {}; - oss << p.x << "," << p.y; - return oss.str(); -} - -static std::string sizeToString(const Size& s) { - std::ostringstream oss = {}; - oss << s.width << "," << s.height; - return oss.str(); -} - -static std::string rectToString(const Rect& r) { - std::ostringstream oss = {}; - oss << r.x << "," << r.y << "," << r.width << "," << r.height; - return oss.str(); -} - -static std::string floatListToString(const std::vector& values) { - std::ostringstream oss = {}; - for (size_t i = 0; i < values.size(); i++) { - if (i > 0) { - oss << ","; - } - oss << values[i]; - } - return oss.str(); -} - -//============================================================================== -// ColorSource serialization helper -//============================================================================== - -static std::string colorSourceToKey(const ColorSource* node) { - if (!node) { - return ""; - } - std::ostringstream oss = {}; - switch (node->type()) { - case ColorSourceType::SolidColor: { - auto solid = static_cast(node); - oss << "SolidColor:" << solid->color.toHexString(true); - break; - } - case ColorSourceType::LinearGradient: { - auto grad = static_cast(node); - oss << "LinearGradient:" << grad->startPoint.x << "," << grad->startPoint.y << ":" - << grad->endPoint.x << "," << grad->endPoint.y << ":" << grad->matrix.toString() << ":"; - for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; - } - break; - } - case ColorSourceType::RadialGradient: { - auto grad = static_cast(node); - oss << "RadialGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->radius - << ":" << grad->matrix.toString() << ":"; - for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; - } - break; - } - case ColorSourceType::ConicGradient: { - auto grad = static_cast(node); - oss << "ConicGradient:" << grad->center.x << "," << grad->center.y << ":" << grad->startAngle - << ":" << grad->endAngle << ":" << grad->matrix.toString() << ":"; - for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; - } - break; - } - case ColorSourceType::DiamondGradient: { - auto grad = static_cast(node); - oss << "DiamondGradient:" << grad->center.x << "," << grad->center.y << ":" - << grad->halfDiagonal << ":" << grad->matrix.toString() << ":"; - for (const auto& stop : grad->colorStops) { - oss << stop.offset << "=" << stop.color.toHexString(true) << ";"; - } - break; - } - case ColorSourceType::ImagePattern: { - auto pattern = static_cast(node); - oss << "ImagePattern:" << pattern->image << ":" << static_cast(pattern->tileModeX) << ":" - << static_cast(pattern->tileModeY) << ":" << static_cast(pattern->sampling) - << ":" << pattern->matrix.toString(); - break; - } - } - return oss.str(); -} - -//============================================================================== -// ResourceContext - tracks extracted resources and reference counts -//============================================================================== - -class ResourceContext { - public: - // PathData: SVG string -> resource id - std::unordered_map pathDataMap = {}; - - // ColorSource: serialized key -> (resource id, reference count) - std::unordered_map> colorSourceMap = {}; - - // All extracted PathData resources (ordered) - std::vector> pathDataResources = {}; // id -> svg string - - // All extracted ColorSource resources (ordered) - std::vector> colorSourceResources = {};; - - int nextPathId = 1; - int nextColorId = 1; - - // First pass: collect and count all resources - void collectFromDocument(const Document& doc) { - for (const auto& layer : doc.layers) { - collectFromLayer(layer.get()); - } - for (const auto& resource : doc.resources) { - if (resource->nodeType() == NodeType::Composition) { - auto comp = static_cast(resource.get()); - for (const auto& layer : comp->layers) { - collectFromLayer(layer.get()); - } - } - } - } - - // Get or create PathData resource id - std::string getPathDataId(const std::string& svgString) { - auto it = pathDataMap.find(svgString); - if (it != pathDataMap.end()) { - return it->second; - } - std::string id = "path" + std::to_string(nextPathId++); - pathDataMap[svgString] = id; - pathDataResources.emplace_back(id, svgString); - return id; - } - - // Register ColorSource usage (for counting) - void registerColorSource(const ColorSource* node) { - if (!node) { - return; - } - std::string key = colorSourceToKey(node); - if (key.empty()) { - return; - } - auto it = colorSourceMap.find(key); - if (it != colorSourceMap.end()) { - it->second.second++; // Increment reference count - } else { - colorSourceMap[key] = {"", 1}; // Will assign id later if needed - } - } - - // Finalize: assign ids to ColorSources with multiple references - void finalizeColorSources() { - for (auto& [key, value] : colorSourceMap) { - if (value.second > 1) { - value.first = "color" + std::to_string(nextColorId++); - } - } - } - - // Check if ColorSource should be extracted to Resources - bool shouldExtractColorSource(const ColorSource* node) const { - if (!node) { - return false; - } - std::string key = colorSourceToKey(node); - auto it = colorSourceMap.find(key); - return it != colorSourceMap.end() && it->second.second > 1; - } - - // Get ColorSource resource id (empty if should inline) - std::string getColorSourceId(const ColorSource* node) const { - if (!node) { - return ""; - } - std::string key = colorSourceToKey(node); - auto it = colorSourceMap.find(key); - if (it != colorSourceMap.end() && it->second.second > 1) { - return it->second.first; - } - return ""; - } - - private: - void collectFromLayer(const Layer* layer) { - for (const auto& element : layer->contents) { - collectFromVectorElement(element.get()); - } - for (const auto& child : layer->children) { - collectFromLayer(child.get()); - } - } - - void collectFromVectorElement(const Element* element) { - switch (element->type()) { - case ElementType::Path: { - auto path = static_cast(element); - if (!path->data.isEmpty()) { - getPathDataId(path->data.toSVGString()); - } - break; - } - case ElementType::Fill: { - auto fill = static_cast(element); - if (fill->colorSource) { - registerColorSource(fill->colorSource.get()); - } - break; - } - case ElementType::Stroke: { - auto stroke = static_cast(element); - if (stroke->colorSource) { - registerColorSource(stroke->colorSource.get()); - } - break; - } - case ElementType::Group: { - auto group = static_cast(element); - for (const auto& child : group->elements) { - collectFromVectorElement(child.get()); - } - break; - } - default: - break; - } - } -}; - -//============================================================================== -// Forward declarations -//============================================================================== - -static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId); -static void writeVectorElement(XMLBuilder& xml, const Element* node, - const ResourceContext& ctx); -static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node); -static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node); -static void writeResource(XMLBuilder& xml, const Node* node, const ResourceContext& ctx); -static void writeLayer(XMLBuilder& xml, const Layer* node, const ResourceContext& ctx); - -//============================================================================== -// ColorStop and ColorSource writing -//============================================================================== - -static void writeColorStops(XMLBuilder& xml, const std::vector& stops) { - for (const auto& stop : stops) { - xml.openElement("ColorStop"); - xml.addRequiredAttribute("offset", stop.offset); - xml.addRequiredAttribute("color", stop.color.toHexString(stop.color.alpha < 1.0f)); - xml.closeElementSelfClosing(); - } -} - -static void writeColorSource(XMLBuilder& xml, const ColorSource* node, bool writeId) { - switch (node->type()) { - case ColorSourceType::SolidColor: { - auto solid = static_cast(node); - xml.openElement("SolidColor"); - if (writeId && !solid->id.empty()) { - xml.addAttribute("id", solid->id); - } - xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); - xml.closeElementSelfClosing(); - break; - } - case ColorSourceType::LinearGradient: { - auto grad = static_cast(node); - xml.openElement("LinearGradient"); - if (writeId && !grad->id.empty()) { - xml.addAttribute("id", grad->id); - } - if (grad->startPoint.x != 0 || grad->startPoint.y != 0) { - xml.addAttribute("startPoint", pointToString(grad->startPoint)); - } - xml.addRequiredAttribute("endPoint", pointToString(grad->endPoint)); - if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); - } - if (grad->colorStops.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - writeColorStops(xml, grad->colorStops); - xml.closeElement(); - } - break; - } - case ColorSourceType::RadialGradient: { - auto grad = static_cast(node); - xml.openElement("RadialGradient"); - if (writeId && !grad->id.empty()) { - xml.addAttribute("id", grad->id); - } - if (grad->center.x != 0 || grad->center.y != 0) { - xml.addAttribute("center", pointToString(grad->center)); - } - xml.addRequiredAttribute("radius", grad->radius); - if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); - } - if (grad->colorStops.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - writeColorStops(xml, grad->colorStops); - xml.closeElement(); - } - break; - } - case ColorSourceType::ConicGradient: { - auto grad = static_cast(node); - xml.openElement("ConicGradient"); - if (writeId && !grad->id.empty()) { - xml.addAttribute("id", grad->id); - } - if (grad->center.x != 0 || grad->center.y != 0) { - xml.addAttribute("center", pointToString(grad->center)); - } - xml.addAttribute("startAngle", grad->startAngle); - xml.addAttribute("endAngle", grad->endAngle, 360.0f); - if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); - } - if (grad->colorStops.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - writeColorStops(xml, grad->colorStops); - xml.closeElement(); - } - break; - } - case ColorSourceType::DiamondGradient: { - auto grad = static_cast(node); - xml.openElement("DiamondGradient"); - if (writeId && !grad->id.empty()) { - xml.addAttribute("id", grad->id); - } - if (grad->center.x != 0 || grad->center.y != 0) { - xml.addAttribute("center", pointToString(grad->center)); - } - xml.addRequiredAttribute("halfDiagonal", grad->halfDiagonal); - if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); - } - if (grad->colorStops.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - writeColorStops(xml, grad->colorStops); - xml.closeElement(); - } - break; - } - case ColorSourceType::ImagePattern: { - auto pattern = static_cast(node); - xml.openElement("ImagePattern"); - if (writeId && !pattern->id.empty()) { - xml.addAttribute("id", pattern->id); - } - xml.addAttribute("image", pattern->image); - if (pattern->tileModeX != TileMode::Clamp) { - xml.addAttribute("tileModeX", TileModeToString(pattern->tileModeX)); - } - if (pattern->tileModeY != TileMode::Clamp) { - xml.addAttribute("tileModeY", TileModeToString(pattern->tileModeY)); - } - if (pattern->sampling != SamplingMode::Linear) { - xml.addAttribute("sampling", SamplingModeToString(pattern->sampling)); - } - if (!pattern->matrix.isIdentity()) { - xml.addAttribute("matrix", pattern->matrix.toString()); - } - xml.closeElementSelfClosing(); - break; - } - } -} - -// Write ColorSource with assigned id (for Resources section) -static void writeColorSourceWithId(XMLBuilder& xml, const ColorSource* node, - const std::string& id) { - switch (node->type()) { - case ColorSourceType::SolidColor: { - auto solid = static_cast(node); - xml.openElement("SolidColor"); - xml.addAttribute("id", id); - xml.addAttribute("color", solid->color.toHexString(solid->color.alpha < 1.0f)); - xml.closeElementSelfClosing(); - break; - } - case ColorSourceType::LinearGradient: { - auto grad = static_cast(node); - xml.openElement("LinearGradient"); - xml.addAttribute("id", id); - if (grad->startPoint.x != 0 || grad->startPoint.y != 0) { - xml.addAttribute("startPoint", pointToString(grad->startPoint)); - } - xml.addRequiredAttribute("endPoint", pointToString(grad->endPoint)); - if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); - } - if (grad->colorStops.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - writeColorStops(xml, grad->colorStops); - xml.closeElement(); - } - break; - } - case ColorSourceType::RadialGradient: { - auto grad = static_cast(node); - xml.openElement("RadialGradient"); - xml.addAttribute("id", id); - if (grad->center.x != 0 || grad->center.y != 0) { - xml.addAttribute("center", pointToString(grad->center)); - } - xml.addRequiredAttribute("radius", grad->radius); - if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); - } - if (grad->colorStops.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - writeColorStops(xml, grad->colorStops); - xml.closeElement(); - } - break; - } - case ColorSourceType::ConicGradient: { - auto grad = static_cast(node); - xml.openElement("ConicGradient"); - xml.addAttribute("id", id); - if (grad->center.x != 0 || grad->center.y != 0) { - xml.addAttribute("center", pointToString(grad->center)); - } - xml.addAttribute("startAngle", grad->startAngle); - xml.addAttribute("endAngle", grad->endAngle, 360.0f); - if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); - } - if (grad->colorStops.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - writeColorStops(xml, grad->colorStops); - xml.closeElement(); - } - break; - } - case ColorSourceType::DiamondGradient: { - auto grad = static_cast(node); - xml.openElement("DiamondGradient"); - xml.addAttribute("id", id); - if (grad->center.x != 0 || grad->center.y != 0) { - xml.addAttribute("center", pointToString(grad->center)); - } - xml.addRequiredAttribute("halfDiagonal", grad->halfDiagonal); - if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); - } - if (grad->colorStops.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - writeColorStops(xml, grad->colorStops); - xml.closeElement(); - } - break; - } - case ColorSourceType::ImagePattern: { - auto pattern = static_cast(node); - xml.openElement("ImagePattern"); - xml.addAttribute("id", id); - xml.addAttribute("image", pattern->image); - if (pattern->tileModeX != TileMode::Clamp) { - xml.addAttribute("tileModeX", TileModeToString(pattern->tileModeX)); - } - if (pattern->tileModeY != TileMode::Clamp) { - xml.addAttribute("tileModeY", TileModeToString(pattern->tileModeY)); - } - if (pattern->sampling != SamplingMode::Linear) { - xml.addAttribute("sampling", SamplingModeToString(pattern->sampling)); - } - if (!pattern->matrix.isIdentity()) { - xml.addAttribute("matrix", pattern->matrix.toString()); - } - xml.closeElementSelfClosing(); - break; - } - } -} - -//============================================================================== -// VectorElement writing -//============================================================================== - -static void writeVectorElement(XMLBuilder& xml, const Element* node, - const ResourceContext& ctx) { - switch (node->type()) { - case ElementType::Rectangle: { - auto rect = static_cast(node); - xml.openElement("Rectangle"); - if (rect->center.x != 0 || rect->center.y != 0) { - xml.addAttribute("center", pointToString(rect->center)); - } - if (rect->size.width != 100 || rect->size.height != 100) { - xml.addAttribute("size", sizeToString(rect->size)); - } - xml.addAttribute("roundness", rect->roundness); - xml.addAttribute("reversed", rect->reversed); - xml.closeElementSelfClosing(); - break; - } - case ElementType::Ellipse: { - auto ellipse = static_cast(node); - xml.openElement("Ellipse"); - if (ellipse->center.x != 0 || ellipse->center.y != 0) { - xml.addAttribute("center", pointToString(ellipse->center)); - } - if (ellipse->size.width != 100 || ellipse->size.height != 100) { - xml.addAttribute("size", sizeToString(ellipse->size)); - } - xml.addAttribute("reversed", ellipse->reversed); - xml.closeElementSelfClosing(); - break; - } - case ElementType::Polystar: { - auto polystar = static_cast(node); - xml.openElement("Polystar"); - if (polystar->center.x != 0 || polystar->center.y != 0) { - xml.addAttribute("center", pointToString(polystar->center)); - } - xml.addAttribute("polystarType", PolystarTypeToString(polystar->polystarType)); - xml.addAttribute("pointCount", polystar->pointCount, 5.0f); - xml.addAttribute("outerRadius", polystar->outerRadius, 100.0f); - xml.addAttribute("innerRadius", polystar->innerRadius, 50.0f); - xml.addAttribute("rotation", polystar->rotation); - xml.addAttribute("outerRoundness", polystar->outerRoundness); - xml.addAttribute("innerRoundness", polystar->innerRoundness); - xml.addAttribute("reversed", polystar->reversed); - xml.closeElementSelfClosing(); - break; - } - case ElementType::Path: { - auto path = static_cast(node); - xml.openElement("Path"); - if (!path->data.isEmpty()) { - // Always reference PathData from Resources - std::string svgStr = path->data.toSVGString(); - auto it = ctx.pathDataMap.find(svgStr); - if (it != ctx.pathDataMap.end()) { - xml.addAttribute("data", "#" + it->second); - } else { - // Fallback to inline if not found (shouldn't happen) - xml.addAttribute("data", svgStr); - } - } - xml.addAttribute("reversed", path->reversed); - xml.closeElementSelfClosing(); - break; - } - case ElementType::TextSpan: { - auto text = static_cast(node); - xml.openElement("TextSpan"); - xml.addAttribute("x", text->x); - xml.addAttribute("y", text->y); - xml.addAttribute("font", text->font); - xml.addAttribute("fontSize", text->fontSize, 12.0f); - xml.addAttribute("fontWeight", text->fontWeight, 400); - if (text->fontStyle != "normal" && !text->fontStyle.empty()) { - xml.addAttribute("fontStyle", text->fontStyle); - } - xml.addAttribute("tracking", text->tracking); - xml.addAttribute("baselineShift", text->baselineShift); - xml.closeElementStart(); - xml.addTextContent(text->text); - xml.closeElement(); - break; - } - case ElementType::Fill: { - auto fill = static_cast(node); - xml.openElement("Fill"); - // Check if ColorSource should be referenced or inlined - if (fill->colorSource) { - std::string colorId = ctx.getColorSourceId(fill->colorSource.get()); - if (!colorId.empty()) { - // Reference the ColorSource from Resources - xml.addAttribute("color", "#" + colorId); - } - // If colorId is empty, we'll inline it below - } else { - xml.addAttribute("color", fill->color); - } - xml.addAttribute("alpha", fill->alpha, 1.0f); - if (fill->blendMode != BlendMode::Normal) { - xml.addAttribute("blendMode", BlendModeToString(fill->blendMode)); - } - if (fill->fillRule != FillRule::Winding) { - xml.addAttribute("fillRule", FillRuleToString(fill->fillRule)); - } - if (fill->placement != LayerPlacement::Background) { - xml.addAttribute("placement", LayerPlacementToString(fill->placement)); - } - // Inline ColorSource only if not extracted to Resources - if (fill->colorSource && ctx.getColorSourceId(fill->colorSource.get()).empty()) { - xml.closeElementStart(); - writeColorSource(xml, fill->colorSource.get(), false); - xml.closeElement(); - } else { - xml.closeElementSelfClosing(); - } - break; - } - case ElementType::Stroke: { - auto stroke = static_cast(node); - xml.openElement("Stroke"); - // Check if ColorSource should be referenced or inlined - if (stroke->colorSource) { - std::string colorId = ctx.getColorSourceId(stroke->colorSource.get()); - if (!colorId.empty()) { - // Reference the ColorSource from Resources - xml.addAttribute("color", "#" + colorId); - } - // If colorId is empty, we'll inline it below - } else { - xml.addAttribute("color", stroke->color); - } - xml.addAttribute("width", stroke->width, 1.0f); - xml.addAttribute("alpha", stroke->alpha, 1.0f); - if (stroke->blendMode != BlendMode::Normal) { - xml.addAttribute("blendMode", BlendModeToString(stroke->blendMode)); - } - if (stroke->cap != LineCap::Butt) { - xml.addAttribute("cap", LineCapToString(stroke->cap)); - } - if (stroke->join != LineJoin::Miter) { - xml.addAttribute("join", LineJoinToString(stroke->join)); - } - xml.addAttribute("miterLimit", stroke->miterLimit, 4.0f); - if (!stroke->dashes.empty()) { - xml.addAttribute("dashes", floatListToString(stroke->dashes)); - } - xml.addAttribute("dashOffset", stroke->dashOffset); - if (stroke->align != StrokeAlign::Center) { - xml.addAttribute("align", StrokeAlignToString(stroke->align)); - } - if (stroke->placement != LayerPlacement::Background) { - xml.addAttribute("placement", LayerPlacementToString(stroke->placement)); - } - // Inline ColorSource only if not extracted to Resources - if (stroke->colorSource && ctx.getColorSourceId(stroke->colorSource.get()).empty()) { - xml.closeElementStart(); - writeColorSource(xml, stroke->colorSource.get(), false); - xml.closeElement(); - } else { - xml.closeElementSelfClosing(); - } - break; - } - case ElementType::TrimPath: { - auto trim = static_cast(node); - xml.openElement("TrimPath"); - xml.addAttribute("start", trim->start); - xml.addAttribute("end", trim->end, 1.0f); - xml.addAttribute("offset", trim->offset); - if (trim->trimType != TrimType::Separate) { - xml.addAttribute("type", TrimTypeToString(trim->trimType)); - } - xml.closeElementSelfClosing(); - break; - } - case ElementType::RoundCorner: { - auto round = static_cast(node); - xml.openElement("RoundCorner"); - xml.addAttribute("radius", round->radius, 10.0f); - xml.closeElementSelfClosing(); - break; - } - case ElementType::MergePath: { - auto merge = static_cast(node); - xml.openElement("MergePath"); - if (merge->mode != MergePathMode::Append) { - xml.addAttribute("mode", MergePathModeToString(merge->mode)); - } - xml.closeElementSelfClosing(); - break; - } - case ElementType::TextModifier: { - auto modifier = static_cast(node); - xml.openElement("TextModifier"); - if (modifier->anchorPoint.x != 0 || modifier->anchorPoint.y != 0) { - xml.addAttribute("anchorPoint", pointToString(modifier->anchorPoint)); - } - if (modifier->position.x != 0 || modifier->position.y != 0) { - xml.addAttribute("position", pointToString(modifier->position)); - } - xml.addAttribute("rotation", modifier->rotation); - if (modifier->scale.x != 1 || modifier->scale.y != 1) { - xml.addAttribute("scale", pointToString(modifier->scale)); - } - xml.addAttribute("skew", modifier->skew); - xml.addAttribute("skewAxis", modifier->skewAxis); - xml.addAttribute("alpha", modifier->alpha, 1.0f); - xml.addAttribute("fillColor", modifier->fillColor); - xml.addAttribute("strokeColor", modifier->strokeColor); - if (modifier->strokeWidth >= 0) { - xml.addAttribute("strokeWidth", modifier->strokeWidth); - } - if (modifier->selectors.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - for (const auto& selector : modifier->selectors) { - if (selector->type() != TextSelectorType::RangeSelector) { - continue; - } - auto rangeSelector = static_cast(selector.get()); - xml.openElement("RangeSelector"); - xml.addAttribute("start", rangeSelector->start); - xml.addAttribute("end", rangeSelector->end, 1.0f); - xml.addAttribute("offset", rangeSelector->offset); - if (rangeSelector->unit != SelectorUnit::Percentage) { - xml.addAttribute("unit", SelectorUnitToString(rangeSelector->unit)); - } - if (rangeSelector->shape != SelectorShape::Square) { - xml.addAttribute("shape", SelectorShapeToString(rangeSelector->shape)); - } - xml.addAttribute("easeIn", rangeSelector->easeIn); - xml.addAttribute("easeOut", rangeSelector->easeOut); - if (rangeSelector->mode != SelectorMode::Add) { - xml.addAttribute("mode", SelectorModeToString(rangeSelector->mode)); - } - xml.addAttribute("weight", rangeSelector->weight, 1.0f); - xml.addAttribute("randomizeOrder", rangeSelector->randomizeOrder); - xml.addAttribute("randomSeed", rangeSelector->randomSeed); - xml.closeElementSelfClosing(); - } - xml.closeElement(); - } - break; - } - case ElementType::TextPath: { - auto textPath = static_cast(node); - xml.openElement("TextPath"); - xml.addAttribute("path", textPath->path); - if (textPath->pathAlign != TextPathAlign::Start) { - xml.addAttribute("align", TextPathAlignToString(textPath->pathAlign)); - } - xml.addAttribute("firstMargin", textPath->firstMargin); - xml.addAttribute("lastMargin", textPath->lastMargin); - xml.addAttribute("perpendicularToPath", textPath->perpendicularToPath, true); - xml.addAttribute("reversed", textPath->reversed); - xml.addAttribute("forceAlignment", textPath->forceAlignment); - xml.closeElementSelfClosing(); - break; - } - case ElementType::TextLayout: { - auto layout = static_cast(node); - xml.openElement("TextLayout"); - xml.addRequiredAttribute("width", layout->width); - xml.addRequiredAttribute("height", layout->height); - if (layout->textAlign != TextAlign::Left) { - xml.addAttribute("align", TextAlignToString(layout->textAlign)); - } - if (layout->verticalAlign != VerticalAlign::Top) { - xml.addAttribute("verticalAlign", VerticalAlignToString(layout->verticalAlign)); - } - xml.addAttribute("lineHeight", layout->lineHeight, 1.2f); - xml.addAttribute("indent", layout->indent); - if (layout->overflow != Overflow::Clip) { - xml.addAttribute("overflow", OverflowToString(layout->overflow)); - } - xml.closeElementSelfClosing(); - break; - } - case ElementType::Repeater: { - auto repeater = static_cast(node); - xml.openElement("Repeater"); - xml.addAttribute("copies", repeater->copies, 3.0f); - xml.addAttribute("offset", repeater->offset); - if (repeater->order != RepeaterOrder::BelowOriginal) { - xml.addAttribute("order", RepeaterOrderToString(repeater->order)); - } - if (repeater->anchorPoint.x != 0 || repeater->anchorPoint.y != 0) { - xml.addAttribute("anchorPoint", pointToString(repeater->anchorPoint)); - } - if (repeater->position.x != 100 || repeater->position.y != 100) { - xml.addAttribute("position", pointToString(repeater->position)); - } - xml.addAttribute("rotation", repeater->rotation); - if (repeater->scale.x != 1 || repeater->scale.y != 1) { - xml.addAttribute("scale", pointToString(repeater->scale)); - } - xml.addAttribute("startAlpha", repeater->startAlpha, 1.0f); - xml.addAttribute("endAlpha", repeater->endAlpha, 1.0f); - xml.closeElementSelfClosing(); - break; - } - case ElementType::Group: { - auto group = static_cast(node); - xml.openElement("Group"); - if (group->anchorPoint.x != 0 || group->anchorPoint.y != 0) { - xml.addAttribute("anchorPoint", pointToString(group->anchorPoint)); - } - if (group->position.x != 0 || group->position.y != 0) { - xml.addAttribute("position", pointToString(group->position)); - } - xml.addAttribute("rotation", group->rotation); - if (group->scale.x != 1 || group->scale.y != 1) { - xml.addAttribute("scale", pointToString(group->scale)); - } - xml.addAttribute("skew", group->skew); - xml.addAttribute("skewAxis", group->skewAxis); - xml.addAttribute("alpha", group->alpha, 1.0f); - if (group->elements.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - for (const auto& element : group->elements) { - writeVectorElement(xml, element.get(), ctx); - } - xml.closeElement(); - } - break; - } - default: - break; - } -} - -//============================================================================== -// LayerStyle writing -//============================================================================== - -static void writeLayerStyle(XMLBuilder& xml, const LayerStyle* node) { - switch (node->type()) { - case LayerStyleType::DropShadowStyle: { - auto style = static_cast(node); - xml.openElement("DropShadowStyle"); - if (style->blendMode != BlendMode::Normal) { - xml.addAttribute("blendMode", BlendModeToString(style->blendMode)); - } - xml.addAttribute("offsetX", style->offsetX); - xml.addAttribute("offsetY", style->offsetY); - xml.addAttribute("blurrinessX", style->blurrinessX); - xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); - xml.addAttribute("showBehindLayer", style->showBehindLayer, true); - xml.closeElementSelfClosing(); - break; - } - case LayerStyleType::InnerShadowStyle: { - auto style = static_cast(node); - xml.openElement("InnerShadowStyle"); - if (style->blendMode != BlendMode::Normal) { - xml.addAttribute("blendMode", BlendModeToString(style->blendMode)); - } - xml.addAttribute("offsetX", style->offsetX); - xml.addAttribute("offsetY", style->offsetY); - xml.addAttribute("blurrinessX", style->blurrinessX); - xml.addAttribute("blurrinessY", style->blurrinessY); - xml.addAttribute("color", style->color.toHexString(style->color.alpha < 1.0f)); - xml.closeElementSelfClosing(); - break; - } - case LayerStyleType::BackgroundBlurStyle: { - auto style = static_cast(node); - xml.openElement("BackgroundBlurStyle"); - if (style->blendMode != BlendMode::Normal) { - xml.addAttribute("blendMode", BlendModeToString(style->blendMode)); - } - xml.addAttribute("blurrinessX", style->blurrinessX); - xml.addAttribute("blurrinessY", style->blurrinessY); - if (style->tileMode != TileMode::Mirror) { - xml.addAttribute("tileMode", TileModeToString(style->tileMode)); - } - xml.closeElementSelfClosing(); - break; - } - default: - break; - } -} - -//============================================================================== -// LayerFilter writing -//============================================================================== - -static void writeLayerFilter(XMLBuilder& xml, const LayerFilter* node) { - switch (node->type()) { - case LayerFilterType::BlurFilter: { - auto filter = static_cast(node); - xml.openElement("BlurFilter"); - xml.addRequiredAttribute("blurrinessX", filter->blurrinessX); - xml.addRequiredAttribute("blurrinessY", filter->blurrinessY); - if (filter->tileMode != TileMode::Decal) { - xml.addAttribute("tileMode", TileModeToString(filter->tileMode)); - } - xml.closeElementSelfClosing(); - break; - } - case LayerFilterType::DropShadowFilter: { - auto filter = static_cast(node); - xml.openElement("DropShadowFilter"); - xml.addAttribute("offsetX", filter->offsetX); - xml.addAttribute("offsetY", filter->offsetY); - xml.addAttribute("blurrinessX", filter->blurrinessX); - xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); - xml.addAttribute("shadowOnly", filter->shadowOnly); - xml.closeElementSelfClosing(); - break; - } - case LayerFilterType::InnerShadowFilter: { - auto filter = static_cast(node); - xml.openElement("InnerShadowFilter"); - xml.addAttribute("offsetX", filter->offsetX); - xml.addAttribute("offsetY", filter->offsetY); - xml.addAttribute("blurrinessX", filter->blurrinessX); - xml.addAttribute("blurrinessY", filter->blurrinessY); - xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); - xml.addAttribute("shadowOnly", filter->shadowOnly); - xml.closeElementSelfClosing(); - break; - } - case LayerFilterType::BlendFilter: { - auto filter = static_cast(node); - xml.openElement("BlendFilter"); - xml.addAttribute("color", filter->color.toHexString(filter->color.alpha < 1.0f)); - if (filter->blendMode != BlendMode::Normal) { - xml.addAttribute("blendMode", BlendModeToString(filter->blendMode)); - } - xml.closeElementSelfClosing(); - break; - } - case LayerFilterType::ColorMatrixFilter: { - auto filter = static_cast(node); - xml.openElement("ColorMatrixFilter"); - std::vector values(filter->matrix.begin(), filter->matrix.end()); - xml.addAttribute("matrix", floatListToString(values)); - xml.closeElementSelfClosing(); - break; - } - default: - break; - } -} - -//============================================================================== -// Resource writing -//============================================================================== - -static void writeResource(XMLBuilder& xml, const Node* node, const ResourceContext& ctx) { - switch (node->nodeType()) { - case NodeType::Image: { - auto image = static_cast(node); - xml.openElement("Image"); - xml.addAttribute("id", image->id); - xml.addAttribute("source", image->source); - xml.closeElementSelfClosing(); - break; - } - case NodeType::PathData: { - auto pathData = static_cast(node); - xml.openElement("PathData"); - xml.addAttribute("id", pathData->id); - xml.addAttribute("data", pathData->data); - xml.closeElementSelfClosing(); - break; - } - case NodeType::Composition: { - auto comp = static_cast(node); - xml.openElement("Composition"); - xml.addAttribute("id", comp->id); - xml.addRequiredAttribute("width", static_cast(comp->width)); - xml.addRequiredAttribute("height", static_cast(comp->height)); - if (comp->layers.empty()) { - xml.closeElementSelfClosing(); - } else { - xml.closeElementStart(); - for (const auto& layer : comp->layers) { - writeLayer(xml, layer.get(), ctx); - } - xml.closeElement(); - } - break; - } - default: - break; - } -} - -//============================================================================== -// Layer writing -//============================================================================== - -static void writeLayer(XMLBuilder& xml, const Layer* node, const ResourceContext& ctx) { - xml.openElement("Layer"); - if (!node->id.empty()) { - xml.addAttribute("id", node->id); - } - xml.addAttribute("name", node->name); - xml.addAttribute("visible", node->visible, true); - xml.addAttribute("alpha", node->alpha, 1.0f); - if (node->blendMode != BlendMode::Normal) { - xml.addAttribute("blendMode", BlendModeToString(node->blendMode)); - } - xml.addAttribute("x", node->x); - xml.addAttribute("y", node->y); - if (!node->matrix.isIdentity()) { - xml.addAttribute("matrix", node->matrix.toString()); - } - if (!node->matrix3D.empty()) { - xml.addAttribute("matrix3D", floatListToString(node->matrix3D)); - } - xml.addAttribute("preserve3D", node->preserve3D); - xml.addAttribute("antiAlias", node->antiAlias, true); - xml.addAttribute("groupOpacity", node->groupOpacity); - xml.addAttribute("passThroughBackground", node->passThroughBackground, true); - xml.addAttribute("excludeChildEffectsInLayerStyle", node->excludeChildEffectsInLayerStyle); - if (node->hasScrollRect) { - xml.addAttribute("scrollRect", rectToString(node->scrollRect)); - } - xml.addAttribute("mask", node->mask); - if (node->maskType != MaskType::Alpha) { - xml.addAttribute("maskType", MaskTypeToString(node->maskType)); - } - xml.addAttribute("composition", node->composition); - - // Write custom data as data-* attributes. - for (const auto& [key, value] : node->customData) { - xml.addAttribute("data-" + key, value); - } - - bool hasChildren = !node->contents.empty() || !node->styles.empty() || !node->filters.empty() || - !node->children.empty(); - if (!hasChildren) { - xml.closeElementSelfClosing(); - return; - } - - xml.closeElementStart(); - - // Write VectorElement (contents) directly without container node. - for (const auto& element : node->contents) { - writeVectorElement(xml, element.get(), ctx); - } - - // Write LayerStyle (styles) directly without container node. - for (const auto& style : node->styles) { - writeLayerStyle(xml, style.get()); - } - - // Write LayerFilter (filters) directly without container node. - for (const auto& filter : node->filters) { - writeLayerFilter(xml, filter.get()); - } - - // Write child Layers. - for (const auto& child : node->children) { - writeLayer(xml, child.get(), ctx); - } - - xml.closeElement(); -} - -//============================================================================== -// Main Write function -//============================================================================== - -std::string PAGXXMLWriter::Write(const Document& doc) { - // First pass: collect resources and count references - ResourceContext ctx = {}; - ctx.collectFromDocument(doc); - ctx.finalizeColorSources(); - - // Build ColorSource resources list (only those with multiple references) - // We need to store pointers to actual ColorSource nodes for writing - std::unordered_map colorSourceByKey = {}; - for (const auto& layer : doc.layers) { - std::function collectColorSources = [&](const Layer* layer) { - for (const auto& element : layer->contents) { - if (element->type() == ElementType::Fill) { - auto fill = static_cast(element.get()); - if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { - std::string key = colorSourceToKey(fill->colorSource.get()); - if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->colorSource.get(); - } - } - } else if (element->type() == ElementType::Stroke) { - auto stroke = static_cast(element.get()); - if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { - std::string key = colorSourceToKey(stroke->colorSource.get()); - if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->colorSource.get(); - } - } - } else if (element->type() == ElementType::Group) { - auto group = static_cast(element.get()); - std::function collectFromGroup = [&](const Group* g) { - for (const auto& child : g->elements) { - if (child->type() == ElementType::Fill) { - auto fill = static_cast(child.get()); - if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { - std::string key = colorSourceToKey(fill->colorSource.get()); - if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->colorSource.get(); - } - } - } else if (child->type() == ElementType::Stroke) { - auto stroke = static_cast(child.get()); - if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { - std::string key = colorSourceToKey(stroke->colorSource.get()); - if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->colorSource.get(); - } - } - } else if (child->type() == ElementType::Group) { - collectFromGroup(static_cast(child.get())); - } - } - }; - collectFromGroup(group); - } - } - for (const auto& child : layer->children) { - collectColorSources(child.get()); - } - }; - collectColorSources(layer.get()); - } - - // Also collect from Compositions - for (const auto& resource : doc.resources) { - if (resource->nodeType() == NodeType::Composition) { - auto comp = static_cast(resource.get()); - std::function collectColorSources = [&](const Layer* layer) { - for (const auto& element : layer->contents) { - if (element->type() == ElementType::Fill) { - auto fill = static_cast(element.get()); - if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { - std::string key = colorSourceToKey(fill->colorSource.get()); - if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->colorSource.get(); - } - } - } else if (element->type() == ElementType::Stroke) { - auto stroke = static_cast(element.get()); - if (stroke->colorSource && ctx.shouldExtractColorSource(stroke->colorSource.get())) { - std::string key = colorSourceToKey(stroke->colorSource.get()); - if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->colorSource.get(); - } - } - } else if (element->type() == ElementType::Group) { - auto group = static_cast(element.get()); - std::function collectFromGroup = [&](const Group* g) { - for (const auto& child : g->elements) { - if (child->type() == ElementType::Fill) { - auto fill = static_cast(child.get()); - if (fill->colorSource && ctx.shouldExtractColorSource(fill->colorSource.get())) { - std::string key = colorSourceToKey(fill->colorSource.get()); - if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = fill->colorSource.get(); - } - } - } else if (child->type() == ElementType::Stroke) { - auto stroke = static_cast(child.get()); - if (stroke->colorSource && - ctx.shouldExtractColorSource(stroke->colorSource.get())) { - std::string key = colorSourceToKey(stroke->colorSource.get()); - if (colorSourceByKey.find(key) == colorSourceByKey.end()) { - colorSourceByKey[key] = stroke->colorSource.get(); - } - } - } else if (child->type() == ElementType::Group) { - collectFromGroup(static_cast(child.get())); - } - } - }; - collectFromGroup(group); - } - } - for (const auto& child : layer->children) { - collectColorSources(child.get()); - } - }; - for (const auto& layer : comp->layers) { - collectColorSources(layer.get()); - } - } - } - - // Second pass: generate XML - XMLBuilder xml = {}; - xml.appendDeclaration(); - - xml.openElement("pagx"); - xml.addAttribute("version", doc.version); - xml.addAttribute("width", doc.width); - xml.addAttribute("height", doc.height); - xml.closeElementStart(); - - // Write Layers first (for better readability) - for (const auto& layer : doc.layers) { - writeLayer(xml, layer.get(), ctx); - } - - // Write Resources section at the end - bool hasResources = !ctx.pathDataResources.empty() || !colorSourceByKey.empty() || - !doc.resources.empty(); - if (hasResources) { - xml.openElement("Resources"); - xml.closeElementStart(); - - // Write PathData resources - for (const auto& [id, svgString] : ctx.pathDataResources) { - xml.openElement("PathData"); - xml.addAttribute("id", id); - xml.addAttribute("data", svgString); - xml.closeElementSelfClosing(); - } - - // Write ColorSource resources (those with multiple references) - for (const auto& [key, node] : colorSourceByKey) { - std::string id = ctx.getColorSourceId(node); - if (!id.empty()) { - writeColorSourceWithId(xml, node, id); - } - } - - // Write original resources (Image, Composition, etc.) - for (const auto& resource : doc.resources) { - writeResource(xml, resource.get(), ctx); - } - - xml.closeElement(); - } - - xml.closeElement(); - - return xml.str(); -} - -} // namespace pagx diff --git a/pagx/src/PathData.cpp b/pagx/src/PathData.cpp index abd206f666..a32a026b84 100644 --- a/pagx/src/PathData.cpp +++ b/pagx/src/PathData.cpp @@ -16,7 +16,7 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "pagx/model/PathData.h" +#include "pagx/nodes/PathData.h" #include #include #include @@ -155,7 +155,7 @@ void PathData::transform(const Matrix& matrix) { _boundsDirty = true; } -Rect PathData::getBounds() const { +Rect PathData::getBounds() { if (!_boundsDirty) { return _cachedBounds; } diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index fe204dd183..dad5eebf13 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -22,14 +22,14 @@ #include #include #include "PAGXStringUtils.h" -#include "pagx/model/Document.h" -#include "pagx/model/SolidColor.h" +#include "pagx/PAGXDocument.h" +#include "pagx/nodes/SolidColor.h" #include "SVGParserInternal.h" #include "xml/XMLDOM.h" namespace pagx { -std::shared_ptr SVGImporter::Parse(const std::string& filePath, +std::shared_ptr SVGImporter::Parse(const std::string& filePath, const Options& options) { SVGParserImpl parser(options); auto doc = parser.parseFile(filePath); @@ -42,13 +42,13 @@ std::shared_ptr SVGImporter::Parse(const std::string& filePath, return doc; } -std::shared_ptr SVGImporter::Parse(const uint8_t* data, size_t length, +std::shared_ptr SVGImporter::Parse(const uint8_t* data, size_t length, const Options& options) { SVGParserImpl parser(options); return parser.parse(data, length); } -std::shared_ptr SVGImporter::ParseString(const std::string& svgContent, +std::shared_ptr SVGImporter::ParseString(const std::string& svgContent, const Options& options) { return Parse(reinterpret_cast(svgContent.data()), svgContent.size(), options); } @@ -58,7 +58,7 @@ std::shared_ptr SVGImporter::ParseString(const std::string& svgContent SVGParserImpl::SVGParserImpl(const SVGImporter::Options& options) : _options(options) { } -std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { +std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { if (!data || length == 0) { return nullptr; } @@ -71,7 +71,7 @@ std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t lengt return parseDOM(dom); } -std::shared_ptr SVGParserImpl::parseFile(const std::string& filePath) { +std::shared_ptr SVGParserImpl::parseFile(const std::string& filePath) { auto dom = DOM::MakeFromFile(filePath); if (!dom) { return nullptr; @@ -87,7 +87,7 @@ std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, return found ? value : defaultValue; } -std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& dom) { +std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& dom) { auto root = dom->getRootNode(); if (!root || root->name != "svg") { return nullptr; @@ -116,7 +116,7 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& do return nullptr; } - _document = Document::Make(width, height); + _document = PAGXDocument::Make(width, height); // Collect all IDs from the SVG to avoid conflicts when generating new IDs. collectAllIds(root); diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 04ea460519..1b4b5460aa 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -23,21 +23,21 @@ #include #include #include -#include "pagx/model/BlurFilter.h" -#include "pagx/model/Document.h" -#include "pagx/model/Ellipse.h" -#include "pagx/model/Fill.h" -#include "pagx/model/Group.h" -#include "pagx/model/Image.h" -#include "pagx/model/ImagePattern.h" -#include "pagx/model/LinearGradient.h" -#include "pagx/model/Path.h" -#include "pagx/model/PathData.h" -#include "pagx/model/RadialGradient.h" -#include "pagx/model/Rectangle.h" -#include "pagx/model/Stroke.h" -#include "pagx/model/TextLayout.h" -#include "pagx/model/TextSpan.h" +#include "pagx/nodes/BlurFilter.h" +#include "pagx/PAGXDocument.h" +#include "pagx/nodes/Ellipse.h" +#include "pagx/nodes/Fill.h" +#include "pagx/nodes/Group.h" +#include "pagx/nodes/Image.h" +#include "pagx/nodes/ImagePattern.h" +#include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/Path.h" +#include "pagx/nodes/PathData.h" +#include "pagx/nodes/RadialGradient.h" +#include "pagx/nodes/Rectangle.h" +#include "pagx/nodes/Stroke.h" +#include "pagx/nodes/TextLayout.h" +#include "pagx/nodes/TextSpan.h" #include "pagx/SVGImporter.h" #include "xml/XMLDOM.h" @@ -61,11 +61,11 @@ class SVGParserImpl { public: explicit SVGParserImpl(const SVGImporter::Options& options); - std::shared_ptr parse(const uint8_t* data, size_t length); - std::shared_ptr parseFile(const std::string& filePath); + std::shared_ptr parse(const uint8_t* data, size_t length); + std::shared_ptr parseFile(const std::string& filePath); private: - std::shared_ptr parseDOM(const std::shared_ptr& dom); + std::shared_ptr parseDOM(const std::shared_ptr& dom); void parseDefs(const std::shared_ptr& defsNode); @@ -134,21 +134,44 @@ class SVGParserImpl { // Collect all IDs from the SVG document to avoid conflicts when generating new IDs. void collectAllIds(const std::shared_ptr& node); + + // First pass: count references to gradients/patterns in defs. + void countColorSourceReferences(const std::shared_ptr& root); + void countColorSourceReferencesInElement(const std::shared_ptr& element); // Generate a unique ID that doesn't conflict with existing SVG IDs. std::string generateUniqueId(const std::string& prefix); + + // Generate a unique ColorSource ID for resources. + std::string generateColorSourceId(); // Parse data-* attributes from element and add to layer's customData. void parseCustomData(const std::shared_ptr& element, Layer* layer); + + // Get or create ColorSource for a gradient/pattern reference. + // If the reference is used multiple times, the ColorSource is added to resources. + // Returns the ColorSource (either new inline instance or reference to resource). + std::unique_ptr getColorSourceForRef(const std::string& refId, + const Rect& shapeBounds); SVGImporter::Options _options = {}; - std::shared_ptr _document = nullptr; + std::shared_ptr _document = nullptr; std::unordered_map> _defs = {}; std::vector> _maskLayers = {}; std::unordered_map _imageSourceToId = {}; // Maps image source to resource ID. std::unordered_set _existingIds = {}; // All IDs found in SVG to avoid conflicts. + + // ColorSource reference counting for gradients and patterns. + // Key is the SVG def id (e.g., "gradient1"), value is the number of times it's referenced. + std::unordered_map _colorSourceRefCount = {}; + // Maps SVG def id to the PAGX resource id (only for those with refCount > 1). + std::unordered_map _colorSourceIdMap = {}; + // Store the converted ColorSource by SVG def id (for reuse when refCount > 1). + std::unordered_map _colorSourceCache = {}; + int _nextImageId = 0; int _nextGeneratedId = 0; // Counter for generating unique IDs. + int _nextColorSourceId = 0; // Counter for generating ColorSource IDs. float _viewBoxWidth = 0; float _viewBoxHeight = 0; }; diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 72d281d8ac..38ec4b938f 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -20,36 +20,37 @@ #include #include #include -#include "pagx/model/BlurFilter.h" -#include "pagx/model/types/ColorSpace.h" +#include "pagx/PAGXImporter.h" +#include "pagx/nodes/BlurFilter.h" +#include "pagx/nodes/ColorSpace.h" #include "tgfx/core/ColorSpace.h" -#include "pagx/model/Composition.h" -#include "pagx/model/ConicGradient.h" -#include "pagx/model/DiamondGradient.h" -#include "pagx/model/DropShadowFilter.h" -#include "pagx/model/DropShadowStyle.h" -#include "pagx/model/Ellipse.h" -#include "pagx/model/Fill.h" -#include "pagx/model/Group.h" -#include "pagx/model/Image.h" -#include "pagx/model/ImagePattern.h" -#include "pagx/model/InnerShadowFilter.h" -#include "pagx/model/InnerShadowStyle.h" -#include "pagx/model/Layer.h" -#include "pagx/model/LinearGradient.h" -#include "pagx/model/MergePath.h" -#include "pagx/model/Node.h" -#include "pagx/model/Path.h" -#include "pagx/model/Polystar.h" -#include "pagx/model/RadialGradient.h" -#include "pagx/model/Rectangle.h" -#include "pagx/model/Repeater.h" -#include "pagx/model/RoundCorner.h" -#include "pagx/model/SolidColor.h" -#include "pagx/model/Stroke.h" -#include "pagx/model/TextLayout.h" -#include "pagx/model/TextSpan.h" -#include "pagx/model/TrimPath.h" +#include "pagx/nodes/Composition.h" +#include "pagx/nodes/ConicGradient.h" +#include "pagx/nodes/DiamondGradient.h" +#include "pagx/nodes/DropShadowFilter.h" +#include "pagx/nodes/DropShadowStyle.h" +#include "pagx/nodes/Ellipse.h" +#include "pagx/nodes/Fill.h" +#include "pagx/nodes/Group.h" +#include "pagx/nodes/Image.h" +#include "pagx/nodes/ImagePattern.h" +#include "pagx/nodes/InnerShadowFilter.h" +#include "pagx/nodes/InnerShadowStyle.h" +#include "pagx/nodes/Layer.h" +#include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/MergePath.h" +#include "pagx/nodes/Node.h" +#include "pagx/nodes/Path.h" +#include "pagx/nodes/Polystar.h" +#include "pagx/nodes/RadialGradient.h" +#include "pagx/nodes/Rectangle.h" +#include "pagx/nodes/Repeater.h" +#include "pagx/nodes/RoundCorner.h" +#include "pagx/nodes/SolidColor.h" +#include "pagx/nodes/Stroke.h" +#include "pagx/nodes/TextLayout.h" +#include "pagx/nodes/TextSpan.h" +#include "pagx/nodes/TrimPath.h" #include "pagx/SVGImporter.h" #include "tgfx/core/Data.h" #include "tgfx/core/Font.h" @@ -259,7 +260,7 @@ class LayerBuilderImpl { } } - PAGXContent build(const Document& document) { + PAGXContent build(const PAGXDocument& document) { // Cache resources for later lookup. _resources = &document.resources; @@ -832,13 +833,13 @@ class LayerBuilderImpl { return nullptr; } - switch (node->type()) { - case LayerStyleType::DropShadowStyle: { + switch (node->nodeType()) { + case NodeType::DropShadowStyle: { auto style = static_cast(node); return tgfx::DropShadowStyle::Make(style->offsetX, style->offsetY, style->blurrinessX, style->blurrinessY, ToTGFX(style->color)); } - case LayerStyleType::InnerShadowStyle: { + case NodeType::InnerShadowStyle: { auto style = static_cast(node); return tgfx::InnerShadowStyle::Make(style->offsetX, style->offsetY, style->blurrinessX, style->blurrinessY, ToTGFX(style->color)); @@ -853,12 +854,12 @@ class LayerBuilderImpl { return nullptr; } - switch (node->type()) { - case LayerFilterType::BlurFilter: { + switch (node->nodeType()) { + case NodeType::BlurFilter: { auto filter = static_cast(node); return tgfx::BlurFilter::Make(filter->blurrinessX, filter->blurrinessY); } - case LayerFilterType::DropShadowFilter: { + case NodeType::DropShadowFilter: { auto filter = static_cast(node); return tgfx::DropShadowFilter::Make(filter->offsetX, filter->offsetY, filter->blurrinessX, filter->blurrinessY, ToTGFX(filter->color)); @@ -878,13 +879,13 @@ class LayerBuilderImpl { // Public API implementation -PAGXContent LayerBuilder::Build(const Document& document, const Options& options) { +PAGXContent LayerBuilder::Build(const PAGXDocument& document, const Options& options) { LayerBuilderImpl builder(options); return builder.build(document); } PAGXContent LayerBuilder::FromFile(const std::string& filePath, const Options& options) { - auto document = Document::FromFile(filePath); + auto document = PAGXImporter::FromFile(filePath); if (!document) { return {}; } @@ -901,7 +902,7 @@ PAGXContent LayerBuilder::FromFile(const std::string& filePath, const Options& o } PAGXContent LayerBuilder::FromData(const uint8_t* data, size_t length, const Options& options) { - auto document = Document::FromXML(data, length); + auto document = PAGXImporter::FromXML(data, length); if (!document) { return {}; } diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index fe21780e9a..3815906108 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -18,19 +18,21 @@ #include #include "pagx/LayerBuilder.h" -#include "pagx/model/Document.h" -#include "pagx/model/DropShadowStyle.h" -#include "pagx/model/BlurFilter.h" -#include "pagx/model/Ellipse.h" -#include "pagx/model/Fill.h" -#include "pagx/model/Group.h" -#include "pagx/model/LinearGradient.h" -#include "pagx/model/Path.h" -#include "pagx/model/RadialGradient.h" -#include "pagx/model/Rectangle.h" -#include "pagx/model/SolidColor.h" +#include "pagx/PAGXDocument.h" +#include "pagx/PAGXExporter.h" +#include "pagx/PAGXImporter.h" +#include "pagx/nodes/DropShadowStyle.h" +#include "pagx/nodes/BlurFilter.h" +#include "pagx/nodes/Ellipse.h" +#include "pagx/nodes/Fill.h" +#include "pagx/nodes/Group.h" +#include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/Path.h" +#include "pagx/nodes/RadialGradient.h" +#include "pagx/nodes/Rectangle.h" +#include "pagx/nodes/SolidColor.h" #include "pagx/SVGImporter.h" -#include "pagx/model/PathData.h" +#include "pagx/nodes/PathData.h" #include "tgfx/core/Data.h" #include "tgfx/core/Stream.h" #include "tgfx/core/Surface.h" @@ -142,7 +144,7 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { pagx::SVGImporter::Options parserOptions; auto doc = pagx::SVGImporter::Parse(svgPath, parserOptions); if (doc) { - std::string xml = doc->toXML(); + std::string xml = pagx::PAGXExporter::ToXML(*doc); auto pagxData = Data::MakeWithCopy(xml.data(), xml.size()); SaveFile(pagxData, "PAGXTest/" + baseName + ".pagx"); } @@ -236,7 +238,7 @@ PAG_TEST(PAGXTest, PAGXNodeBasic) { * Test case: PAGXDocument creation and XML export */ PAG_TEST(PAGXTest, PAGXDocumentXMLExport) { - auto doc = pagx::Document::Make(400, 300); + auto doc = pagx::PAGXDocument::Make(400, 300); ASSERT_TRUE(doc != nullptr); EXPECT_EQ(doc->width, 400.0f); EXPECT_EQ(doc->height, 300.0f); @@ -267,7 +269,7 @@ PAG_TEST(PAGXTest, PAGXDocumentXMLExport) { doc->layers.push_back(std::move(layer)); // Export to XML - std::string xml = doc->toXML(); + std::string xml = pagx::PAGXExporter::ToXML(*doc); EXPECT_FALSE(xml.empty()); EXPECT_NE(xml.find("(); @@ -305,11 +307,11 @@ PAG_TEST(PAGXTest, PAGXDocumentRoundTrip) { doc1->layers.push_back(std::move(layer)); // Export to XML - std::string xml = doc1->toXML(); + std::string xml = pagx::PAGXExporter::ToXML(*doc1); EXPECT_FALSE(xml.empty()); // Parse the XML back - auto doc2 = pagx::Document::FromXML(xml); + auto doc2 = pagx::PAGXImporter::FromXML(xml); ASSERT_TRUE(doc2 != nullptr); // Verify the dimensions @@ -439,8 +441,8 @@ PAG_TEST(PAGXTest, LayerStylesFilters) { EXPECT_EQ(layer->styles.size(), 1u); EXPECT_EQ(layer->filters.size(), 1u); - EXPECT_EQ(layer->styles[0]->type(), pagx::LayerStyleType::DropShadowStyle); - EXPECT_EQ(layer->filters[0]->type(), pagx::LayerFilterType::BlurFilter); + EXPECT_EQ(layer->styles[0]->nodeType(), pagx::NodeType::DropShadowStyle); + EXPECT_EQ(layer->filters[0]->nodeType(), pagx::NodeType::BlurFilter); } } // namespace pag From 711bf4e33dfcee2dbfcd0f83c1838e372d9e75e0 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 23:13:31 +0800 Subject: [PATCH 113/678] Refactor ImagePattern sampling: replace SamplingMode with FilterMode and MipmapMode to align with TGFX. --- pagx/docs/pagx_spec.md | 15 ++++-- pagx/include/pagx/nodes/BlendMode.h | 60 +++++++++++++++++++++++- pagx/include/pagx/nodes/LayerPlacement.h | 8 +++- pagx/include/pagx/nodes/PathData.h | 12 +---- pagx/include/pagx/nodes/RangeSelector.h | 48 +++++++++++++++++-- pagx/include/pagx/nodes/TileMode.h | 14 +++++- pagx/src/PAGXExporter.cpp | 10 +++- pagx/src/PAGXImporter.cpp | 4 +- pagx/src/PAGXStringUtils.cpp | 12 +++-- pagx/src/PAGXStringUtils.h | 10 ++-- pagx/src/svg/SVGImporter.cpp | 4 +- 11 files changed, 165 insertions(+), 32 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 43f5552ba2..453ec0a35e 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -428,12 +428,16 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | `image` | idref | (必填) | 图片引用 "@id" | | `tileModeX` | TileMode | clamp | X 方向平铺模式 | | `tileModeY` | TileMode | clamp | Y 方向平铺模式 | -| `sampling` | SamplingMode | linear | 采样模式 | +| `minFilterMode` | FilterMode | linear | 缩小滤镜模式 | +| `magFilterMode` | FilterMode | linear | 放大滤镜模式 | +| `mipmapMode` | MipmapMode | linear | 多级渐远纹理模式 | | `matrix` | string | 单位矩阵 | 变换矩阵 | **TileMode(平铺模式)**:`clamp`(钳制)、`repeat`(重复)、`mirror`(镜像)、`decal`(贴花) -**SamplingMode(采样模式)**:`nearest`(最近邻)、`linear`(双线性)、`mipmap`(多级渐远) +**FilterMode(滤镜模式)**:`nearest`(最近邻)、`linear`(双线性插值) + +**MipmapMode(多级渐远模式)**:`none`(禁用)、`nearest`(最近级别)、`linear`(线性插值) ##### 颜色源坐标系统 @@ -2097,7 +2101,9 @@ Layer / Group | `image` | idref | (必填) | | `tileModeX` | TileMode | clamp | | `tileModeY` | TileMode | clamp | -| `sampling` | SamplingMode | linear | +| `minFilterMode` | FilterMode | linear | +| `magFilterMode` | FilterMode | linear | +| `mipmapMode` | MipmapMode | linear | | `matrix` | string | 单位矩阵 | ### C.3 图层节点 @@ -2409,7 +2415,8 @@ Layer / Group | **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter`, `plusDarker` | | **MaskType** | `alpha`, `luminance`, `contour` | | **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | -| **SamplingMode** | `nearest`, `linear`, `mipmap` | +| **FilterMode** | `nearest`, `linear` | +| **MipmapMode** | `none`, `nearest`, `linear` | #### 颜色相关 diff --git a/pagx/include/pagx/nodes/BlendMode.h b/pagx/include/pagx/nodes/BlendMode.h index dc900be275..b0bf542fc3 100644 --- a/pagx/include/pagx/nodes/BlendMode.h +++ b/pagx/include/pagx/nodes/BlendMode.h @@ -21,26 +21,84 @@ namespace pagx { /** - * Blend modes for compositing. + * Blend modes for compositing layers and colors. */ enum class BlendMode { + /** + * Normal blending, the source color replaces the destination. + */ Normal, + /** + * Multiplies the source and destination colors. + */ Multiply, + /** + * Inverts, multiplies, and inverts again, resulting in a lighter image. + */ Screen, + /** + * Combines Multiply and Screen modes depending on the destination color. + */ Overlay, + /** + * Retains the darker of the source and destination colors. + */ Darken, + /** + * Retains the lighter of the source and destination colors. + */ Lighten, + /** + * Brightens the destination color to reflect the source color. + */ ColorDodge, + /** + * Darkens the destination color to reflect the source color. + */ ColorBurn, + /** + * Combines Multiply and Screen modes depending on the source color. + */ HardLight, + /** + * A softer version of Hard Light, producing a more subtle effect. + */ SoftLight, + /** + * Subtracts the darker color from the lighter color. + */ Difference, + /** + * Similar to Difference but with lower contrast. + */ Exclusion, + /** + * Creates a result color with the hue of the source and the saturation and luminosity of the + * destination. + */ Hue, + /** + * Creates a result color with the saturation of the source and the hue and luminosity of the + * destination. + */ Saturation, + /** + * Creates a result color with the hue and saturation of the source and the luminosity of the + * destination. + */ Color, + /** + * Creates a result color with the luminosity of the source and the hue and saturation of the + * destination. + */ Luminosity, + /** + * Adds the source and destination colors, clamping to white. + */ PlusLighter, + /** + * Adds the source and destination colors, clamping to black. + */ PlusDarker }; diff --git a/pagx/include/pagx/nodes/LayerPlacement.h b/pagx/include/pagx/nodes/LayerPlacement.h index 46f8e9e839..6d804f6e20 100644 --- a/pagx/include/pagx/nodes/LayerPlacement.h +++ b/pagx/include/pagx/nodes/LayerPlacement.h @@ -21,10 +21,16 @@ namespace pagx { /** - * Placement of fill/stroke relative to child layers. + * Placement of fill or stroke relative to other painters. */ enum class LayerPlacement { + /** + * Place behind other painters (rendered first). + */ Background, + /** + * Place in front of other painters (rendered last). + */ Foreground }; diff --git a/pagx/include/pagx/nodes/PathData.h b/pagx/include/pagx/nodes/PathData.h index 3692ee2e29..40253bb6bc 100644 --- a/pagx/include/pagx/nodes/PathData.h +++ b/pagx/include/pagx/nodes/PathData.h @@ -29,7 +29,7 @@ namespace pagx { /** * Path command types. */ -enum class PathVerb : uint8_t { +enum class PathVerb { Move, // 1 point: destination Line, // 1 point: end point Quad, // 2 points: control point, end point @@ -46,11 +46,6 @@ class PathData : public Node { public: PathData() = default; - /** - * Creates a PathData from an SVG path data string (d attribute). - */ - static PathData FromSVGString(const std::string& d); - /** * Starts a new contour at the specified point. */ @@ -137,11 +132,6 @@ class PathData : public Node { } } - /** - * Converts the path to an SVG path data string. - */ - std::string toSVGString() const; - /** * Returns the bounding rectangle of the path. */ diff --git a/pagx/include/pagx/nodes/RangeSelector.h b/pagx/include/pagx/nodes/RangeSelector.h index b25cdcb6b3..f54ce9fada 100644 --- a/pagx/include/pagx/nodes/RangeSelector.h +++ b/pagx/include/pagx/nodes/RangeSelector.h @@ -23,34 +23,76 @@ namespace pagx { /** - * Range selector unit. + * The unit type for range selector values. */ enum class SelectorUnit { + /** + * Values are specified as character indices. + */ Index, + /** + * Values are specified as percentages of the total text length. + */ Percentage }; /** - * Range selector shape. + * The shape of the selection falloff curve. */ enum class SelectorShape { + /** + * A square falloff with no transition. + */ Square, + /** + * A ramp that increases from start to end. + */ RampUp, + /** + * A ramp that decreases from start to end. + */ RampDown, + /** + * A triangle shape that peaks in the middle. + */ Triangle, + /** + * A rounded falloff with smooth edges. + */ Round, + /** + * A smooth S-curve falloff. + */ Smooth }; /** - * Range selector combination mode. + * The mode for combining multiple selectors. */ enum class SelectorMode { + /** + * Add selector values together. + */ Add, + /** + * Subtract selector values. + */ Subtract, + /** + * Use the intersection of selector ranges. + */ Intersect, + /** + * Use the minimum of selector values. + */ Min, + /** + * Use the maximum of selector values. + */ Max, + /** + * Use the absolute difference of selector values. + */ Difference }; diff --git a/pagx/include/pagx/nodes/TileMode.h b/pagx/include/pagx/nodes/TileMode.h index 502ec64349..46a9d483ac 100644 --- a/pagx/include/pagx/nodes/TileMode.h +++ b/pagx/include/pagx/nodes/TileMode.h @@ -21,12 +21,24 @@ namespace pagx { /** - * Tile modes for patterns and gradients. + * Tile modes for patterns, gradients, and filters. */ enum class TileMode { + /** + * Clamp to the edge color, extending the edge pixels outward. + */ Clamp, + /** + * Repeat the pattern by tiling it. + */ Repeat, + /** + * Repeat the pattern by mirroring it at the edges. + */ Mirror, + /** + * Render transparent pixels outside the bounds. + */ Decal }; diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index 55db9ba4af..c7c7beb775 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -341,8 +341,14 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { if (pattern->tileModeY != TileMode::Clamp) { xml.addAttribute("tileModeY", TileModeToString(pattern->tileModeY)); } - if (pattern->sampling != SamplingMode::Linear) { - xml.addAttribute("sampling", SamplingModeToString(pattern->sampling)); + if (pattern->minFilterMode != FilterMode::Linear) { + xml.addAttribute("minFilterMode", FilterModeToString(pattern->minFilterMode)); + } + if (pattern->magFilterMode != FilterMode::Linear) { + xml.addAttribute("magFilterMode", FilterModeToString(pattern->magFilterMode)); + } + if (pattern->mipmapMode != MipmapMode::Linear) { + xml.addAttribute("mipmapMode", MipmapModeToString(pattern->mipmapMode)); } if (!pattern->matrix.isIdentity()) { xml.addAttribute("matrix", pattern->matrix.toString()); diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index a08aed7afe..5a06d9b8d5 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -876,7 +876,9 @@ std::unique_ptr PAGXImporterImpl::parseImagePattern(const XMLNode* pattern->image = getAttribute(node, "image"); pattern->tileModeX = TileModeFromString(getAttribute(node, "tileModeX", "clamp")); pattern->tileModeY = TileModeFromString(getAttribute(node, "tileModeY", "clamp")); - pattern->sampling = SamplingModeFromString(getAttribute(node, "sampling", "linear")); + pattern->minFilterMode = FilterModeFromString(getAttribute(node, "minFilterMode", "linear")); + pattern->magFilterMode = FilterModeFromString(getAttribute(node, "magFilterMode", "linear")); + pattern->mipmapMode = MipmapModeFromString(getAttribute(node, "mipmapMode", "linear")); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { pattern->matrix = Matrix::Parse(matrixStr); diff --git a/pagx/src/PAGXStringUtils.cpp b/pagx/src/PAGXStringUtils.cpp index 44a3ab66b4..0b35bdbbf4 100644 --- a/pagx/src/PAGXStringUtils.cpp +++ b/pagx/src/PAGXStringUtils.cpp @@ -197,10 +197,14 @@ DEFINE_ENUM_CONVERSION(TileMode, {TileMode::Mirror, "mirror"}, {TileMode::Decal, "decal"}) -DEFINE_ENUM_CONVERSION(SamplingMode, - {SamplingMode::Nearest, "nearest"}, - {SamplingMode::Linear, "linear"}, - {SamplingMode::Mipmap, "mipmap"}) +DEFINE_ENUM_CONVERSION(FilterMode, + {FilterMode::Nearest, "nearest"}, + {FilterMode::Linear, "linear"}) + +DEFINE_ENUM_CONVERSION(MipmapMode, + {MipmapMode::None, "none"}, + {MipmapMode::Nearest, "nearest"}, + {MipmapMode::Linear, "linear"}) DEFINE_ENUM_CONVERSION(MaskType, {MaskType::Alpha, "alpha"}, diff --git a/pagx/src/PAGXStringUtils.h b/pagx/src/PAGXStringUtils.h index ce7d0fb121..3de411a58e 100644 --- a/pagx/src/PAGXStringUtils.h +++ b/pagx/src/PAGXStringUtils.h @@ -39,6 +39,8 @@ #include "pagx/nodes/Color.h" #include "pagx/nodes/ColorSpace.h" #include "pagx/nodes/LayerPlacement.h" +#include "pagx/nodes/FilterMode.h" +#include "pagx/nodes/MipmapMode.h" #include "pagx/nodes/TileMode.h" namespace pagx { @@ -118,10 +120,12 @@ std::string MergePathModeToString(MergePathMode mode); MergePathMode MergePathModeFromString(const std::string& str); //============================================================================== -// SamplingMode +// FilterMode, MipmapMode //============================================================================== -std::string SamplingModeToString(SamplingMode mode); -SamplingMode SamplingModeFromString(const std::string& str); +std::string FilterModeToString(FilterMode mode); +FilterMode FilterModeFromString(const std::string& str); +std::string MipmapModeToString(MipmapMode mode); +MipmapMode MipmapModeFromString(const std::string& str); //============================================================================== // TextAlign, VerticalAlign, Overflow diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index dad5eebf13..bdf233dd5c 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -1994,7 +1994,9 @@ std::unique_ptr SVGParserImpl::getColorSourceForRef(const std::stri pattern->image = src->image; pattern->tileModeX = src->tileModeX; pattern->tileModeY = src->tileModeY; - pattern->sampling = src->sampling; + pattern->minFilterMode = src->minFilterMode; + pattern->magFilterMode = src->magFilterMode; + pattern->mipmapMode = src->mipmapMode; pattern->matrix = src->matrix; return pattern; } From b5b032425f6126bc323f20c4729547f17f82ba0f Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 23:16:44 +0800 Subject: [PATCH 114/678] Add detailed comments for all enum values in PAGX nodes. --- pagx/include/pagx/nodes/Fill.h | 8 ++++++- pagx/include/pagx/nodes/Layer.h | 11 +++++++++- pagx/include/pagx/nodes/MergePath.h | 17 ++++++++++++++- pagx/include/pagx/nodes/Polystar.h | 8 ++++++- pagx/include/pagx/nodes/Stroke.h | 33 ++++++++++++++++++++++++++--- pagx/include/pagx/nodes/TrimPath.h | 8 ++++++- 6 files changed, 77 insertions(+), 8 deletions(-) diff --git a/pagx/include/pagx/nodes/Fill.h b/pagx/include/pagx/nodes/Fill.h index 5db840c7ba..a508839f0f 100644 --- a/pagx/include/pagx/nodes/Fill.h +++ b/pagx/include/pagx/nodes/Fill.h @@ -28,10 +28,16 @@ namespace pagx { /** - * Fill rules for paths. + * Fill rules that determine the interior of self-intersecting paths. */ enum class FillRule { + /** + * Non-zero winding rule. A point is inside if the winding number is non-zero. + */ Winding, + /** + * Even-odd rule. A point is inside if the number of crossings is odd. + */ EvenOdd }; diff --git a/pagx/include/pagx/nodes/Layer.h b/pagx/include/pagx/nodes/Layer.h index faea8a3e11..0e111f53a4 100644 --- a/pagx/include/pagx/nodes/Layer.h +++ b/pagx/include/pagx/nodes/Layer.h @@ -33,11 +33,20 @@ namespace pagx { /** - * Mask types for layer masking. + * Mask types that define how a mask layer affects its target. */ enum class MaskType { + /** + * Use the alpha channel of the mask to determine visibility. + */ Alpha, + /** + * Use the luminance (brightness) of the mask to determine visibility. + */ Luminance, + /** + * Use the contour (outline) of the mask for masking. + */ Contour }; diff --git a/pagx/include/pagx/nodes/MergePath.h b/pagx/include/pagx/nodes/MergePath.h index 2c3b6b8e90..1f474e3ab1 100644 --- a/pagx/include/pagx/nodes/MergePath.h +++ b/pagx/include/pagx/nodes/MergePath.h @@ -23,13 +23,28 @@ namespace pagx { /** - * Path merge modes (boolean operations). + * Path merge modes for boolean operations. */ enum class MergePathMode { + /** + * Append all paths without any boolean operation. + */ Append, + /** + * Union (add) all paths together. + */ Union, + /** + * Intersect all paths, keeping only overlapping areas. + */ Intersect, + /** + * Exclusive-or of all paths, keeping non-overlapping areas. + */ Xor, + /** + * Subtract subsequent paths from the first path. + */ Difference }; diff --git a/pagx/include/pagx/nodes/Polystar.h b/pagx/include/pagx/nodes/Polystar.h index ad0c1cdf84..76e648e1f3 100644 --- a/pagx/include/pagx/nodes/Polystar.h +++ b/pagx/include/pagx/nodes/Polystar.h @@ -24,10 +24,16 @@ namespace pagx { /** - * Polystar types. + * Polystar shape types. */ enum class PolystarType { + /** + * A regular polygon with equal-length sides. + */ Polygon, + /** + * A star shape with alternating inner and outer points. + */ Star }; diff --git a/pagx/include/pagx/nodes/Stroke.h b/pagx/include/pagx/nodes/Stroke.h index afb557408a..901f39ef25 100644 --- a/pagx/include/pagx/nodes/Stroke.h +++ b/pagx/include/pagx/nodes/Stroke.h @@ -29,29 +29,56 @@ namespace pagx { /** - * Line cap styles for strokes. + * Line cap styles that define the shape at the endpoints of open paths. */ enum class LineCap { + /** + * A flat cap that ends exactly at the path endpoint. + */ Butt, + /** + * A rounded cap that extends beyond the endpoint by half the stroke width. + */ Round, + /** + * A square cap that extends beyond the endpoint by half the stroke width. + */ Square }; /** - * Line join styles for strokes. + * Line join styles that define the shape at the corners of paths. */ enum class LineJoin { + /** + * A sharp join that extends to a point. + */ Miter, + /** + * A rounded join with a circular arc. + */ Round, + /** + * A beveled join that cuts off the corner. + */ Bevel }; /** - * Stroke alignment relative to path. + * Stroke alignment relative to the path. */ enum class StrokeAlign { + /** + * Stroke is centered on the path. + */ Center, + /** + * Stroke is drawn inside the path boundary. + */ Inside, + /** + * Stroke is drawn outside the path boundary. + */ Outside }; diff --git a/pagx/include/pagx/nodes/TrimPath.h b/pagx/include/pagx/nodes/TrimPath.h index 3fe55a5e5b..26b1883228 100644 --- a/pagx/include/pagx/nodes/TrimPath.h +++ b/pagx/include/pagx/nodes/TrimPath.h @@ -23,10 +23,16 @@ namespace pagx { /** - * Trim path types. + * Trim path types that control how multiple paths are trimmed. */ enum class TrimType { + /** + * Trim each path individually within the group. + */ Separate, + /** + * Treat all paths as one continuous path and trim collectively. + */ Continuous }; From 10eec4fdfc1ae1dca44eed3970df7e97f676af26 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 23:22:44 +0800 Subject: [PATCH 115/678] Remove baselineShift from TextSpan as it can be handled via pre-layout or TextModifier. --- pagx/docs/pagx_spec.md | 4 +--- pagx/include/pagx/nodes/TextSpan.h | 5 ----- pagx/src/PAGXExporter.cpp | 15 +++++++-------- pagx/src/PAGXImporter.cpp | 17 ++++++++--------- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 453ec0a35e..f15aadc590 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -1023,7 +1023,7 @@ y = center.y + outerRadius * sin(angle) 文本片段提供文本内容的几何形状。一个 TextSpan 经过塑形后会产生**字形列表**(多个字形),而非单一 Path。 ```xml - + ``` @@ -1036,7 +1036,6 @@ y = center.y + outerRadius * sin(angle) | `fontWeight` | int | 400 | 字重(100-900) | | `fontStyle` | enum | normal | normal 或 italic | | `tracking` | float | 0 | 字距 | -| `baselineShift` | float | 0 | 基线偏移 | **处理流程**: 1. 根据 `font`、`fontSize`、`fontWeight`、`fontStyle` 查找字体 @@ -2261,7 +2260,6 @@ Layer / Group | `fontWeight` | int | 400 | | `fontStyle` | enum | normal | | `tracking` | float | 0 | -| `baselineShift` | float | 0 | 内容:`CDATA` 文本 diff --git a/pagx/include/pagx/nodes/TextSpan.h b/pagx/include/pagx/nodes/TextSpan.h index 844527f67d..e87dd62b31 100644 --- a/pagx/include/pagx/nodes/TextSpan.h +++ b/pagx/include/pagx/nodes/TextSpan.h @@ -60,11 +60,6 @@ class TextSpan : public Element { */ float tracking = 0; - /** - * The baseline shift in pixels, positive values shift the text up. The default value is 0. - */ - float baselineShift = 0; - /** * The text content to render. */ diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index c7c7beb775..f28036d1e8 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -258,7 +258,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { } xml.addRequiredAttribute("endPoint", pointToString(grad->endPoint)); if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); + xml.addAttribute("matrix", MatrixToString(grad->matrix)); } if (grad->colorStops.empty()) { xml.closeElementSelfClosing(); @@ -278,7 +278,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { } xml.addRequiredAttribute("radius", grad->radius); if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); + xml.addAttribute("matrix", MatrixToString(grad->matrix)); } if (grad->colorStops.empty()) { xml.closeElementSelfClosing(); @@ -299,7 +299,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { xml.addAttribute("startAngle", grad->startAngle); xml.addAttribute("endAngle", grad->endAngle, 360.0f); if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); + xml.addAttribute("matrix", MatrixToString(grad->matrix)); } if (grad->colorStops.empty()) { xml.closeElementSelfClosing(); @@ -319,7 +319,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { } xml.addRequiredAttribute("halfDiagonal", grad->halfDiagonal); if (!grad->matrix.isIdentity()) { - xml.addAttribute("matrix", grad->matrix.toString()); + xml.addAttribute("matrix", MatrixToString(grad->matrix)); } if (grad->colorStops.empty()) { xml.closeElementSelfClosing(); @@ -351,7 +351,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { xml.addAttribute("mipmapMode", MipmapModeToString(pattern->mipmapMode)); } if (!pattern->matrix.isIdentity()) { - xml.addAttribute("matrix", pattern->matrix.toString()); + xml.addAttribute("matrix", MatrixToString(pattern->matrix)); } xml.closeElementSelfClosing(); break; @@ -418,7 +418,7 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.addAttribute("data", path->dataRef); } else { // Inline the path data - xml.addAttribute("data", path->data.toSVGString()); + xml.addAttribute("data", PathDataToSVGString(path->data)); } } xml.addAttribute("reversed", path->reversed); @@ -438,7 +438,6 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node) { xml.addAttribute("fontStyle", text->fontStyle); } xml.addAttribute("tracking", text->tracking); - xml.addAttribute("baselineShift", text->baselineShift); xml.closeElementStart(); xml.addTextContent(text->text); xml.closeElement(); @@ -872,7 +871,7 @@ static void writeLayer(XMLBuilder& xml, const Layer* node) { xml.addAttribute("x", node->x); xml.addAttribute("y", node->y); if (!node->matrix.isIdentity()) { - xml.addAttribute("matrix", node->matrix.toString()); + xml.addAttribute("matrix", MatrixToString(node->matrix)); } if (!node->matrix3D.empty()) { xml.addAttribute("matrix3D", floatListToString(node->matrix3D)); diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 5a06d9b8d5..43d6b07264 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -332,7 +332,7 @@ std::unique_ptr PAGXImporterImpl::parseLayer(const XMLNode* node) { layer->y = getFloatAttribute(node, "y", 0); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { - layer->matrix = Matrix::Parse(matrixStr); + layer->matrix = MatrixFromString(matrixStr); } auto matrix3DStr = getAttribute(node, "matrix3D"); if (!matrix3DStr.empty()) { @@ -578,7 +578,7 @@ std::unique_ptr PAGXImporterImpl::parsePath(const XMLNode* node) { auto path = std::make_unique(); auto dataAttr = getAttribute(node, "data"); if (!dataAttr.empty()) { - path->data = PathData::FromSVGString(dataAttr); + path->data = PathDataFromSVGString(dataAttr); } path->reversed = getBoolAttribute(node, "reversed", false); return path; @@ -593,7 +593,6 @@ std::unique_ptr PAGXImporterImpl::parseTextSpan(const XMLNode* node) { textSpan->fontWeight = getIntAttribute(node, "fontWeight", 400); textSpan->fontStyle = getAttribute(node, "fontStyle", "normal"); textSpan->tracking = getFloatAttribute(node, "tracking", 0); - textSpan->baselineShift = getFloatAttribute(node, "baselineShift", 0); textSpan->text = node->text; return textSpan; } @@ -805,7 +804,7 @@ std::unique_ptr PAGXImporterImpl::parseLinearGradient(const XMLN gradient->endPoint = parsePoint(endPointStr); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { - gradient->matrix = Matrix::Parse(matrixStr); + gradient->matrix = MatrixFromString(matrixStr); } for (const auto& child : node->children) { if (child->tag == "ColorStop") { @@ -823,7 +822,7 @@ std::unique_ptr PAGXImporterImpl::parseRadialGradient(const XMLN gradient->radius = getFloatAttribute(node, "radius", 0); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { - gradient->matrix = Matrix::Parse(matrixStr); + gradient->matrix = MatrixFromString(matrixStr); } for (const auto& child : node->children) { if (child->tag == "ColorStop") { @@ -842,7 +841,7 @@ std::unique_ptr PAGXImporterImpl::parseConicGradient(const XMLNod gradient->endAngle = getFloatAttribute(node, "endAngle", 360); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { - gradient->matrix = Matrix::Parse(matrixStr); + gradient->matrix = MatrixFromString(matrixStr); } for (const auto& child : node->children) { if (child->tag == "ColorStop") { @@ -860,7 +859,7 @@ std::unique_ptr PAGXImporterImpl::parseDiamondGradient(const XM gradient->halfDiagonal = getFloatAttribute(node, "halfDiagonal", 0); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { - gradient->matrix = Matrix::Parse(matrixStr); + gradient->matrix = MatrixFromString(matrixStr); } for (const auto& child : node->children) { if (child->tag == "ColorStop") { @@ -881,7 +880,7 @@ std::unique_ptr PAGXImporterImpl::parseImagePattern(const XMLNode* pattern->mipmapMode = MipmapModeFromString(getAttribute(node, "mipmapMode", "linear")); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { - pattern->matrix = Matrix::Parse(matrixStr); + pattern->matrix = MatrixFromString(matrixStr); } return pattern; } @@ -909,7 +908,7 @@ std::unique_ptr PAGXImporterImpl::parseImage(const XMLNode* node) { std::unique_ptr PAGXImporterImpl::parsePathData(const XMLNode* node) { auto data = getAttribute(node, "data"); - auto pathData = std::make_unique(PathData::FromSVGString(data)); + auto pathData = std::make_unique(PathDataFromSVGString(data)); pathData->id = getAttribute(node, "id"); return pathData; } From bda3608568b2bce90b419dced0022314b9267270 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 21 Jan 2026 23:23:11 +0800 Subject: [PATCH 116/678] Decouple encoding/decoding from Nodes: move SVG and Matrix string functions to internal modules. - Remove PathData::toSVGString() and FromSVGString() from public API - Remove Matrix::toString() and Parse() from public API - Add PathDataToSVGString(), PathDataFromSVGString() to PAGXStringUtils - Add MatrixToString(), MatrixFromString() to PAGXStringUtils - Update all call sites to use the new internal functions - Update tests to use PathData public construction API instead of SVG parsing This change ensures Nodes objects focus purely on rendering data, while encoding/decoding logic is encapsulated in importer/exporter modules. --- pagx/include/pagx/nodes/Matrix.h | 39 --- pagx/src/PAGXStringUtils.cpp | 470 +++++++++++++++++++++++++++++++ pagx/src/PAGXStringUtils.h | 15 + pagx/src/PathData.cpp | 429 ---------------------------- pagx/src/svg/SVGImporter.cpp | 6 +- test/src/PAGXTest.cpp | 40 +-- 6 files changed, 511 insertions(+), 488 deletions(-) diff --git a/pagx/include/pagx/nodes/Matrix.h b/pagx/include/pagx/nodes/Matrix.h index 65f9882ccf..3394d73b2e 100644 --- a/pagx/include/pagx/nodes/Matrix.h +++ b/pagx/include/pagx/nodes/Matrix.h @@ -19,9 +19,6 @@ #pragma once #include -#include -#include -#include #include "pagx/nodes/Point.h" namespace pagx { @@ -106,42 +103,6 @@ struct Matrix { return m; } - /** - * Parses a matrix string "a,b,c,d,tx,ty". - */ - static Matrix Parse(const std::string& str) { - Matrix m = {}; - std::istringstream iss(str); - std::string token = {}; - std::vector values = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - values.push_back(std::stof(trimmed)); - } - } - if (values.size() >= 6) { - m.a = values[0]; - m.b = values[1]; - m.c = values[2]; - m.d = values[3]; - m.tx = values[4]; - m.ty = values[5]; - } - return m; - } - - /** - * Returns the matrix as a string "a,b,c,d,tx,ty". - */ - std::string toString() const { - std::ostringstream oss = {}; - oss << a << "," << b << "," << c << "," << d << "," << tx << "," << ty; - return oss.str(); - } - /** * Returns true if this is the identity matrix. */ diff --git a/pagx/src/PAGXStringUtils.cpp b/pagx/src/PAGXStringUtils.cpp index 0b35bdbbf4..341e6ba809 100644 --- a/pagx/src/PAGXStringUtils.cpp +++ b/pagx/src/PAGXStringUtils.cpp @@ -17,6 +17,7 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "PAGXStringUtils.h" +#include #include #include #include @@ -307,6 +308,475 @@ std::string ColorToHexString(const Color& color, bool withAlpha) { return result; } +//============================================================================== +// Matrix encoding/decoding +//============================================================================== + +std::string MatrixToString(const Matrix& matrix) { + std::ostringstream oss = {}; + oss << matrix.a << "," << matrix.b << "," << matrix.c << "," << matrix.d << "," << matrix.tx + << "," << matrix.ty; + return oss.str(); +} + +Matrix MatrixFromString(const std::string& str) { + Matrix m = {}; + std::istringstream iss(str); + std::string token = {}; + std::vector values = {}; + while (std::getline(iss, token, ',')) { + auto trimmed = token; + trimmed.erase(0, trimmed.find_first_not_of(" \t")); + trimmed.erase(trimmed.find_last_not_of(" \t") + 1); + if (!trimmed.empty()) { + values.push_back(std::stof(trimmed)); + } + } + if (values.size() >= 6) { + m.a = values[0]; + m.b = values[1]; + m.c = values[2]; + m.d = values[3]; + m.tx = values[4]; + m.ty = values[5]; + } + return m; +} + +//============================================================================== +// PathData SVG encoding/decoding +//============================================================================== + +std::string PathDataToSVGString(const PathData& pathData) { + std::ostringstream oss; + oss.precision(6); + + size_t pointIndex = 0; + const auto& verbs = pathData.verbs(); + const auto& points = pathData.points(); + + for (auto verb : verbs) { + const float* pts = points.data() + pointIndex; + switch (verb) { + case PathVerb::Move: + oss << "M" << pts[0] << " " << pts[1]; + break; + case PathVerb::Line: + oss << "L" << pts[0] << " " << pts[1]; + break; + case PathVerb::Quad: + oss << "Q" << pts[0] << " " << pts[1] << " " << pts[2] << " " << pts[3]; + break; + case PathVerb::Cubic: + oss << "C" << pts[0] << " " << pts[1] << " " << pts[2] << " " << pts[3] << " " << pts[4] + << " " << pts[5]; + break; + case PathVerb::Close: + oss << "Z"; + break; + } + pointIndex += PathData::PointsPerVerb(verb) * 2; + } + + return oss.str(); +} + +// SVG path parser helper functions +static void SkipWhitespaceAndCommas(const char*& ptr, const char* end) { + while (ptr < end && (std::isspace(*ptr) || *ptr == ',')) { + ++ptr; + } +} + +static bool ParseNumber(const char*& ptr, const char* end, float& result) { + SkipWhitespaceAndCommas(ptr, end); + if (ptr >= end) { + return false; + } + + const char* start = ptr; + if (*ptr == '-' || *ptr == '+') { + ++ptr; + } + bool hasDigits = false; + while (ptr < end && std::isdigit(*ptr)) { + ++ptr; + hasDigits = true; + } + if (ptr < end && *ptr == '.') { + ++ptr; + while (ptr < end && std::isdigit(*ptr)) { + ++ptr; + hasDigits = true; + } + } + if (ptr < end && (*ptr == 'e' || *ptr == 'E')) { + ++ptr; + if (ptr < end && (*ptr == '-' || *ptr == '+')) { + ++ptr; + } + while (ptr < end && std::isdigit(*ptr)) { + ++ptr; + } + } + + if (!hasDigits) { + ptr = start; + return false; + } + + std::string numStr(start, ptr); + result = std::stof(numStr); + return true; +} + +static bool ParseFlag(const char*& ptr, const char* end, bool& result) { + SkipWhitespaceAndCommas(ptr, end); + if (ptr >= end) { + return false; + } + if (*ptr == '0') { + result = false; + ++ptr; + return true; + } + if (*ptr == '1') { + result = true; + ++ptr; + return true; + } + return false; +} + +// Convert arc to cubic Bezier curves +static void ArcToCubics(PathData& path, float x1, float y1, float rx, float ry, float angle, + bool largeArc, bool sweep, float x2, float y2) { + if (rx == 0 || ry == 0) { + path.lineTo(x2, y2); + return; + } + + rx = std::abs(rx); + ry = std::abs(ry); + + float radians = angle * 3.14159265358979323846f / 180.0f; + float cosAngle = std::cos(radians); + float sinAngle = std::sin(radians); + + float dx2 = (x1 - x2) / 2.0f; + float dy2 = (y1 - y2) / 2.0f; + + float x1p = cosAngle * dx2 + sinAngle * dy2; + float y1p = -sinAngle * dx2 + cosAngle * dy2; + + float lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry); + if (lambda > 1.0f) { + float sqrtLambda = std::sqrt(lambda); + rx *= sqrtLambda; + ry *= sqrtLambda; + } + + float rx2 = rx * rx; + float ry2 = ry * ry; + float x1p2 = x1p * x1p; + float y1p2 = y1p * y1p; + + float num = rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2; + float den = rx2 * y1p2 + ry2 * x1p2; + float sq = (num < 0 || den == 0) ? 0.0f : std::sqrt(num / den); + + if (largeArc == sweep) { + sq = -sq; + } + + float cxp = sq * rx * y1p / ry; + float cyp = -sq * ry * x1p / rx; + + float cx = cosAngle * cxp - sinAngle * cyp + (x1 + x2) / 2.0f; + float cy = sinAngle * cxp + cosAngle * cyp + (y1 + y2) / 2.0f; + + auto vectorAngle = [](float ux, float uy, float vx, float vy) -> float { + float n = std::sqrt(ux * ux + uy * uy) * std::sqrt(vx * vx + vy * vy); + if (n == 0) { + return 0; + } + float c = (ux * vx + uy * vy) / n; + c = std::max(-1.0f, std::min(1.0f, c)); + float angle = std::acos(c); + if (ux * vy - uy * vx < 0) { + angle = -angle; + } + return angle; + }; + + float theta1 = vectorAngle(1.0f, 0.0f, (x1p - cxp) / rx, (y1p - cyp) / ry); + float dtheta = vectorAngle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, + (-y1p - cyp) / ry); + + if (!sweep && dtheta > 0) { + dtheta -= 2.0f * 3.14159265358979323846f; + } else if (sweep && dtheta < 0) { + dtheta += 2.0f * 3.14159265358979323846f; + } + + int segments = static_cast(std::ceil(std::abs(dtheta) / (3.14159265358979323846f / 2.0f))); + float segmentAngle = dtheta / segments; + + float t = std::tan(segmentAngle / 2.0f); + float alpha = std::sin(segmentAngle) * (std::sqrt(4.0f + 3.0f * t * t) - 1.0f) / 3.0f; + + float currentAngle = theta1; + float currentX = x1; + float currentY = y1; + + for (int i = 0; i < segments; ++i) { + float nextAngle = currentAngle + segmentAngle; + + float cosStart = std::cos(currentAngle); + float sinStart = std::sin(currentAngle); + float cosEnd = std::cos(nextAngle); + float sinEnd = std::sin(nextAngle); + + float ex = cx + rx * (cosAngle * cosEnd - sinAngle * sinEnd); + float ey = cy + rx * (sinAngle * cosEnd + cosAngle * sinEnd); + + float dx1 = -rx * (cosAngle * sinStart + sinAngle * cosStart); + float dy1 = -rx * (sinAngle * sinStart - cosAngle * cosStart); + float dx2m = -rx * (cosAngle * sinEnd + sinAngle * cosEnd); + float dy2m = -rx * (sinAngle * sinEnd - cosAngle * cosEnd); + + float c1x = currentX + alpha * dx1; + float c1y = currentY + alpha * dy1; + float c2x = ex - alpha * dx2m; + float c2y = ey - alpha * dy2m; + + path.cubicTo(c1x, c1y, c2x, c2y, ex, ey); + + currentAngle = nextAngle; + currentX = ex; + currentY = ey; + } +} + +PathData PathDataFromSVGString(const std::string& d) { + PathData path; + if (d.empty()) { + return path; + } + + const char* ptr = d.c_str(); + const char* end = ptr + d.length(); + + float currentX = 0; + float currentY = 0; + float startX = 0; + float startY = 0; + float lastControlX = 0; + float lastControlY = 0; + char lastCommand = 0; + + while (ptr < end) { + SkipWhitespaceAndCommas(ptr, end); + if (ptr >= end) { + break; + } + + char command = *ptr; + if (std::isalpha(command)) { + ++ptr; + } else { + command = lastCommand; + } + + bool isRelative = std::islower(command); + char upperCommand = std::toupper(command); + + switch (upperCommand) { + case 'M': { + float x, y; + if (!ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x += currentX; + y += currentY; + } + path.moveTo(x, y); + currentX = startX = x; + currentY = startY = y; + lastCommand = isRelative ? 'l' : 'L'; + break; + } + case 'L': { + float x, y; + if (!ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x += currentX; + y += currentY; + } + path.lineTo(x, y); + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'H': { + float x; + if (!ParseNumber(ptr, end, x)) { + break; + } + if (isRelative) { + x += currentX; + } + path.lineTo(x, currentY); + currentX = x; + lastCommand = command; + break; + } + case 'V': { + float y; + if (!ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + y += currentY; + } + path.lineTo(currentX, y); + currentY = y; + lastCommand = command; + break; + } + case 'C': { + float x1, y1, x2, y2, x, y; + if (!ParseNumber(ptr, end, x1) || !ParseNumber(ptr, end, y1) || + !ParseNumber(ptr, end, x2) || !ParseNumber(ptr, end, y2) || + !ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x1 += currentX; + y1 += currentY; + x2 += currentX; + y2 += currentY; + x += currentX; + y += currentY; + } + path.cubicTo(x1, y1, x2, y2, x, y); + lastControlX = x2; + lastControlY = y2; + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'S': { + float x2, y2, x, y; + if (!ParseNumber(ptr, end, x2) || !ParseNumber(ptr, end, y2) || + !ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x2 += currentX; + y2 += currentY; + x += currentX; + y += currentY; + } + float x1 = currentX; + float y1 = currentY; + char lastUpper = std::toupper(lastCommand); + if (lastUpper == 'C' || lastUpper == 'S') { + x1 = 2 * currentX - lastControlX; + y1 = 2 * currentY - lastControlY; + } + path.cubicTo(x1, y1, x2, y2, x, y); + lastControlX = x2; + lastControlY = y2; + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'Q': { + float x1, y1, x, y; + if (!ParseNumber(ptr, end, x1) || !ParseNumber(ptr, end, y1) || + !ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x1 += currentX; + y1 += currentY; + x += currentX; + y += currentY; + } + path.quadTo(x1, y1, x, y); + lastControlX = x1; + lastControlY = y1; + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'T': { + float x, y; + if (!ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x += currentX; + y += currentY; + } + float x1 = currentX; + float y1 = currentY; + char lastUpper = std::toupper(lastCommand); + if (lastUpper == 'Q' || lastUpper == 'T') { + x1 = 2 * currentX - lastControlX; + y1 = 2 * currentY - lastControlY; + } + path.quadTo(x1, y1, x, y); + lastControlX = x1; + lastControlY = y1; + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'A': { + float rx, ry, arcAngle, x, y; + bool largeArc, sweep; + if (!ParseNumber(ptr, end, rx) || !ParseNumber(ptr, end, ry) || + !ParseNumber(ptr, end, arcAngle) || !ParseFlag(ptr, end, largeArc) || + !ParseFlag(ptr, end, sweep) || !ParseNumber(ptr, end, x) || + !ParseNumber(ptr, end, y)) { + break; + } + if (isRelative) { + x += currentX; + y += currentY; + } + ArcToCubics(path, currentX, currentY, rx, ry, arcAngle, largeArc, sweep, x, y); + currentX = x; + currentY = y; + lastCommand = command; + break; + } + case 'Z': { + path.close(); + currentX = startX; + currentY = startY; + lastCommand = command; + break; + } + default: + ++ptr; + break; + } + } + + return path; +} + #undef DEFINE_ENUM_CONVERSION } // namespace pagx diff --git a/pagx/src/PAGXStringUtils.h b/pagx/src/PAGXStringUtils.h index 3de411a58e..27146a667f 100644 --- a/pagx/src/PAGXStringUtils.h +++ b/pagx/src/PAGXStringUtils.h @@ -28,6 +28,7 @@ #include "pagx/nodes/LayerStyle.h" #include "pagx/nodes/MergePath.h" #include "pagx/nodes/Node.h" +#include "pagx/nodes/PathData.h" #include "pagx/nodes/Polystar.h" #include "pagx/nodes/RangeSelector.h" #include "pagx/nodes/Repeater.h" @@ -42,6 +43,8 @@ #include "pagx/nodes/FilterMode.h" #include "pagx/nodes/MipmapMode.h" #include "pagx/nodes/TileMode.h" +#include "pagx/nodes/Matrix.h" +#include "pagx/nodes/PathData.h" namespace pagx { @@ -158,4 +161,16 @@ SelectorMode SelectorModeFromString(const std::string& str); //============================================================================== std::string ColorToHexString(const Color& color, bool withAlpha = false); +//============================================================================== +// Matrix - encoding/decoding for PAGX format +//============================================================================== +std::string MatrixToString(const Matrix& matrix); +Matrix MatrixFromString(const std::string& str); + +//============================================================================== +// PathData +//============================================================================== +PathData PathDataFromSVGString(const std::string& d); +std::string PathDataToSVGString(const PathData& path); + } // namespace pagx diff --git a/pagx/src/PathData.cpp b/pagx/src/PathData.cpp index a32a026b84..004754d44f 100644 --- a/pagx/src/PathData.cpp +++ b/pagx/src/PathData.cpp @@ -17,9 +17,7 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "pagx/nodes/PathData.h" -#include #include -#include namespace pagx { @@ -185,431 +183,4 @@ Rect PathData::getBounds() { return _cachedBounds; } -std::string PathData::toSVGString() const { - std::ostringstream oss; - oss.precision(6); - - size_t pointIndex = 0; - for (auto verb : _verbs) { - const float* pts = _points.data() + pointIndex; - switch (verb) { - case PathVerb::Move: - oss << "M" << pts[0] << " " << pts[1]; - break; - case PathVerb::Line: - oss << "L" << pts[0] << " " << pts[1]; - break; - case PathVerb::Quad: - oss << "Q" << pts[0] << " " << pts[1] << " " << pts[2] << " " << pts[3]; - break; - case PathVerb::Cubic: - oss << "C" << pts[0] << " " << pts[1] << " " << pts[2] << " " << pts[3] << " " << pts[4] - << " " << pts[5]; - break; - case PathVerb::Close: - oss << "Z"; - break; - } - pointIndex += PointsPerVerb(verb) * 2; - } - - return oss.str(); -} - -// SVG path parser helper functions -static void SkipWhitespaceAndCommas(const char*& ptr, const char* end) { - while (ptr < end && (std::isspace(*ptr) || *ptr == ',')) { - ++ptr; - } -} - -static bool ParseNumber(const char*& ptr, const char* end, float& result) { - SkipWhitespaceAndCommas(ptr, end); - if (ptr >= end) { - return false; - } - - const char* start = ptr; - if (*ptr == '-' || *ptr == '+') { - ++ptr; - } - bool hasDigits = false; - while (ptr < end && std::isdigit(*ptr)) { - ++ptr; - hasDigits = true; - } - if (ptr < end && *ptr == '.') { - ++ptr; - while (ptr < end && std::isdigit(*ptr)) { - ++ptr; - hasDigits = true; - } - } - if (ptr < end && (*ptr == 'e' || *ptr == 'E')) { - ++ptr; - if (ptr < end && (*ptr == '-' || *ptr == '+')) { - ++ptr; - } - while (ptr < end && std::isdigit(*ptr)) { - ++ptr; - } - } - - if (!hasDigits) { - ptr = start; - return false; - } - - std::string numStr(start, ptr); - result = std::stof(numStr); - return true; -} - -static bool ParseFlag(const char*& ptr, const char* end, bool& result) { - SkipWhitespaceAndCommas(ptr, end); - if (ptr >= end) { - return false; - } - if (*ptr == '0') { - result = false; - ++ptr; - return true; - } - if (*ptr == '1') { - result = true; - ++ptr; - return true; - } - return false; -} - -// Convert arc to cubic Bezier curves -static void ArcToCubics(PathData& path, float x1, float y1, float rx, float ry, float angle, - bool largeArc, bool sweep, float x2, float y2) { - if (rx == 0 || ry == 0) { - path.lineTo(x2, y2); - return; - } - - rx = std::abs(rx); - ry = std::abs(ry); - - float radians = angle * 3.14159265358979323846f / 180.0f; - float cosAngle = std::cos(radians); - float sinAngle = std::sin(radians); - - float dx2 = (x1 - x2) / 2.0f; - float dy2 = (y1 - y2) / 2.0f; - - float x1p = cosAngle * dx2 + sinAngle * dy2; - float y1p = -sinAngle * dx2 + cosAngle * dy2; - - float lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry); - if (lambda > 1.0f) { - float sqrtLambda = std::sqrt(lambda); - rx *= sqrtLambda; - ry *= sqrtLambda; - } - - float rx2 = rx * rx; - float ry2 = ry * ry; - float x1p2 = x1p * x1p; - float y1p2 = y1p * y1p; - - float num = rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2; - float den = rx2 * y1p2 + ry2 * x1p2; - float sq = (num < 0 || den == 0) ? 0.0f : std::sqrt(num / den); - - if (largeArc == sweep) { - sq = -sq; - } - - float cxp = sq * rx * y1p / ry; - float cyp = -sq * ry * x1p / rx; - - float cx = cosAngle * cxp - sinAngle * cyp + (x1 + x2) / 2.0f; - float cy = sinAngle * cxp + cosAngle * cyp + (y1 + y2) / 2.0f; - - auto vectorAngle = [](float ux, float uy, float vx, float vy) -> float { - float n = std::sqrt(ux * ux + uy * uy) * std::sqrt(vx * vx + vy * vy); - if (n == 0) { - return 0; - } - float c = (ux * vx + uy * vy) / n; - c = std::max(-1.0f, std::min(1.0f, c)); - float angle = std::acos(c); - if (ux * vy - uy * vx < 0) { - angle = -angle; - } - return angle; - }; - - float theta1 = vectorAngle(1.0f, 0.0f, (x1p - cxp) / rx, (y1p - cyp) / ry); - float dtheta = vectorAngle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, - (-y1p - cyp) / ry); - - if (!sweep && dtheta > 0) { - dtheta -= 2.0f * 3.14159265358979323846f; - } else if (sweep && dtheta < 0) { - dtheta += 2.0f * 3.14159265358979323846f; - } - - int segments = static_cast(std::ceil(std::abs(dtheta) / (3.14159265358979323846f / 2.0f))); - float segmentAngle = dtheta / segments; - - float t = std::tan(segmentAngle / 2.0f); - float alpha = std::sin(segmentAngle) * (std::sqrt(4.0f + 3.0f * t * t) - 1.0f) / 3.0f; - - float currentAngle = theta1; - float currentX = x1; - float currentY = y1; - - for (int i = 0; i < segments; ++i) { - float nextAngle = currentAngle + segmentAngle; - - float cosStart = std::cos(currentAngle); - float sinStart = std::sin(currentAngle); - float cosEnd = std::cos(nextAngle); - float sinEnd = std::sin(nextAngle); - - float ex = cx + rx * (cosAngle * cosEnd - sinAngle * sinEnd); - float ey = cy + rx * (sinAngle * cosEnd + cosAngle * sinEnd); - - float dx1 = -rx * (cosAngle * sinStart + sinAngle * cosStart); - float dy1 = -rx * (sinAngle * sinStart - cosAngle * cosStart); - float dx2 = -rx * (cosAngle * sinEnd + sinAngle * cosEnd); - float dy2 = -rx * (sinAngle * sinEnd - cosAngle * cosEnd); - - float c1x = currentX + alpha * dx1; - float c1y = currentY + alpha * dy1; - float c2x = ex - alpha * dx2; - float c2y = ey - alpha * dy2; - - path.cubicTo(c1x, c1y, c2x, c2y, ex, ey); - - currentAngle = nextAngle; - currentX = ex; - currentY = ey; - } -} - -PathData PathData::FromSVGString(const std::string& d) { - PathData path; - if (d.empty()) { - return path; - } - - const char* ptr = d.c_str(); - const char* end = ptr + d.length(); - - float currentX = 0; - float currentY = 0; - float startX = 0; - float startY = 0; - float lastControlX = 0; - float lastControlY = 0; - char lastCommand = 0; - - while (ptr < end) { - SkipWhitespaceAndCommas(ptr, end); - if (ptr >= end) { - break; - } - - char command = *ptr; - if (std::isalpha(command)) { - ++ptr; - } else { - command = lastCommand; - } - - bool isRelative = std::islower(command); - char upperCommand = std::toupper(command); - - switch (upperCommand) { - case 'M': { - float x, y; - if (!ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { - break; - } - if (isRelative) { - x += currentX; - y += currentY; - } - path.moveTo(x, y); - currentX = startX = x; - currentY = startY = y; - lastCommand = isRelative ? 'l' : 'L'; - break; - } - case 'L': { - float x, y; - if (!ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { - break; - } - if (isRelative) { - x += currentX; - y += currentY; - } - path.lineTo(x, y); - currentX = x; - currentY = y; - lastCommand = command; - break; - } - case 'H': { - float x; - if (!ParseNumber(ptr, end, x)) { - break; - } - if (isRelative) { - x += currentX; - } - path.lineTo(x, currentY); - currentX = x; - lastCommand = command; - break; - } - case 'V': { - float y; - if (!ParseNumber(ptr, end, y)) { - break; - } - if (isRelative) { - y += currentY; - } - path.lineTo(currentX, y); - currentY = y; - lastCommand = command; - break; - } - case 'C': { - float x1, y1, x2, y2, x, y; - if (!ParseNumber(ptr, end, x1) || !ParseNumber(ptr, end, y1) || - !ParseNumber(ptr, end, x2) || !ParseNumber(ptr, end, y2) || - !ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { - break; - } - if (isRelative) { - x1 += currentX; - y1 += currentY; - x2 += currentX; - y2 += currentY; - x += currentX; - y += currentY; - } - path.cubicTo(x1, y1, x2, y2, x, y); - lastControlX = x2; - lastControlY = y2; - currentX = x; - currentY = y; - lastCommand = command; - break; - } - case 'S': { - float x2, y2, x, y; - if (!ParseNumber(ptr, end, x2) || !ParseNumber(ptr, end, y2) || - !ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { - break; - } - if (isRelative) { - x2 += currentX; - y2 += currentY; - x += currentX; - y += currentY; - } - float x1 = currentX; - float y1 = currentY; - char lastUpper = std::toupper(lastCommand); - if (lastUpper == 'C' || lastUpper == 'S') { - x1 = 2 * currentX - lastControlX; - y1 = 2 * currentY - lastControlY; - } - path.cubicTo(x1, y1, x2, y2, x, y); - lastControlX = x2; - lastControlY = y2; - currentX = x; - currentY = y; - lastCommand = command; - break; - } - case 'Q': { - float x1, y1, x, y; - if (!ParseNumber(ptr, end, x1) || !ParseNumber(ptr, end, y1) || - !ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { - break; - } - if (isRelative) { - x1 += currentX; - y1 += currentY; - x += currentX; - y += currentY; - } - path.quadTo(x1, y1, x, y); - lastControlX = x1; - lastControlY = y1; - currentX = x; - currentY = y; - lastCommand = command; - break; - } - case 'T': { - float x, y; - if (!ParseNumber(ptr, end, x) || !ParseNumber(ptr, end, y)) { - break; - } - if (isRelative) { - x += currentX; - y += currentY; - } - float x1 = currentX; - float y1 = currentY; - char lastUpper = std::toupper(lastCommand); - if (lastUpper == 'Q' || lastUpper == 'T') { - x1 = 2 * currentX - lastControlX; - y1 = 2 * currentY - lastControlY; - } - path.quadTo(x1, y1, x, y); - lastControlX = x1; - lastControlY = y1; - currentX = x; - currentY = y; - lastCommand = command; - break; - } - case 'A': { - float rx, ry, angle, x, y; - bool largeArc, sweep; - if (!ParseNumber(ptr, end, rx) || !ParseNumber(ptr, end, ry) || - !ParseNumber(ptr, end, angle) || !ParseFlag(ptr, end, largeArc) || - !ParseFlag(ptr, end, sweep) || !ParseNumber(ptr, end, x) || - !ParseNumber(ptr, end, y)) { - break; - } - if (isRelative) { - x += currentX; - y += currentY; - } - ArcToCubics(path, currentX, currentY, rx, ry, angle, largeArc, sweep, x, y); - currentX = x; - currentY = y; - lastCommand = command; - break; - } - case 'Z': { - path.close(); - currentX = startX; - currentY = startY; - lastCommand = command; - break; - } - default: - ++ptr; - break; - } - } - - return path; -} - } // namespace pagx diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index bdf233dd5c..81805048c6 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -508,7 +508,7 @@ std::unique_ptr SVGParserImpl::convertPath( auto path = std::make_unique(); std::string d = getAttribute(element, "d"); if (!d.empty()) { - path->data = PathData::FromSVGString(d); + path->data = PathDataFromSVGString(d); } return path; } @@ -1050,7 +1050,7 @@ Rect SVGParserImpl::getShapeBounds(const std::shared_ptr& element) { if (tag == "path") { std::string d = getAttribute(element, "d"); if (!d.empty()) { - auto pathData = PathData::FromSVGString(d); + auto pathData = PathDataFromSVGString(d); return pathData.getBounds(); } } @@ -1687,7 +1687,7 @@ static bool isSameGeometry(const Element* a, const Element* b) { case NodeType::Path: { auto pathA = static_cast(a); auto pathB = static_cast(b); - return pathA->data.toSVGString() == pathB->data.toSVGString(); + return PathDataToSVGString(pathA->data) == PathDataToSVGString(pathB->data); } default: return false; diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 3815906108..c14054accb 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -161,33 +161,38 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { } /** - * Test case: PathData SVG string parsing and round-trip conversion + * Test case: PathData public API for path construction */ -PAG_TEST(PAGXTest, PathDataSVGRoundTrip) { - // Test basic path commands - std::string pathStr = "M10 20 L30 40 H50 V60 C70 80 90 100 110 120 S130 140 150 160 " - "Q170 180 190 200 T210 220 A10 20 30 1 0 230 240 Z"; +PAG_TEST(PAGXTest, PathDataConstruction) { + // Test basic path commands using public API + pagx::PathData pathData; + pathData.moveTo(10, 20); + pathData.lineTo(30, 40); + pathData.lineTo(50, 60); + pathData.cubicTo(70, 80, 90, 100, 110, 120); + pathData.quadTo(130, 140, 150, 160); + pathData.close(); - auto pathData = pagx::PathData::FromSVGString(pathStr); EXPECT_GT(pathData.verbs().size(), 0u); EXPECT_GT(pathData.countPoints(), 0u); + EXPECT_EQ(pathData.verbs().size(), 6u); // M, L, L, C, Q, Z - // Verify round-trip conversion - std::string outputStr = pathData.toSVGString(); - EXPECT_FALSE(outputStr.empty()); - - // Parse the output string and verify it produces the same structure - auto pathData2 = pagx::PathData::FromSVGString(outputStr); - EXPECT_EQ(pathData.verbs().size(), pathData2.verbs().size()); + // Test bounds calculation + auto bounds = pathData.getBounds(); + EXPECT_FALSE(pathData.isEmpty()); + EXPECT_GT(bounds.width, 0.0f); } /** * Test case: PathData forEach iteration */ PAG_TEST(PAGXTest, PathDataForEach) { - std::string pathStr = "M0 0 L100 0 L100 100 L0 100 Z"; - - auto pathData = pagx::PathData::FromSVGString(pathStr); + pagx::PathData pathData; + pathData.moveTo(0, 0); + pathData.lineTo(100, 0); + pathData.lineTo(100, 100); + pathData.lineTo(0, 100); + pathData.close(); int verbCount = 0; pathData.forEach([&verbCount](pagx::PathVerb, const float*) { verbCount++; }); @@ -213,7 +218,8 @@ PAG_TEST(PAGXTest, PAGXNodeBasic) { // Test Path creation auto path = std::make_unique(); - path->data = pagx::PathData::FromSVGString("M0 0 L100 100"); + path->data.moveTo(0, 0); + path->data.lineTo(100, 100); EXPECT_EQ(path->nodeType(), pagx::NodeType::Path); EXPECT_GT(path->data.verbs().size(), 0u); From 587b52dcb7ea6b07a9aacc1b5bbd5eb3cb449145 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 09:27:20 +0800 Subject: [PATCH 117/678] Add clip-path support in SVGImporter to fix edge clipping mask issue. --- pagx/src/svg/SVGImporter.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 81805048c6..16b6df2601 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -309,6 +309,24 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrsecond, inheritedStyle); + if (clipLayer) { + layer->mask = "#" + clipLayer->id; + // SVG clip-path uses alpha (shape outline) for clipping. + layer->maskType = MaskType::Alpha; + // Add clip layer as invisible layer to the document. + _maskLayers.push_back(std::move(clipLayer)); + } + } + } + // Handle filter attribute. std::string filterAttr = getAttribute(element, "filter"); if (!filterAttr.empty() && filterAttr != "none") { @@ -1994,8 +2012,7 @@ std::unique_ptr SVGParserImpl::getColorSourceForRef(const std::stri pattern->image = src->image; pattern->tileModeX = src->tileModeX; pattern->tileModeY = src->tileModeY; - pattern->minFilterMode = src->minFilterMode; - pattern->magFilterMode = src->magFilterMode; + pattern->filterMode = src->filterMode; pattern->mipmapMode = src->mipmapMode; pattern->matrix = src->matrix; return pattern; From e6b4dc353e7e8365be9de703d1e53e3e3f113709 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 09:28:37 +0800 Subject: [PATCH 118/678] refactor(ImagePattern): simplify sampling properties from 3 to 2 Combine minFilterMode and magFilterMode into a single filterMode property based on the analysis that min/mag filters are rarely set to different values in practice (similar to CSS image-rendering and CALayer's approach). - ImagePattern.h: replace minFilterMode/magFilterMode with filterMode - PAGXImporter.cpp: parse single filterMode attribute - PAGXExporter.cpp: export single filterMode attribute - pagx_spec.md: update documentation for new property structure --- pagx/docs/pagx_spec.md | 3 +-- pagx/include/pagx/nodes/ImagePattern.h | 9 ++------- pagx/src/PAGXExporter.cpp | 7 ++----- pagx/src/PAGXImporter.cpp | 3 +-- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index f15aadc590..42e23e71df 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -428,8 +428,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | `image` | idref | (必填) | 图片引用 "@id" | | `tileModeX` | TileMode | clamp | X 方向平铺模式 | | `tileModeY` | TileMode | clamp | Y 方向平铺模式 | -| `minFilterMode` | FilterMode | linear | 缩小滤镜模式 | -| `magFilterMode` | FilterMode | linear | 放大滤镜模式 | +| `filterMode` | FilterMode | linear | 纹理滤镜模式 | | `mipmapMode` | MipmapMode | linear | 多级渐远纹理模式 | | `matrix` | string | 单位矩阵 | 变换矩阵 | diff --git a/pagx/include/pagx/nodes/ImagePattern.h b/pagx/include/pagx/nodes/ImagePattern.h index 5b5a951390..0de0bb962d 100644 --- a/pagx/include/pagx/nodes/ImagePattern.h +++ b/pagx/include/pagx/nodes/ImagePattern.h @@ -48,14 +48,9 @@ class ImagePattern : public ColorSource { TileMode tileModeY = TileMode::Clamp; /** - * The filter mode for minification (when the image is scaled down). The default value is Linear. + * The filter mode for texture sampling. The default value is Linear. */ - FilterMode minFilterMode = FilterMode::Linear; - - /** - * The filter mode for magnification (when the image is scaled up). The default value is Linear. - */ - FilterMode magFilterMode = FilterMode::Linear; + FilterMode filterMode = FilterMode::Linear; /** * The mipmap mode for texture sampling. The default value is Linear. diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index f28036d1e8..573f98dec4 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -341,11 +341,8 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { if (pattern->tileModeY != TileMode::Clamp) { xml.addAttribute("tileModeY", TileModeToString(pattern->tileModeY)); } - if (pattern->minFilterMode != FilterMode::Linear) { - xml.addAttribute("minFilterMode", FilterModeToString(pattern->minFilterMode)); - } - if (pattern->magFilterMode != FilterMode::Linear) { - xml.addAttribute("magFilterMode", FilterModeToString(pattern->magFilterMode)); + if (pattern->filterMode != FilterMode::Linear) { + xml.addAttribute("filterMode", FilterModeToString(pattern->filterMode)); } if (pattern->mipmapMode != MipmapMode::Linear) { xml.addAttribute("mipmapMode", MipmapModeToString(pattern->mipmapMode)); diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 43d6b07264..3afcab3a08 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -875,8 +875,7 @@ std::unique_ptr PAGXImporterImpl::parseImagePattern(const XMLNode* pattern->image = getAttribute(node, "image"); pattern->tileModeX = TileModeFromString(getAttribute(node, "tileModeX", "clamp")); pattern->tileModeY = TileModeFromString(getAttribute(node, "tileModeY", "clamp")); - pattern->minFilterMode = FilterModeFromString(getAttribute(node, "minFilterMode", "linear")); - pattern->magFilterMode = FilterModeFromString(getAttribute(node, "magFilterMode", "linear")); + pattern->filterMode = FilterModeFromString(getAttribute(node, "filterMode", "linear")); pattern->mipmapMode = MipmapModeFromString(getAttribute(node, "mipmapMode", "linear")); auto matrixStr = getAttribute(node, "matrix"); if (!matrixStr.empty()) { From 9120aadfcb47eaad884c53e7baa71d3d77471ff0 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 09:32:11 +0800 Subject: [PATCH 119/678] Add transform support for mask and clipPath child elements in SVGImporter. --- pagx/src/svg/SVGImporter.cpp | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 16b6df2601..d839bc519b 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -1811,13 +1811,39 @@ std::unique_ptr SVGParserImpl::convertMaskElement( if (child->name == "rect" || child->name == "circle" || child->name == "ellipse" || child->name == "path" || child->name == "polygon" || child->name == "polyline") { InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); - convertChildren(child, maskLayer->contents, inheritedStyle); + std::string transformStr = getAttribute(child, "transform"); + if (!transformStr.empty()) { + // If child has transform, wrap it in a sub-layer with the matrix. + auto subLayer = std::make_unique(); + subLayer->matrix = parseTransform(transformStr); + convertChildren(child, subLayer->contents, inheritedStyle); + maskLayer->children.push_back(std::move(subLayer)); + } else { + convertChildren(child, maskLayer->contents, inheritedStyle); + } } else if (child->name == "g") { // Handle group inside mask. InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); + std::string groupTransform = getAttribute(child, "transform"); auto groupChild = child->getFirstChild(); while (groupChild) { - convertChildren(groupChild, maskLayer->contents, inheritedStyle); + std::string childTransform = getAttribute(groupChild, "transform"); + // Combine group transform and child transform if needed. + if (!groupTransform.empty() || !childTransform.empty()) { + auto subLayer = std::make_unique(); + Matrix combinedMatrix = Matrix::Identity(); + if (!groupTransform.empty()) { + combinedMatrix = parseTransform(groupTransform); + } + if (!childTransform.empty()) { + combinedMatrix = combinedMatrix * parseTransform(childTransform); + } + subLayer->matrix = combinedMatrix; + convertChildren(groupChild, subLayer->contents, inheritedStyle); + maskLayer->children.push_back(std::move(subLayer)); + } else { + convertChildren(groupChild, maskLayer->contents, inheritedStyle); + } groupChild = groupChild->getNextSibling(); } } From a98f34d0e3c77683a387dd20f7fab586366fb43e Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 09:35:17 +0800 Subject: [PATCH 120/678] Collect inline mask and clipPath elements to defs for proper reference resolution. --- pagx/src/svg/SVGImporter.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index d839bc519b..be95318272 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -1886,6 +1886,13 @@ void SVGParserImpl::collectAllIds(const std::shared_ptr& node) { auto [found, id] = node->findAttribute("id"); if (found && !id.empty()) { _existingIds.insert(id); + // Also collect referenceable elements (mask, clipPath, filter, etc.) to _defs, + // even if they are defined inline (not inside ). + const auto& name = node->name; + if (name == "mask" || name == "clipPath" || name == "filter" || name == "linearGradient" || + name == "radialGradient" || name == "pattern") { + _defs[id] = node; + } } // Recursively collect from children. From 04f384ad2d0d6c0a7e2b5103baa487e6662623e2 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 09:36:07 +0800 Subject: [PATCH 121/678] Fix Color struct comments to remove misleading wide gamut range description. --- pagx/include/pagx/nodes/Color.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pagx/include/pagx/nodes/Color.h b/pagx/include/pagx/nodes/Color.h index 04d7a7543b..300a44c485 100644 --- a/pagx/include/pagx/nodes/Color.h +++ b/pagx/include/pagx/nodes/Color.h @@ -24,22 +24,20 @@ namespace pagx { /** * An RGBA color with floating-point components and color space. - * For sRGB colors, components are typically in [0, 1]. - * For wide gamut colors (Display P3), components may exceed [0, 1]. */ struct Color { /** - * Red component, typically in [0, 1] for sRGB, may exceed for wide gamut. + * Red component in [0, 1] range. */ float red = 0; /** - * Green component, typically in [0, 1] for sRGB, may exceed for wide gamut. + * Green component in [0, 1] range. */ float green = 0; /** - * Blue component, typically in [0, 1] for sRGB, may exceed for wide gamut. + * Blue component in [0, 1] range. */ float blue = 0; From e0d181a7fdcc2945fa246f4620529320e3102ae1 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 09:38:01 +0800 Subject: [PATCH 122/678] Fix mask element style inheritance to correctly apply fill color from mask element. --- pagx/src/svg/SVGImporter.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index be95318272..f995119aa5 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -1805,12 +1805,15 @@ std::unique_ptr SVGParserImpl::convertMaskElement( maskLayer->name = maskLayer->id; maskLayer->visible = false; + // Compute inherited style from the mask element itself (it may have fill="white" etc.). + InheritedStyle maskStyle = computeInheritedStyle(maskElement, parentStyle); + // Parse mask contents. auto child = maskElement->getFirstChild(); while (child) { if (child->name == "rect" || child->name == "circle" || child->name == "ellipse" || child->name == "path" || child->name == "polygon" || child->name == "polyline") { - InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); + InheritedStyle inheritedStyle = computeInheritedStyle(child, maskStyle); std::string transformStr = getAttribute(child, "transform"); if (!transformStr.empty()) { // If child has transform, wrap it in a sub-layer with the matrix. @@ -1823,7 +1826,7 @@ std::unique_ptr SVGParserImpl::convertMaskElement( } } else if (child->name == "g") { // Handle group inside mask. - InheritedStyle inheritedStyle = computeInheritedStyle(child, parentStyle); + InheritedStyle inheritedStyle = computeInheritedStyle(child, maskStyle); std::string groupTransform = getAttribute(child, "transform"); auto groupChild = child->getFirstChild(); while (groupChild) { From df98de6d484bd5253d0d4c75a6fdcc6e8dfa916f Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 09:48:58 +0800 Subject: [PATCH 123/678] Remove redundant ColorSourceType enum and use NodeType for color source type checking. --- pagx/include/pagx/nodes/ColorSource.h | 18 ------------------ pagx/include/pagx/nodes/ConicGradient.h | 4 ---- pagx/include/pagx/nodes/DiamondGradient.h | 4 ---- pagx/include/pagx/nodes/ImagePattern.h | 4 ---- pagx/include/pagx/nodes/LinearGradient.h | 4 ---- pagx/include/pagx/nodes/RadialGradient.h | 4 ---- pagx/include/pagx/nodes/SolidColor.h | 4 ---- pagx/src/PAGXExporter.cpp | 16 +++++++++------- pagx/src/PAGXStringUtils.cpp | 19 ------------------- pagx/src/PAGXStringUtils.h | 5 ----- pagx/src/tgfx/LayerBuilder.cpp | 10 +++++----- test/src/PAGXTest.cpp | 6 +++--- 12 files changed, 17 insertions(+), 81 deletions(-) diff --git a/pagx/include/pagx/nodes/ColorSource.h b/pagx/include/pagx/nodes/ColorSource.h index 5015b6abcd..5eb95062a5 100644 --- a/pagx/include/pagx/nodes/ColorSource.h +++ b/pagx/include/pagx/nodes/ColorSource.h @@ -22,30 +22,12 @@ namespace pagx { -/** - * Color source types. - */ -enum class ColorSourceType { - SolidColor, - LinearGradient, - RadialGradient, - ConicGradient, - DiamondGradient, - ImagePattern -}; - /** * Base class for color sources (SolidColor, gradients, ImagePattern). * ColorSource can be used both inline in painters and as standalone resources in defs. * Inherits from Node so it can be stored in resources and referenced by ID. */ class ColorSource : public Node { - public: - /** - * Returns the color source type of this color source. - */ - virtual ColorSourceType type() const = 0; - protected: ColorSource() = default; }; diff --git a/pagx/include/pagx/nodes/ConicGradient.h b/pagx/include/pagx/nodes/ConicGradient.h index 68ad825082..fa99dc9eb6 100644 --- a/pagx/include/pagx/nodes/ConicGradient.h +++ b/pagx/include/pagx/nodes/ConicGradient.h @@ -56,10 +56,6 @@ class ConicGradient : public ColorSource { */ std::vector colorStops = {}; - ColorSourceType type() const override { - return ColorSourceType::ConicGradient; - } - NodeType nodeType() const override { return NodeType::ConicGradient; } diff --git a/pagx/include/pagx/nodes/DiamondGradient.h b/pagx/include/pagx/nodes/DiamondGradient.h index 05eb353c84..66c8e4110d 100644 --- a/pagx/include/pagx/nodes/DiamondGradient.h +++ b/pagx/include/pagx/nodes/DiamondGradient.h @@ -51,10 +51,6 @@ class DiamondGradient : public ColorSource { */ std::vector colorStops = {}; - ColorSourceType type() const override { - return ColorSourceType::DiamondGradient; - } - NodeType nodeType() const override { return NodeType::DiamondGradient; } diff --git a/pagx/include/pagx/nodes/ImagePattern.h b/pagx/include/pagx/nodes/ImagePattern.h index 0de0bb962d..f9f9755641 100644 --- a/pagx/include/pagx/nodes/ImagePattern.h +++ b/pagx/include/pagx/nodes/ImagePattern.h @@ -62,10 +62,6 @@ class ImagePattern : public ColorSource { */ Matrix matrix = {}; - ColorSourceType type() const override { - return ColorSourceType::ImagePattern; - } - NodeType nodeType() const override { return NodeType::ImagePattern; } diff --git a/pagx/include/pagx/nodes/LinearGradient.h b/pagx/include/pagx/nodes/LinearGradient.h index c2b60239fb..6d1011c281 100644 --- a/pagx/include/pagx/nodes/LinearGradient.h +++ b/pagx/include/pagx/nodes/LinearGradient.h @@ -51,10 +51,6 @@ class LinearGradient : public ColorSource { */ std::vector colorStops = {}; - ColorSourceType type() const override { - return ColorSourceType::LinearGradient; - } - NodeType nodeType() const override { return NodeType::LinearGradient; } diff --git a/pagx/include/pagx/nodes/RadialGradient.h b/pagx/include/pagx/nodes/RadialGradient.h index dc0fe15440..0ed140f3f9 100644 --- a/pagx/include/pagx/nodes/RadialGradient.h +++ b/pagx/include/pagx/nodes/RadialGradient.h @@ -51,10 +51,6 @@ class RadialGradient : public ColorSource { */ std::vector colorStops = {}; - ColorSourceType type() const override { - return ColorSourceType::RadialGradient; - } - NodeType nodeType() const override { return NodeType::RadialGradient; } diff --git a/pagx/include/pagx/nodes/SolidColor.h b/pagx/include/pagx/nodes/SolidColor.h index 848bf0008c..4f870fab0a 100644 --- a/pagx/include/pagx/nodes/SolidColor.h +++ b/pagx/include/pagx/nodes/SolidColor.h @@ -33,10 +33,6 @@ class SolidColor : public ColorSource { */ Color color = {}; - ColorSourceType type() const override { - return ColorSourceType::SolidColor; - } - NodeType nodeType() const override { return NodeType::SolidColor; } diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index 573f98dec4..4d09f31436 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -240,8 +240,8 @@ static void writeColorStops(XMLBuilder& xml, const std::vector& stops } static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { - switch (node->type()) { - case ColorSourceType::SolidColor: { + switch (node->nodeType()) { + case NodeType::SolidColor: { auto solid = static_cast(node); xml.openElement("SolidColor"); xml.addAttribute("id", solid->id); @@ -249,7 +249,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { xml.closeElementSelfClosing(); break; } - case ColorSourceType::LinearGradient: { + case NodeType::LinearGradient: { auto grad = static_cast(node); xml.openElement("LinearGradient"); xml.addAttribute("id", grad->id); @@ -269,7 +269,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { } break; } - case ColorSourceType::RadialGradient: { + case NodeType::RadialGradient: { auto grad = static_cast(node); xml.openElement("RadialGradient"); xml.addAttribute("id", grad->id); @@ -289,7 +289,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { } break; } - case ColorSourceType::ConicGradient: { + case NodeType::ConicGradient: { auto grad = static_cast(node); xml.openElement("ConicGradient"); xml.addAttribute("id", grad->id); @@ -310,7 +310,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { } break; } - case ColorSourceType::DiamondGradient: { + case NodeType::DiamondGradient: { auto grad = static_cast(node); xml.openElement("DiamondGradient"); xml.addAttribute("id", grad->id); @@ -330,7 +330,7 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { } break; } - case ColorSourceType::ImagePattern: { + case NodeType::ImagePattern: { auto pattern = static_cast(node); xml.openElement("ImagePattern"); xml.addAttribute("id", pattern->id); @@ -353,6 +353,8 @@ static void writeColorSource(XMLBuilder& xml, const ColorSource* node) { xml.closeElementSelfClosing(); break; } + default: + break; } } diff --git a/pagx/src/PAGXStringUtils.cpp b/pagx/src/PAGXStringUtils.cpp index 341e6ba809..b8521d7357 100644 --- a/pagx/src/PAGXStringUtils.cpp +++ b/pagx/src/PAGXStringUtils.cpp @@ -126,25 +126,6 @@ const char* NodeTypeName(NodeType type) { } } -const char* ColorSourceTypeName(ColorSourceType type) { - switch (type) { - case ColorSourceType::SolidColor: - return "SolidColor"; - case ColorSourceType::LinearGradient: - return "LinearGradient"; - case ColorSourceType::RadialGradient: - return "RadialGradient"; - case ColorSourceType::ConicGradient: - return "ConicGradient"; - case ColorSourceType::DiamondGradient: - return "DiamondGradient"; - case ColorSourceType::ImagePattern: - return "ImagePattern"; - default: - return "Unknown"; - } -} - //============================================================================== // Enum string conversions //============================================================================== diff --git a/pagx/src/PAGXStringUtils.h b/pagx/src/PAGXStringUtils.h index 27146a667f..5f6b95c6b6 100644 --- a/pagx/src/PAGXStringUtils.h +++ b/pagx/src/PAGXStringUtils.h @@ -53,11 +53,6 @@ namespace pagx { //============================================================================== const char* NodeTypeName(NodeType type); -//============================================================================== -// ColorSource types -//============================================================================== -const char* ColorSourceTypeName(ColorSourceType type); - //============================================================================== // BlendMode //============================================================================== diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 38ec4b938f..f1fe3a3fe2 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -521,20 +521,20 @@ class LayerBuilderImpl { return nullptr; } - switch (node->type()) { - case ColorSourceType::SolidColor: { + switch (node->nodeType()) { + case NodeType::SolidColor: { auto solid = static_cast(node); return tgfx::SolidColor::Make(ToTGFX(solid->color)); } - case ColorSourceType::LinearGradient: { + case NodeType::LinearGradient: { auto grad = static_cast(node); return convertLinearGradient(grad); } - case ColorSourceType::RadialGradient: { + case NodeType::RadialGradient: { auto grad = static_cast(node); return convertRadialGradient(grad); } - case ColorSourceType::ImagePattern: { + case NodeType::ImagePattern: { auto pattern = static_cast(node); return convertImagePattern(pattern); } diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index c14054accb..503b5a6a93 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -387,7 +387,7 @@ PAG_TEST(PAGXTest, ColorSources) { // Test SolidColor auto solid = std::make_unique(); solid->color = {1.0f, 0.0f, 0.0f, 1.0f}; // Red - EXPECT_EQ(solid->type(), pagx::ColorSourceType::SolidColor); + EXPECT_EQ(solid->nodeType(), pagx::NodeType::SolidColor); EXPECT_FLOAT_EQ(solid->color.red, 1.0f); // Test LinearGradient @@ -408,7 +408,7 @@ PAG_TEST(PAGXTest, ColorSources) { linear->colorStops.push_back(stop1); linear->colorStops.push_back(stop2); - EXPECT_EQ(linear->type(), pagx::ColorSourceType::LinearGradient); + EXPECT_EQ(linear->nodeType(), pagx::NodeType::LinearGradient); EXPECT_EQ(linear->colorStops.size(), 2u); // Test RadialGradient @@ -418,7 +418,7 @@ PAG_TEST(PAGXTest, ColorSources) { radial->radius = 50; radial->colorStops = linear->colorStops; - EXPECT_EQ(radial->type(), pagx::ColorSourceType::RadialGradient); + EXPECT_EQ(radial->nodeType(), pagx::NodeType::RadialGradient); } /** From 96c8f37cbcfe131af043c17397c77336d6ccee7e Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 09:53:08 +0800 Subject: [PATCH 124/678] Distinguish background blur from regular blur in SVG filter conversion. --- pagx/src/svg/SVGImporter.cpp | 27 +++++++++++++++++++++------ pagx/src/svg/SVGParserInternal.h | 4 +++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index f995119aa5..f5abcd342d 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -333,7 +333,7 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrsecond, layer->filters); + convertFilterElement(filterIt->second, layer->filters, layer->styles); } } @@ -1858,12 +1858,13 @@ std::unique_ptr SVGParserImpl::convertMaskElement( void SVGParserImpl::convertFilterElement( const std::shared_ptr& filterElement, - std::vector>& filters) { + std::vector>& filters, + std::vector>& styles) { // Parse filter children to find effect elements. auto child = filterElement->getFirstChild(); while (child) { if (child->name == "feGaussianBlur") { - auto blurFilter = std::make_unique(); + std::string inAttr = getAttribute(child, "in"); std::string stdDeviation = getAttribute(child, "stdDeviation", "0"); // stdDeviation can be one value (both X and Y) or two values (X Y). std::istringstream iss(stdDeviation); @@ -1872,9 +1873,23 @@ void SVGParserImpl::convertFilterElement( if (!(iss >> devY)) { devY = devX; } - blurFilter->blurrinessX = devX; - blurFilter->blurrinessY = devY; - filters.push_back(std::move(blurFilter)); + + // Check if this is a background blur (in="BackgroundImageFix" or similar background input). + if (inAttr == "BackgroundImageFix" || inAttr == "BackgroundImage") { + // This is a background blur effect, use BackgroundBlurStyle. + auto bgBlur = std::make_unique(); + bgBlur->blurrinessX = devX; + bgBlur->blurrinessY = devY; + styles.push_back(std::move(bgBlur)); + } else if (inAttr.empty() || inAttr == "SourceGraphic" || inAttr == "SourceAlpha") { + // Regular blur filter on the element itself. + auto blurFilter = std::make_unique(); + blurFilter->blurrinessX = devX; + blurFilter->blurrinessY = devY; + filters.push_back(std::move(blurFilter)); + } + // Other "in" values (like result names from previous filter stages) are ignored + // as they are intermediate steps in complex filter chains. } child = child->getNextSibling(); } diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 1b4b5460aa..005e527e07 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -23,6 +23,7 @@ #include #include #include +#include "pagx/nodes/BackgroundBlurStyle.h" #include "pagx/nodes/BlurFilter.h" #include "pagx/PAGXDocument.h" #include "pagx/nodes/Ellipse.h" @@ -98,7 +99,8 @@ class SVGParserImpl { std::unique_ptr convertMaskElement(const std::shared_ptr& maskElement, const InheritedStyle& parentStyle); void convertFilterElement(const std::shared_ptr& filterElement, - std::vector>& filters); + std::vector>& filters, + std::vector>& styles); void addFillStroke(const std::shared_ptr& element, std::vector>& contents, From bec3589a58e8c5aacea83f3d547898299ee1b8c3 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 09:54:37 +0800 Subject: [PATCH 125/678] Fix comment line wrapping in pagx module to use 100-character limit. --- pagx/include/pagx/nodes/BlendMode.h | 16 ++++++++-------- pagx/include/pagx/nodes/Group.h | 6 +++--- pagx/include/pagx/nodes/Layer.h | 4 ++-- pagx/include/pagx/nodes/Node.h | 5 ++--- pagx/include/pagx/nodes/Polystar.h | 3 +-- pagx/include/pagx/nodes/RangeSelector.h | 4 ++-- 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/pagx/include/pagx/nodes/BlendMode.h b/pagx/include/pagx/nodes/BlendMode.h index b0bf542fc3..55cdbcebf8 100644 --- a/pagx/include/pagx/nodes/BlendMode.h +++ b/pagx/include/pagx/nodes/BlendMode.h @@ -73,23 +73,23 @@ enum class BlendMode { */ Exclusion, /** - * Creates a result color with the hue of the source and the saturation and luminosity of the - * destination. + * Creates a result color with the hue of the source and the saturation and luminosity of + * the destination. */ Hue, /** - * Creates a result color with the saturation of the source and the hue and luminosity of the - * destination. + * Creates a result color with the saturation of the source and the hue and luminosity of + * the destination. */ Saturation, /** - * Creates a result color with the hue and saturation of the source and the luminosity of the - * destination. + * Creates a result color with the hue and saturation of the source and the luminosity of + * the destination. */ Color, /** - * Creates a result color with the luminosity of the source and the hue and saturation of the - * destination. + * Creates a result color with the luminosity of the source and the hue and saturation of + * the destination. */ Luminosity, /** diff --git a/pagx/include/pagx/nodes/Group.h b/pagx/include/pagx/nodes/Group.h index 9ddf078b10..e8fe749cc8 100644 --- a/pagx/include/pagx/nodes/Group.h +++ b/pagx/include/pagx/nodes/Group.h @@ -26,9 +26,9 @@ namespace pagx { /** - * Group is a container that groups multiple vector elements with its own transform. It can contain - * shapes, painters, modifiers, and nested groups, allowing for hierarchical organization of - * content. + * Group is a container that groups multiple vector elements with its own transform. It can + * contain shapes, painters, modifiers, and nested groups, allowing for hierarchical organization + * of content. */ class Group : public Element { public: diff --git a/pagx/include/pagx/nodes/Layer.h b/pagx/include/pagx/nodes/Layer.h index 0e111f53a4..4cbb1b549e 100644 --- a/pagx/include/pagx/nodes/Layer.h +++ b/pagx/include/pagx/nodes/Layer.h @@ -137,8 +137,8 @@ class Layer : public Node { std::string mask = {}; /** - * The type of masking to apply (Alpha, Luminosity, InvertedAlpha, or InvertedLuminosity). The - * default value is Alpha. + * The type of masking to apply (Alpha, Luminosity, InvertedAlpha, or InvertedLuminosity). + * The default value is Alpha. */ MaskType maskType = MaskType::Alpha; diff --git a/pagx/include/pagx/nodes/Node.h b/pagx/include/pagx/nodes/Node.h index df7f8a4080..0225a1e6c9 100644 --- a/pagx/include/pagx/nodes/Node.h +++ b/pagx/include/pagx/nodes/Node.h @@ -24,9 +24,8 @@ namespace pagx { /** - * NodeType enumerates all types of nodes that can be stored in a PAGX document. - * This includes resources (Image, Composition, ColorSources) and elements (shapes, painters, - * modifiers, etc.). + * NodeType enumerates all types of nodes that can be stored in a PAGX document. This includes + * resources (Image, Composition, ColorSources) and elements (shapes, painters, modifiers, etc.). */ enum class NodeType { // Resources diff --git a/pagx/include/pagx/nodes/Polystar.h b/pagx/include/pagx/nodes/Polystar.h index 76e648e1f3..a3d5fc1a45 100644 --- a/pagx/include/pagx/nodes/Polystar.h +++ b/pagx/include/pagx/nodes/Polystar.h @@ -63,8 +63,7 @@ class Polystar : public Element { float outerRadius = 100; /** - * The inner radius of the polystar. Only applies when type is Star. The default value - * is 50. + * The inner radius of the polystar. Only applies when type is Star. The default value is 50. */ float innerRadius = 50; diff --git a/pagx/include/pagx/nodes/RangeSelector.h b/pagx/include/pagx/nodes/RangeSelector.h index f54ce9fada..9e3bfcec6a 100644 --- a/pagx/include/pagx/nodes/RangeSelector.h +++ b/pagx/include/pagx/nodes/RangeSelector.h @@ -104,8 +104,8 @@ enum class SelectorMode { class RangeSelector : public TextSelector { public: /** - * The starting position of the selection range, in units defined by the unit property. The - * default value is 0. + * The starting position of the selection range, in units defined by the unit property. + * The default value is 0. */ float start = 0; From 947f57dc739fda466b499c8708a5184771dee443 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 10:03:54 +0800 Subject: [PATCH 126/678] Move blendMode property from LayerStyle subclasses to the base class. --- pagx/include/pagx/nodes/BackgroundBlurStyle.h | 6 ------ pagx/include/pagx/nodes/DropShadowStyle.h | 6 ------ pagx/include/pagx/nodes/InnerShadowStyle.h | 6 ------ pagx/include/pagx/nodes/LayerStyle.h | 6 ++++++ 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/pagx/include/pagx/nodes/BackgroundBlurStyle.h b/pagx/include/pagx/nodes/BackgroundBlurStyle.h index 1c584ca803..f34d6bdc9c 100644 --- a/pagx/include/pagx/nodes/BackgroundBlurStyle.h +++ b/pagx/include/pagx/nodes/BackgroundBlurStyle.h @@ -19,7 +19,6 @@ #pragma once #include "pagx/nodes/LayerStyle.h" -#include "pagx/nodes/BlendMode.h" #include "pagx/nodes/TileMode.h" namespace pagx { @@ -44,11 +43,6 @@ class BackgroundBlurStyle : public LayerStyle { */ TileMode tileMode = TileMode::Mirror; - /** - * The blend mode used when compositing the blur. The default value is Normal. - */ - BlendMode blendMode = BlendMode::Normal; - NodeType nodeType() const override { return NodeType::BackgroundBlurStyle; } diff --git a/pagx/include/pagx/nodes/DropShadowStyle.h b/pagx/include/pagx/nodes/DropShadowStyle.h index e884a50f3a..beeaa7c076 100644 --- a/pagx/include/pagx/nodes/DropShadowStyle.h +++ b/pagx/include/pagx/nodes/DropShadowStyle.h @@ -19,7 +19,6 @@ #pragma once #include "pagx/nodes/LayerStyle.h" -#include "pagx/nodes/BlendMode.h" #include "pagx/nodes/Color.h" namespace pagx { @@ -59,11 +58,6 @@ class DropShadowStyle : public LayerStyle { */ bool showBehindLayer = true; - /** - * The blend mode used when compositing the shadow. The default value is Normal. - */ - BlendMode blendMode = BlendMode::Normal; - NodeType nodeType() const override { return NodeType::DropShadowStyle; } diff --git a/pagx/include/pagx/nodes/InnerShadowStyle.h b/pagx/include/pagx/nodes/InnerShadowStyle.h index d99b02676a..81324a3458 100644 --- a/pagx/include/pagx/nodes/InnerShadowStyle.h +++ b/pagx/include/pagx/nodes/InnerShadowStyle.h @@ -19,7 +19,6 @@ #pragma once #include "pagx/nodes/LayerStyle.h" -#include "pagx/nodes/BlendMode.h" #include "pagx/nodes/Color.h" namespace pagx { @@ -54,11 +53,6 @@ class InnerShadowStyle : public LayerStyle { */ Color color = {}; - /** - * The blend mode used when compositing the shadow. The default value is Normal. - */ - BlendMode blendMode = BlendMode::Normal; - NodeType nodeType() const override { return NodeType::InnerShadowStyle; } diff --git a/pagx/include/pagx/nodes/LayerStyle.h b/pagx/include/pagx/nodes/LayerStyle.h index ce7c6ecf42..f43090c94a 100644 --- a/pagx/include/pagx/nodes/LayerStyle.h +++ b/pagx/include/pagx/nodes/LayerStyle.h @@ -18,6 +18,7 @@ #pragma once +#include "pagx/nodes/BlendMode.h" #include "pagx/nodes/Node.h" namespace pagx { @@ -27,6 +28,11 @@ namespace pagx { */ class LayerStyle : public Node { public: + /** + * The blend mode used when compositing the style. The default value is Normal. + */ + BlendMode blendMode = BlendMode::Normal; + ~LayerStyle() override = default; protected: From 0c3a5c7e02559995408d8307e34850d2001e15b3 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 10:06:41 +0800 Subject: [PATCH 127/678] Fix SVG to PAGX blur filter conversion causing entire screen to be blurred. --- pagx/src/svg/SVGImporter.cpp | 25 ++++++++++++++----------- pagx/src/svg/SVGParserInternal.h | 1 - 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index f5abcd342d..3cc6f97ddd 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -1861,8 +1861,13 @@ void SVGParserImpl::convertFilterElement( std::vector>& filters, std::vector>& styles) { // Parse filter children to find effect elements. + // In SVG filter chains, only convert feGaussianBlur to BlurFilter when it directly operates on + // SourceGraphic. Other cases (SourceAlpha, BackgroundImageFix, or chained from previous + // primitives) are typically parts of complex effects like drop shadows and should be skipped. auto child = filterElement->getFirstChild(); + bool isFirstPrimitive = true; while (child) { + bool isFilterPrimitive = !child->name.empty() && child->name.substr(0, 2) == "fe"; if (child->name == "feGaussianBlur") { std::string inAttr = getAttribute(child, "in"); std::string stdDeviation = getAttribute(child, "stdDeviation", "0"); @@ -1874,22 +1879,20 @@ void SVGParserImpl::convertFilterElement( devY = devX; } - // Check if this is a background blur (in="BackgroundImageFix" or similar background input). - if (inAttr == "BackgroundImageFix" || inAttr == "BackgroundImage") { - // This is a background blur effect, use BackgroundBlurStyle. - auto bgBlur = std::make_unique(); - bgBlur->blurrinessX = devX; - bgBlur->blurrinessY = devY; - styles.push_back(std::move(bgBlur)); - } else if (inAttr.empty() || inAttr == "SourceGraphic" || inAttr == "SourceAlpha") { - // Regular blur filter on the element itself. + // Only convert to BlurFilter when explicitly operating on SourceGraphic, or when it's + // the first filter primitive with no "in" attribute (defaults to SourceGraphic). + // Skip SourceAlpha (used in drop shadows), BackgroundImageFix (Figma pattern), and + // feGaussianBlur that chains from previous primitives (part of complex effects). + bool isSourceGraphic = (inAttr == "SourceGraphic") || (inAttr.empty() && isFirstPrimitive); + if (isSourceGraphic) { auto blurFilter = std::make_unique(); blurFilter->blurrinessX = devX; blurFilter->blurrinessY = devY; filters.push_back(std::move(blurFilter)); } - // Other "in" values (like result names from previous filter stages) are ignored - // as they are intermediate steps in complex filter chains. + } + if (isFilterPrimitive) { + isFirstPrimitive = false; } child = child->getNextSibling(); } diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 005e527e07..59322fec31 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -23,7 +23,6 @@ #include #include #include -#include "pagx/nodes/BackgroundBlurStyle.h" #include "pagx/nodes/BlurFilter.h" #include "pagx/PAGXDocument.h" #include "pagx/nodes/Ellipse.h" From cae9e84eed2c45abe19a09175f2bcfda863d0e62 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 10:10:16 +0800 Subject: [PATCH 128/678] Make ColorStop inherit from Node to support id and data-* attributes. --- pagx/include/pagx/nodes/ColorStop.h | 8 +++++++- pagx/include/pagx/nodes/Node.h | 4 ++++ pagx/src/PAGXStringUtils.cpp | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pagx/include/pagx/nodes/ColorStop.h b/pagx/include/pagx/nodes/ColorStop.h index e7f9051a34..a7ce140328 100644 --- a/pagx/include/pagx/nodes/ColorStop.h +++ b/pagx/include/pagx/nodes/ColorStop.h @@ -19,13 +19,15 @@ #pragma once #include "pagx/nodes/Color.h" +#include "pagx/nodes/Node.h" namespace pagx { /** * A color stop defines a color at a specific position in a gradient. */ -struct ColorStop { +class ColorStop : public Node { + public: /** * The position of this color stop along the gradient, ranging from 0 to 1. */ @@ -35,6 +37,10 @@ struct ColorStop { * The color value at this stop position. */ Color color = {}; + + NodeType nodeType() const override { + return NodeType::ColorStop; + } }; } // namespace pagx diff --git a/pagx/include/pagx/nodes/Node.h b/pagx/include/pagx/nodes/Node.h index 0225a1e6c9..b902939527 100644 --- a/pagx/include/pagx/nodes/Node.h +++ b/pagx/include/pagx/nodes/Node.h @@ -65,6 +65,10 @@ enum class NodeType { * An image pattern color source. */ ImagePattern, + /** + * A color stop in a gradient. + */ + ColorStop, // Layer /** diff --git a/pagx/src/PAGXStringUtils.cpp b/pagx/src/PAGXStringUtils.cpp index b8521d7357..61c274226e 100644 --- a/pagx/src/PAGXStringUtils.cpp +++ b/pagx/src/PAGXStringUtils.cpp @@ -71,6 +71,8 @@ const char* NodeTypeName(NodeType type) { return "DiamondGradient"; case NodeType::ImagePattern: return "ImagePattern"; + case NodeType::ColorStop: + return "ColorStop"; case NodeType::Layer: return "Layer"; case NodeType::DropShadowStyle: From 681f0f45964ddbfccd080fc5162f295c91944531 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 10:18:01 +0800 Subject: [PATCH 129/678] Fix comment formatting and add BackgroundBlurStyle support in LayerBuilder. --- pagx/include/pagx/nodes/BlendMode.h | 12 ++++++------ pagx/include/pagx/nodes/Group.h | 6 +++--- pagx/include/pagx/nodes/Layer.h | 2 +- pagx/include/pagx/nodes/Polystar.h | 4 ++-- pagx/include/pagx/nodes/RangeSelector.h | 6 +++--- pagx/src/tgfx/LayerBuilder.cpp | 6 ++++++ 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/pagx/include/pagx/nodes/BlendMode.h b/pagx/include/pagx/nodes/BlendMode.h index 55cdbcebf8..73439f0b7e 100644 --- a/pagx/include/pagx/nodes/BlendMode.h +++ b/pagx/include/pagx/nodes/BlendMode.h @@ -73,8 +73,8 @@ enum class BlendMode { */ Exclusion, /** - * Creates a result color with the hue of the source and the saturation and luminosity of - * the destination. + * Creates a result color with the hue of the source and the saturation and luminosity of the + * destination. */ Hue, /** @@ -83,13 +83,13 @@ enum class BlendMode { */ Saturation, /** - * Creates a result color with the hue and saturation of the source and the luminosity of - * the destination. + * Creates a result color with the hue and saturation of the source and the luminosity of the + * destination. */ Color, /** - * Creates a result color with the luminosity of the source and the hue and saturation of - * the destination. + * Creates a result color with the luminosity of the source and the hue and saturation of the + * destination. */ Luminosity, /** diff --git a/pagx/include/pagx/nodes/Group.h b/pagx/include/pagx/nodes/Group.h index e8fe749cc8..9ddf078b10 100644 --- a/pagx/include/pagx/nodes/Group.h +++ b/pagx/include/pagx/nodes/Group.h @@ -26,9 +26,9 @@ namespace pagx { /** - * Group is a container that groups multiple vector elements with its own transform. It can - * contain shapes, painters, modifiers, and nested groups, allowing for hierarchical organization - * of content. + * Group is a container that groups multiple vector elements with its own transform. It can contain + * shapes, painters, modifiers, and nested groups, allowing for hierarchical organization of + * content. */ class Group : public Element { public: diff --git a/pagx/include/pagx/nodes/Layer.h b/pagx/include/pagx/nodes/Layer.h index 4cbb1b549e..29e79f3581 100644 --- a/pagx/include/pagx/nodes/Layer.h +++ b/pagx/include/pagx/nodes/Layer.h @@ -102,7 +102,7 @@ class Layer : public Node { bool preserve3D = false; /** - * Whether to apply anti-aliasing to the layer edges. The default value is true. + * Whether to apply antialiasing to the layer edges. The default value is true. */ bool antiAlias = true; diff --git a/pagx/include/pagx/nodes/Polystar.h b/pagx/include/pagx/nodes/Polystar.h index a3d5fc1a45..ce3f8a0adb 100644 --- a/pagx/include/pagx/nodes/Polystar.h +++ b/pagx/include/pagx/nodes/Polystar.h @@ -78,8 +78,8 @@ class Polystar : public Element { float outerRoundness = 0; /** - * The roundness of the inner points, ranging from 0 to 100. Only applies when type is - * Star. The default value is 0. + * The roundness of the inner points, ranging from 0 to 100. Only applies when type is Star. The + * default value is 0. */ float innerRoundness = 0; diff --git a/pagx/include/pagx/nodes/RangeSelector.h b/pagx/include/pagx/nodes/RangeSelector.h index 9e3bfcec6a..09671623cd 100644 --- a/pagx/include/pagx/nodes/RangeSelector.h +++ b/pagx/include/pagx/nodes/RangeSelector.h @@ -97,9 +97,9 @@ enum class SelectorMode { }; /** - * A range selector that defines which characters in a text are affected by a text modifier. - * It provides flexible control over character selection through start/end positions, shapes, - * and randomization. + * A range selector that defines which characters in a text are affected by a text modifier. It + * provides flexible control over character selection through start/end positions, shapes, and + * randomization. */ class RangeSelector : public TextSelector { public: diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index f1fe3a3fe2..b689e95a6f 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -21,6 +21,7 @@ #include #include #include "pagx/PAGXImporter.h" +#include "pagx/nodes/BackgroundBlurStyle.h" #include "pagx/nodes/BlurFilter.h" #include "pagx/nodes/ColorSpace.h" #include "tgfx/core/ColorSpace.h" @@ -63,6 +64,7 @@ #include "tgfx/layers/filters/BlurFilter.h" #include "tgfx/layers/filters/DropShadowFilter.h" #include "tgfx/layers/filters/LayerFilter.h" +#include "tgfx/layers/layerstyles/BackgroundBlurStyle.h" #include "tgfx/layers/layerstyles/DropShadowStyle.h" #include "tgfx/layers/layerstyles/InnerShadowStyle.h" #include "tgfx/layers/vectors/Ellipse.h" @@ -844,6 +846,10 @@ class LayerBuilderImpl { return tgfx::InnerShadowStyle::Make(style->offsetX, style->offsetY, style->blurrinessX, style->blurrinessY, ToTGFX(style->color)); } + case NodeType::BackgroundBlurStyle: { + auto style = static_cast(node); + return tgfx::BackgroundBlurStyle::Make(style->blurrinessX, style->blurrinessY); + } default: return nullptr; } From 2c36b6763ec3a741cadae0fcd538d2a2ddaa782a Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 10:18:20 +0800 Subject: [PATCH 130/678] Add missing DropShadowFilter include in SVGParserInternal. --- pagx/src/svg/SVGParserInternal.h | 1 + 1 file changed, 1 insertion(+) diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 59322fec31..afbdf78a47 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -24,6 +24,7 @@ #include #include #include "pagx/nodes/BlurFilter.h" +#include "pagx/nodes/DropShadowFilter.h" #include "pagx/PAGXDocument.h" #include "pagx/nodes/Ellipse.h" #include "pagx/nodes/Fill.h" From a702f3defe89dc2e12921504adb994aba58d5624 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 10:23:52 +0800 Subject: [PATCH 131/678] Add SVG drop shadow filter conversion to DropShadowFilter in PAGX. --- pagx/src/svg/SVGImporter.cpp | 106 +++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 3cc6f97ddd..55cd8d6a95 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -1860,41 +1860,99 @@ void SVGParserImpl::convertFilterElement( const std::shared_ptr& filterElement, std::vector>& filters, std::vector>& styles) { - // Parse filter children to find effect elements. - // In SVG filter chains, only convert feGaussianBlur to BlurFilter when it directly operates on - // SourceGraphic. Other cases (SourceAlpha, BackgroundImageFix, or chained from previous - // primitives) are typically parts of complex effects like drop shadows and should be skipped. + // Collect all filter primitives for analysis. + std::vector> primitives; auto child = filterElement->getFirstChild(); - bool isFirstPrimitive = true; while (child) { - bool isFilterPrimitive = !child->name.empty() && child->name.substr(0, 2) == "fe"; - if (child->name == "feGaussianBlur") { - std::string inAttr = getAttribute(child, "in"); - std::string stdDeviation = getAttribute(child, "stdDeviation", "0"); - // stdDeviation can be one value (both X and Y) or two values (X Y). - std::istringstream iss(stdDeviation); - float devX = 0, devY = 0; - iss >> devX; - if (!(iss >> devY)) { - devY = devX; + if (!child->name.empty() && child->name.substr(0, 2) == "fe") { + primitives.push_back(child); + } + child = child->getNextSibling(); + } + + // Detect Figma-style drop shadow pattern and extract shadow parameters. + // Pattern: feColorMatrix(in=SourceAlpha) → feOffset → feGaussianBlur → feComposite → feColorMatrix + // This pattern may repeat multiple times for multiple shadows. + size_t i = 0; + while (i < primitives.size()) { + auto& node = primitives[i]; + + // Check for drop shadow pattern starting with feColorMatrix in="SourceAlpha". + if (node->name == "feColorMatrix" && getAttribute(node, "in") == "SourceAlpha") { + // Look for the sequence: feColorMatrix → feOffset → feGaussianBlur → feComposite → feColorMatrix + if (i + 4 < primitives.size() && primitives[i + 1]->name == "feOffset" && + primitives[i + 2]->name == "feGaussianBlur" && + primitives[i + 3]->name == "feComposite" && primitives[i + 4]->name == "feColorMatrix") { + // Extract offset from feOffset. + float offsetX = 0, offsetY = 0; + std::string dx = getAttribute(primitives[i + 1], "dx", "0"); + std::string dy = getAttribute(primitives[i + 1], "dy", "0"); + offsetX = std::stof(dx); + offsetY = std::stof(dy); + + // Extract blur from feGaussianBlur. + float blurX = 0, blurY = 0; + std::string stdDeviation = getAttribute(primitives[i + 2], "stdDeviation", "0"); + std::istringstream iss(stdDeviation); + iss >> blurX; + if (!(iss >> blurY)) { + blurY = blurX; + } + + // Extract color from the second feColorMatrix. + // Format: "0 0 0 0 R 0 0 0 0 G 0 0 0 0 B 0 0 0 A 0" where R,G,B are 0-1 and A is alpha. + Color shadowColor = {0, 0, 0, 1.0f}; + std::string colorMatrix = getAttribute(primitives[i + 4], "values"); + if (!colorMatrix.empty()) { + std::istringstream cms(colorMatrix); + float values[20] = {}; + for (int j = 0; j < 20 && cms >> values[j]; j++) { + } + // R is at index 4, G at index 9, B at index 14, A at index 18. + shadowColor.red = values[4]; + shadowColor.green = values[9]; + shadowColor.blue = values[14]; + shadowColor.alpha = values[18]; + } + + auto dropShadow = std::make_unique(); + dropShadow->offsetX = offsetX; + dropShadow->offsetY = offsetY; + dropShadow->blurrinessX = blurX; + dropShadow->blurrinessY = blurY; + dropShadow->color = shadowColor; + dropShadow->shadowOnly = false; + filters.push_back(std::move(dropShadow)); + + // Skip the consumed primitives (5 elements) plus the feBlend that follows. + i += 5; + if (i < primitives.size() && primitives[i]->name == "feBlend") { + i++; + } + continue; } + } - // Only convert to BlurFilter when explicitly operating on SourceGraphic, or when it's - // the first filter primitive with no "in" attribute (defaults to SourceGraphic). - // Skip SourceAlpha (used in drop shadows), BackgroundImageFix (Figma pattern), and - // feGaussianBlur that chains from previous primitives (part of complex effects). - bool isSourceGraphic = (inAttr == "SourceGraphic") || (inAttr.empty() && isFirstPrimitive); + // Check for simple blur filter (first primitive with no "in" or in="SourceGraphic"). + if (node->name == "feGaussianBlur") { + std::string inAttr = getAttribute(node, "in"); + bool isSourceGraphic = (inAttr == "SourceGraphic") || (inAttr.empty() && i == 0); if (isSourceGraphic) { + std::string stdDeviation = getAttribute(node, "stdDeviation", "0"); + std::istringstream iss(stdDeviation); + float devX = 0, devY = 0; + iss >> devX; + if (!(iss >> devY)) { + devY = devX; + } auto blurFilter = std::make_unique(); blurFilter->blurrinessX = devX; blurFilter->blurrinessY = devY; filters.push_back(std::move(blurFilter)); } } - if (isFilterPrimitive) { - isFirstPrimitive = false; - } - child = child->getNextSibling(); + + i++; } } From 07556223273def15092151adf016f9e4df4cb2f1 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 10:27:47 +0800 Subject: [PATCH 132/678] Fix SVG foreground blur filter pattern not being converted to BlurFilter. --- pagx/src/svg/SVGImporter.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 55cd8d6a95..0afb56c632 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -1933,10 +1933,24 @@ void SVGParserImpl::convertFilterElement( } } - // Check for simple blur filter (first primitive with no "in" or in="SourceGraphic"). + // Check for blur filter that should apply to the source graphic. + // This includes: + // 1. feGaussianBlur with in="SourceGraphic" + // 2. feGaussianBlur as the first primitive with no "in" attribute + // 3. feGaussianBlur following a feBlend that merges SourceGraphic (Figma foreground blur pattern) if (node->name == "feGaussianBlur") { std::string inAttr = getAttribute(node, "in"); bool isSourceGraphic = (inAttr == "SourceGraphic") || (inAttr.empty() && i == 0); + + // Check for Figma foreground blur pattern: feFlood → feBlend(in=SourceGraphic) → feGaussianBlur. + if (!isSourceGraphic && i >= 2) { + auto& prevNode = primitives[i - 1]; + if (prevNode->name == "feBlend" && getAttribute(prevNode, "in") == "SourceGraphic") { + // This blur follows a blend with SourceGraphic, treat it as foreground blur. + isSourceGraphic = true; + } + } + if (isSourceGraphic) { std::string stdDeviation = getAttribute(node, "stdDeviation", "0"); std::istringstream iss(stdDeviation); From 3449d8e8de7011c68556b8dccd294d0d740e519f Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 11:44:24 +0800 Subject: [PATCH 133/678] Fix SVG viewBox handling to use viewBox dimensions directly without unit conversion scaling. --- pagx/src/svg/SVGImporter.cpp | 40 +++++++++++----------------- test/src/PAGXTest.cpp | 51 ++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 0afb56c632..c4a3215255 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -94,20 +94,22 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr } // Parse viewBox and dimensions. + // When viewBox is present, use viewBox dimensions for the PAGX document size, + // because PAGX doesn't support viewBox and all SVG coordinates are in viewBox space. + // The explicit width/height with unit conversions (e.g., "1080pt" -> 1440px) are ignored + // to avoid coordinate mismatch. auto viewBox = parseViewBox(getAttribute(root, "viewBox")); - float width = parseLength(getAttribute(root, "width"), 0); - float height = parseLength(getAttribute(root, "height"), 0); + float width = 0; + float height = 0; if (viewBox.size() >= 4) { _viewBoxWidth = viewBox[2]; _viewBoxHeight = viewBox[3]; - if (width == 0) { - width = _viewBoxWidth; - } - if (height == 0) { - height = _viewBoxHeight; - } + width = _viewBoxWidth; + height = _viewBoxHeight; } else { + width = parseLength(getAttribute(root, "width"), 0); + height = parseLength(getAttribute(root, "height"), 0); _viewBoxWidth = width; _viewBoxHeight = height; } @@ -134,28 +136,16 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr // This determines which ColorSources should be extracted to resources. countColorSourceReferences(root); - // Check if we need a viewBox transform. + // Handle viewBox offset if present (viewBox origin is not 0,0). bool needsViewBoxTransform = false; Matrix viewBoxMatrix = Matrix::Identity(); if (viewBox.size() >= 4) { float viewBoxX = viewBox[0]; float viewBoxY = viewBox[1]; - float viewBoxW = viewBox[2]; - float viewBoxH = viewBox[3]; - - if (viewBoxW > 0 && viewBoxH > 0 && - (viewBoxX != 0 || viewBoxY != 0 || viewBoxW != width || viewBoxH != height)) { - // Calculate uniform scale (meet behavior: fit inside viewport). - float scaleX = width / viewBoxW; - float scaleY = height / viewBoxH; - float scale = std::min(scaleX, scaleY); - - // Calculate translation to center content (xMidYMid). - float translateX = (width - viewBoxW * scale) / 2.0f - viewBoxX * scale; - float translateY = (height - viewBoxH * scale) / 2.0f - viewBoxY * scale; - - // Build the transform matrix: scale then translate. - viewBoxMatrix = Matrix::Translate(translateX, translateY) * Matrix::Scale(scale, scale); + + if (viewBoxX != 0 || viewBoxY != 0) { + // Only translate for non-zero viewBox origin. + viewBoxMatrix = Matrix::Translate(-viewBoxX, -viewBoxY); needsViewBoxTransform = true; } } diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 503b5a6a93..fb3bd1bdf9 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -109,7 +109,24 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { for (const auto& svgPath : svgFiles) { std::string baseName = std::filesystem::path(svgPath).stem().string(); - // Load original SVG with text shaper + // Convert to PAGX using new API + pagx::LayerBuilder::Options options; + options.fallbackTypefaces = GetFallbackTypefaces(); + auto content = pagx::LayerBuilder::FromSVGFile(svgPath, options); + if (content.root == nullptr) { + continue; + } + + // Use PAGX document size for rendering. + // PAGX uses viewBox dimensions when viewBox is present, avoiding unit conversion issues + // (e.g., "1080pt" would become 1440px but viewBox coordinates remain 1080). + int pagxWidth = static_cast(content.width); + int pagxHeight = static_cast(content.height); + if (pagxWidth <= 0 || pagxHeight <= 0) { + continue; + } + + // Load original SVG with text shaper. auto svgStream = Stream::MakeFromFile(svgPath); if (svgStream == nullptr) { continue; @@ -119,25 +136,19 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { continue; } + // Get tgfx's container size (respects width/height unit conversion). auto containerSize = svgDOM->getContainerSize(); - int width = static_cast(containerSize.width); - int height = static_cast(containerSize.height); - if (width <= 0 || height <= 0) { - continue; - } - - // Render original SVG - auto svgSurface = Surface::Make(context, width, height); - auto svgCanvas = svgSurface->getCanvas(); - svgDOM->render(svgCanvas); - EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); - - // Convert to PAGX using new API - pagx::LayerBuilder::Options options; - options.fallbackTypefaces = GetFallbackTypefaces(); - auto content = pagx::LayerBuilder::FromSVGFile(svgPath, options); - if (content.root == nullptr) { - continue; + int svgWidth = static_cast(containerSize.width); + int svgHeight = static_cast(containerSize.height); + + // Only compare SVG rendering when sizes match (no unit conversion difference). + // When viewBox is present with non-pixel width/height (e.g., "1080pt"), tgfx will scale + // content based on the unit conversion, but PAGX uses viewBox coordinates directly. + if (svgWidth == pagxWidth && svgHeight == pagxHeight) { + auto svgSurface = Surface::Make(context, svgWidth, svgHeight); + auto svgCanvas = svgSurface->getCanvas(); + svgDOM->render(svgCanvas); + EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); } // Save PAGX file to output directory @@ -150,7 +161,7 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { } // Render PAGX using DisplayList (required for mask to work). - auto pagxSurface = Surface::Make(context, width, height); + auto pagxSurface = Surface::Make(context, pagxWidth, pagxHeight); DisplayList displayList; displayList.root()->addChild(content.root); displayList.render(pagxSurface.get(), false); From 8efb1ae6f9d78619e8fa18b2db84f82f77a7c424 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 15:24:27 +0800 Subject: [PATCH 134/678] Refine PAGX spec documentation for layer system - Add section 4.1 Core Concepts with layer rendering flow, layer content, layer contour, and layer background definitions - Update layer styles section (4.3) with detailed input source descriptions and opaque mode explanation - Update layer filters section (4.4) to clarify filter input concept - Add rendering steps for each layer style and filter - Clarify the difference between DropShadowStyle and DropShadowFilter - Fix section numbering throughout chapter 4 --- DEPS | 2 +- pagx/docs/pagx_spec.md | 173 +++++++++++++++++++++++++---------------- 2 files changed, 107 insertions(+), 68 deletions(-) diff --git a/DEPS b/DEPS index 94acd5ad88..136ce97170 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "9b732d8a3810d71b5a69ee735635baeb79a09cbe", + "commit": "c1463b7cc765348f098f9640d31a6bc9c1c37bce", "dir": "third_party/tgfx" }, { diff --git a/pagx/docs/pagx_spec.md b/pagx/docs/pagx_spec.md index 42e23e71df..431284e111 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/docs/pagx_spec.md @@ -517,7 +517,57 @@ PAGX 文档采用层级结构组织内容: 图层(Layer)是 PAGX 内容组织的基本单元,提供了丰富的视觉效果控制能力。 -### 4.1 图层(Layer) +### 4.1 核心概念 + +本节介绍图层系统的核心概念,这些概念是理解图层样式、滤镜和遮罩等功能的基础。 + +#### 图层渲染流程 + +图层绑定的绘制器(Fill、Stroke 等)通过 `placement` 属性分为下层内容和上层内容,默认为下层内容。单个图层渲染时按以下顺序处理: + +1. **图层样式(下方)**:渲染位于内容下方的图层样式(如投影阴影) +2. **下层内容**:渲染 `placement="background"` 的 Fill 和 Stroke +3. **子图层**:按文档顺序递归渲染所有子图层 +4. **图层样式(上方)**:渲染位于内容上方的图层样式(如内阴影) +5. **上层内容**:渲染 `placement="foreground"` 的 Fill 和 Stroke +6. **图层滤镜**:将前面步骤的整体输出作为滤镜链的输入,依次应用所有滤镜 + +#### 图层内容(Layer Content) + +**图层内容**是指图层的下层内容、子图层和上层内容的完整渲染结果。图层样式基于图层内容计算效果。例如,当填充为下层、描边为上层时,描边会绘制在子图层之上,但投影阴影仍然基于包含填充、子图层和描边的完整图层内容计算。 + +#### 图层轮廓(Layer Contour) + +**图层轮廓**用于遮罩和部分图层样式。与正常图层内容相比,图层轮廓有以下区别: + +1. **包含 alpha=0 的几何绘制**:填充透明度完全为 0 的几何形状也会加入轮廓 +2. **纯色填充和渐变填充**:忽略原始填充,替换为不透明白色绘制 +3. **图片填充**:保留原始像素,但将半透明像素转换为完全不透明(完全透明的像素保留) + +注意:几何元素必须有绘制器才能参与轮廓,单独的几何元素(Rectangle、Ellipse 等)如果没有对应的 Fill 或 Stroke,则不会参与轮廓计算。 + +图层轮廓主要用于: + +- **图层样式**:部分图层样式需要轮廓作为其中一个输入源 +- **遮罩**:`maskType="contour"` 使用遮罩图层的轮廓进行裁剪 + +#### 图层背景(Layer Background) + +**图层背景**是指当前图层下方所有已渲染内容的合成结果,包括: +- 当前图层下方的所有兄弟图层及其子树的渲染结果 +- 当前图层已绘制的下方图层样式(不包括 BackgroundBlurStyle 本身) + +图层背景主要用于: + +- **图层样式**:部分图层样式需要背景作为其中一个输入源 +- **混合模式**:部分混合模式需要背景信息才能正确渲染 + +**背景透传**:通过 `passThroughBackground` 属性控制是否允许图层背景透传给子图层。当设置为 `false` 时,子图层的背景依赖样式将无法获取到正确的图层背景。以下情况会自动禁用背景透传: +- 图层使用了非 `normal` 的混合模式 +- 图层应用了滤镜 +- 图层使用了 3D 变换或投影变换 + +### 4.2 图层(Layer) `` 是内容和子图层的基本容器。 @@ -608,46 +658,15 @@ Layer 的子元素按类型自动归类为四个集合: | `plusLighter` | S + D | 相加(趋向白色) | | `plusDarker` | S + D - 1 | 相加减一(趋向黑色) | -#### 图层渲染流程 +### 4.3 图层样式(Layer Styles) -图层内容(填充、描边等)通过 `placement` 属性分为背景内容和前景内容,默认为背景内容。单个图层渲染时按以下顺序处理: +图层样式在图层内容的上方或下方添加视觉效果,不会替换原有内容。 -1. **图层样式(下方)**:渲染位于内容下方的图层样式(如投影阴影) -2. **图层背景内容**:渲染填充和描边 -3. **子图层**:按文档顺序递归渲染所有子图层 -4. **图层样式(上方)**:渲染位于内容上方的图层样式(如内阴影) -5. **图层前景内容**:渲染填充和描边 -6. **滤镜**:应用滤镜链 - -**图层样式的参考内容**:图层样式计算时使用的参考内容包含背景内容和前景内容的完整形状。例如,当填充为背景、描边为前景时,描边会绘制在子图层之上,但投影阴影仍然基于包含填充和描边的完整形状计算。 +**图层样式的输入源**: -#### 图层轮廓(Layer Contour) +所有图层样式都基于**图层内容**计算效果。在图层样式中,图层内容会自动转换为 **Opaque 模式**:使用正常的填充方式渲染几何形状,然后将所有半透明像素转换为完全不透明(完全透明的像素保留)。这意味着半透明填充产生的阴影效果与完全不透明填充相同。 -**图层轮廓**是图层内容的形状信息,代表图层内容的外形边界。轮廓不包含实际的 RGBA 颜色数据,仅表示形状。图层轮廓主要用于: - -- **图层样式**:投影阴影、内阴影、背景模糊等效果基于轮廓计算 -- **遮罩**:`maskType="contour"` 使用遮罩图层的轮廓进行裁剪 - -**轮廓与可见内容的关系**: - -- **透明度为 0 的绘制器仍参与轮廓**:即使 Fill 或 Stroke 的 `alpha="0"`,其形状仍然会参与图层轮廓的构建。这意味着完全透明的内容虽然不可见,但仍会影响图层样式的效果范围 -- **几何元素必须有绘制器才能参与轮廓**:单独的几何元素(Rectangle、Ellipse 等)如果没有对应的 Fill 或 Stroke,则不会参与轮廓计算 - -**示例**:创建一个不可见但有阴影的图层: - -```xml - - - - - -``` - -上例中,矩形填充完全透明不可见,但投影阴影仍然会基于矩形的轮廓生成。 - -### 4.2 图层样式(Layer Styles) - -图层样式在图层内容渲染完成后应用。 +部分图层样式还会额外使用**图层轮廓**或**图层背景**作为输入(详见各样式说明)。图层轮廓和图层背景的定义参见 4.1 节。 ```xml @@ -665,9 +684,9 @@ Layer 的子元素按类型自动归类为四个集合: |------|------|--------|------| | `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | -#### 4.2.1 投影阴影(DropShadowStyle) +#### 4.3.1 投影阴影(DropShadowStyle) -在图层外部绘制投影阴影。 +在图层**下方**绘制投影阴影。基于不透明图层内容计算阴影形状。当 `showBehindLayer="false"` 时,额外使用**图层轮廓**作为擦除遮罩挖空被图层遮挡的部分。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| @@ -679,49 +698,53 @@ Layer 的子元素按类型自动归类为四个集合: | `showBehindLayer` | bool | true | 图层后面是否显示阴影 | **渲染步骤**: -1. 将图层内容偏移 `(offsetX, offsetY)` -2. 应用高斯模糊 `(blurrinessX, blurrinessY)` -3. 将图层 alpha 通道替换为 `color` 的颜色 +1. 获取不透明图层内容并偏移 `(offsetX, offsetY)` +2. 对偏移后的内容应用高斯模糊 `(blurrinessX, blurrinessY)` +3. 使用 `color` 的颜色填充阴影区域 +4. 如果 `showBehindLayer="false"`,使用图层轮廓作为擦除遮罩挖空被遮挡部分 **showBehindLayer**: - `true`:阴影完整显示,包括被图层内容遮挡的部分 -- `false`:阴影被图层内容遮挡的部分会被挖空(仅显示图层轮廓外的阴影) +- `false`:阴影被图层内容遮挡的部分会被挖空(使用图层轮廓作为擦除遮罩) -#### 4.2.2 内阴影(InnerShadowStyle) +#### 4.3.2 背景模糊(BackgroundBlurStyle) -在图层内部绘制内阴影。 +在图层**下方**对图层背景应用模糊效果。基于**图层背景**计算效果,使用不透明图层内容作为遮罩裁剪。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `offsetX` | float | 0 | X 偏移 | -| `offsetY` | float | 0 | Y 偏移 | | `blurrinessX` | float | 0 | X 模糊半径 | | `blurrinessY` | float | 0 | Y 模糊半径 | -| `color` | color | #000000 | 阴影颜色 | +| `tileMode` | TileMode | mirror | 平铺模式 | **渲染步骤**: -1. 创建图层轮廓的反向遮罩 -2. 偏移并模糊 -3. 与图层内容求交集 +1. 获取图层边界下方的图层背景 +2. 对图层背景应用高斯模糊 `(blurrinessX, blurrinessY)` +3. 使用不透明图层内容作为遮罩裁剪模糊结果 -#### 4.2.3 背景模糊(BackgroundBlurStyle) +#### 4.3.3 内阴影(InnerShadowStyle) -对图层下方的背景应用模糊效果。 +在图层**上方**绘制内阴影,效果呈现在图层内容之内。基于不透明图层内容计算阴影范围。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| +| `offsetX` | float | 0 | X 偏移 | +| `offsetY` | float | 0 | Y 偏移 | | `blurrinessX` | float | 0 | X 模糊半径 | | `blurrinessY` | float | 0 | Y 模糊半径 | -| `tileMode` | TileMode | mirror | 平铺模式 | +| `color` | color | #000000 | 阴影颜色 | **渲染步骤**: -1. 获取图层边界下方的背景内容 -2. 应用高斯模糊 `(blurrinessX, blurrinessY)` -3. 使用图层轮廓作为遮罩裁剪模糊结果 +1. 获取不透明图层内容并偏移 `(offsetX, offsetY)` +2. 对偏移后内容的反向(内容外部区域)应用高斯模糊 `(blurrinessX, blurrinessY)` +3. 使用 `color` 的颜色填充阴影区域 +4. 与不透明图层内容求交集,仅保留内容内部的阴影 -### 4.3 滤镜效果(Filter Effects) +### 4.4 图层滤镜(Layer Filters) -滤镜按文档顺序链式应用,每个滤镜的输出作为下一个滤镜的输入。 +图层滤镜是图层渲染的最后一个环节,所有之前按顺序渲染的结果(包含图层样式)累积起来作为滤镜的输入。滤镜按文档顺序链式应用,每个滤镜的输出作为下一个滤镜的输入。 + +与图层样式(4.3 节)的关键区别:图层样式在图层内容的上方或下方**独立渲染**视觉效果,而滤镜**修改**图层的整体渲染输出。图层样式先于滤镜应用。 ```xml @@ -733,7 +756,7 @@ Layer 的子元素按类型自动归类为四个集合: ``` -#### 4.3.1 模糊滤镜(BlurFilter) +#### 4.4.1 模糊滤镜(BlurFilter) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| @@ -741,7 +764,9 @@ Layer 的子元素按类型自动归类为四个集合: | `blurrinessY` | float | (必填) | Y 模糊半径 | | `tileMode` | TileMode | decal | 平铺模式 | -#### 4.3.2 投影阴影滤镜(DropShadowFilter) +#### 4.4.2 投影阴影滤镜(DropShadowFilter) + +基于滤镜输入生成阴影效果。与 DropShadowStyle 的核心区别:滤镜基于原始渲染内容投影,支持半透明度;而样式基于不透明图层内容投影。此外两者支持的属性功能也有所不同。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| @@ -752,7 +777,15 @@ Layer 的子元素按类型自动归类为四个集合: | `color` | color | #000000 | 阴影颜色 | | `shadowOnly` | bool | false | 仅显示阴影 | -#### 4.3.3 内阴影滤镜(InnerShadowFilter) +**渲染步骤**: +1. 将滤镜输入偏移 `(offsetX, offsetY)` +2. 提取 alpha 通道并应用高斯模糊 `(blurrinessX, blurrinessY)` +3. 使用 `color` 的颜色填充阴影区域 +4. 将阴影与滤镜输入合成(`shadowOnly=false`)或仅输出阴影(`shadowOnly=true`) + +#### 4.4.3 内阴影滤镜(InnerShadowFilter) + +在滤镜输入的内部绘制阴影。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| @@ -763,7 +796,13 @@ Layer 的子元素按类型自动归类为四个集合: | `color` | color | #000000 | 阴影颜色 | | `shadowOnly` | bool | false | 仅显示阴影 | -#### 4.3.4 混合滤镜(BlendFilter) +**渲染步骤**: +1. 创建滤镜输入 alpha 通道的反向遮罩 +2. 偏移并应用高斯模糊 +3. 与滤镜输入的 alpha 通道求交集 +4. 将结果与滤镜输入合成(`shadowOnly=false`)或仅输出阴影(`shadowOnly=true`) + +#### 4.4.4 混合滤镜(BlendFilter) 将指定颜色以指定混合模式叠加到图层上。 @@ -772,7 +811,7 @@ Layer 的子元素按类型自动归类为四个集合: | `color` | color | (必填) | 混合颜色 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | -#### 4.3.5 颜色矩阵滤镜(ColorMatrixFilter) +#### 4.4.5 颜色矩阵滤镜(ColorMatrixFilter) 使用 4×5 颜色矩阵变换颜色。 @@ -789,9 +828,9 @@ Layer 的子元素按类型自动归类为四个集合: | 1 | ``` -### 4.4 裁剪与遮罩(Clipping and Masking) +### 4.5 裁剪与遮罩(Clipping and Masking) -#### 4.4.1 scrollRect(滚动裁剪) +#### 4.5.1 scrollRect(滚动裁剪) `scrollRect` 属性定义图层的可视区域,超出该区域的内容会被裁剪。 @@ -802,7 +841,7 @@ Layer 的子元素按类型自动归类为四个集合: ``` -#### 4.4.2 遮罩(Masking) +#### 4.5.2 遮罩(Masking) 通过 `mask` 属性引用另一个图层作为遮罩。 From fd03c2f3b702816f5870c7f6a7c4a4f04bc17147 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 16:33:43 +0800 Subject: [PATCH 135/678] Fix SolidColor parsing to support HEX and p3() color formats. The PAGXImporter was expecting individual red/green/blue/alpha attributes, but PAGXExporter outputs color as a single "color" attribute in HEX (#RRGGBB) or p3(r, g, b) format for wide gamut colors. This mismatch caused all colors to default to black (0, 0, 0, 1). --- pagx/src/PAGXImporter.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 3afcab3a08..d9035bc939 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -787,11 +787,18 @@ RangeSelector PAGXImporterImpl::parseRangeSelector(const XMLNode* node) { std::unique_ptr PAGXImporterImpl::parseSolidColor(const XMLNode* node) { auto solid = std::make_unique(); solid->id = getAttribute(node, "id"); - solid->color.red = getFloatAttribute(node, "red", 0); - solid->color.green = getFloatAttribute(node, "green", 0); - solid->color.blue = getFloatAttribute(node, "blue", 0); - solid->color.alpha = getFloatAttribute(node, "alpha", 1); - solid->color.colorSpace = ColorSpaceFromString(getAttribute(node, "colorSpace", "sRGB")); + // Support "color" attribute with HEX (#RRGGBB) or p3() format + auto colorStr = getAttribute(node, "color"); + if (!colorStr.empty()) { + solid->color = parseColor(colorStr); + } else { + // Fallback to individual red/green/blue/alpha attributes + solid->color.red = getFloatAttribute(node, "red", 0); + solid->color.green = getFloatAttribute(node, "green", 0); + solid->color.blue = getFloatAttribute(node, "blue", 0); + solid->color.alpha = getFloatAttribute(node, "alpha", 1); + solid->color.colorSpace = ColorSpaceFromString(getAttribute(node, "colorSpace", "sRGB")); + } return solid; } From 01934fe2cf2127ea2d574c78fe59abb81b320347 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 17:01:30 +0800 Subject: [PATCH 136/678] Fix inline ColorSource id not being cleared causing gradient references to fail. --- pagx/src/svg/SVGImporter.cpp | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index c4a3215255..04146b6859 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -1072,6 +1072,22 @@ Rect SVGParserImpl::getShapeBounds(const std::shared_ptr& element) { } } + // For use element, get bounds of the referenced element and apply x/y offset. + if (tag == "use") { + std::string href = getAttribute(element, "xlink:href"); + if (href.empty()) { + href = getAttribute(element, "href"); + } + std::string refId = resolveUrl(href); + auto it = _defs.find(refId); + if (it != _defs.end()) { + Rect refBounds = getShapeBounds(it->second); + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); + return Rect::MakeXYWH(refBounds.x + x, refBounds.y + y, refBounds.width, refBounds.height); + } + } + return Rect::MakeXYWH(0, 0, 0, 0); } @@ -2163,15 +2179,19 @@ std::unique_ptr SVGParserImpl::getColorSourceForRef(const std::stri } // refCount <= 1: inline the ColorSource (no id). + std::unique_ptr colorSource = nullptr; if (defName == "linearGradient") { - return convertLinearGradient(defNode, shapeBounds); + colorSource = convertLinearGradient(defNode, shapeBounds); } else if (defName == "radialGradient") { - return convertRadialGradient(defNode, shapeBounds); + colorSource = convertRadialGradient(defNode, shapeBounds); } else if (defName == "pattern") { - return convertPattern(defNode, shapeBounds); + colorSource = convertPattern(defNode, shapeBounds); } - - return nullptr; + if (colorSource) { + // Clear the id for inline ColorSource to avoid being treated as a reference. + colorSource->id.clear(); + } + return colorSource; } } // namespace pagx From 974e10cdc70dd8e8331a785141392eab95ccb6e0 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 17:15:35 +0800 Subject: [PATCH 137/678] Skip SVG elements with unsupported filter effects to avoid rendering incorrect black fills. --- pagx/src/svg/SVGImporter.cpp | 15 +++++++++++++-- pagx/src/svg/SVGParserInternal.h | 4 +++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 04146b6859..7953062969 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -323,7 +323,12 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrsecond, layer->filters, layer->styles); + bool filterConverted = convertFilterElement(filterIt->second, layer->filters, layer->styles); + if (!filterConverted) { + // Filter could not be converted to PAGX format. Skip this element entirely to avoid + // rendering incorrect results (e.g., black fill instead of shadow effect). + return nullptr; + } } } @@ -1862,10 +1867,13 @@ std::unique_ptr SVGParserImpl::convertMaskElement( return maskLayer; } -void SVGParserImpl::convertFilterElement( +bool SVGParserImpl::convertFilterElement( const std::shared_ptr& filterElement, std::vector>& filters, std::vector>& styles) { + size_t initialFilterCount = filters.size(); + size_t initialStyleCount = styles.size(); + // Collect all filter primitives for analysis. std::vector> primitives; auto child = filterElement->getFirstChild(); @@ -1974,6 +1982,9 @@ void SVGParserImpl::convertFilterElement( i++; } + + // Return true if any filter or style was added. + return filters.size() > initialFilterCount || styles.size() > initialStyleCount; } void SVGParserImpl::collectAllIds(const std::shared_ptr& node) { diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index afbdf78a47..8fa156f68c 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -98,7 +98,9 @@ class SVGParserImpl { std::unique_ptr convertMaskElement(const std::shared_ptr& maskElement, const InheritedStyle& parentStyle); - void convertFilterElement(const std::shared_ptr& filterElement, + // Converts SVG filter element to PAGX filters/styles. + // Returns true if the filter was successfully converted, false otherwise. + bool convertFilterElement(const std::shared_ptr& filterElement, std::vector>& filters, std::vector>& styles); From 32358b66cab2bb2bccb29754db672160462f2104 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 17:19:22 +0800 Subject: [PATCH 138/678] Scale SVG test output to 400px min edge for better visibility. --- test/src/PAGXTest.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index fb3bd1bdf9..8f8eab84fb 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -16,6 +16,7 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// +#include #include #include "pagx/LayerBuilder.h" #include "pagx/PAGXDocument.h" @@ -87,6 +88,9 @@ static void SaveFile(const std::shared_ptr& data, const std::string& key) * Test case: Convert all SVG files in apitest/SVG directory to PAGX format and render them. */ PAG_TEST(PAGXTest, SVGToPAGXAll) { + // Minimum canvas edge length for rendering. Small images will be scaled up for better visibility. + constexpr int MinCanvasEdge = 400; + std::string svgDir = ProjectPath::Absolute("resources/apitest/SVG"); std::vector svgFiles = {}; @@ -120,12 +124,18 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { // Use PAGX document size for rendering. // PAGX uses viewBox dimensions when viewBox is present, avoiding unit conversion issues // (e.g., "1080pt" would become 1440px but viewBox coordinates remain 1080). - int pagxWidth = static_cast(content.width); - int pagxHeight = static_cast(content.height); + float pagxWidth = content.width; + float pagxHeight = content.height; if (pagxWidth <= 0 || pagxHeight <= 0) { continue; } + // Scale up small images for better visibility. + float maxEdge = std::max(pagxWidth, pagxHeight); + float scale = (maxEdge < MinCanvasEdge) ? (MinCanvasEdge / maxEdge) : 1.0f; + int canvasWidth = static_cast(std::ceil(pagxWidth * scale)); + int canvasHeight = static_cast(std::ceil(pagxHeight * scale)); + // Load original SVG with text shaper. auto svgStream = Stream::MakeFromFile(svgPath); if (svgStream == nullptr) { @@ -144,9 +154,10 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { // Only compare SVG rendering when sizes match (no unit conversion difference). // When viewBox is present with non-pixel width/height (e.g., "1080pt"), tgfx will scale // content based on the unit conversion, but PAGX uses viewBox coordinates directly. - if (svgWidth == pagxWidth && svgHeight == pagxHeight) { - auto svgSurface = Surface::Make(context, svgWidth, svgHeight); + if (svgWidth == static_cast(pagxWidth) && svgHeight == static_cast(pagxHeight)) { + auto svgSurface = Surface::Make(context, canvasWidth, canvasHeight); auto svgCanvas = svgSurface->getCanvas(); + svgCanvas->scale(scale, scale); svgDOM->render(svgCanvas); EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); } @@ -161,8 +172,9 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { } // Render PAGX using DisplayList (required for mask to work). - auto pagxSurface = Surface::Make(context, pagxWidth, pagxHeight); + auto pagxSurface = Surface::Make(context, canvasWidth, canvasHeight); DisplayList displayList; + content.root->setMatrix(tgfx::Matrix::MakeScale(scale, scale)); displayList.root()->addChild(content.root); displayList.render(pagxSurface.get(), false); EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName + "_pagx")); From eb1957ee54da485b841d14223d0cb84a6fe56eab Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 17:28:35 +0800 Subject: [PATCH 139/678] Fix parsePoints incorrectly consuming digits when skipping space separators. --- pagx/src/svg/SVGImporter.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 7953062969..b958b86a96 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -1626,11 +1626,12 @@ PathData SVGParserImpl::parsePoints(const std::string& value, bool closed) { std::vector points; std::istringstream iss(value); float num = 0; + // The >> operator automatically skips whitespace, so we just need to handle commas. while (iss >> num) { points.push_back(num); - char c = 0; - if (iss.peek() == ',' || iss.peek() == ' ') { - iss >> c; + // Skip optional comma separator (whitespace is handled automatically by >>). + if (iss.peek() == ',') { + iss.get(); } } From 86510c14494f1233411783484cd1cf27b664ea4a Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 17:46:09 +0800 Subject: [PATCH 140/678] Replace std::stringstream with C-style string parsing in pagx module to reduce binary size. --- pagx/src/PAGXExporter.cpp | 108 +++++++++++++++------------ pagx/src/PAGXImporter.cpp | 78 ++------------------ pagx/src/PAGXImporterAPI.cpp | 30 ++++++-- pagx/src/PAGXStringUtils.cpp | 131 +++++++++++++++++++++++++-------- pagx/src/PAGXStringUtils.h | 17 +++++ pagx/src/svg/SVGImporter.cpp | 139 ++++++++++++++++++++--------------- pagx/src/xml/XMLParser.cpp | 27 +++++-- 7 files changed, 312 insertions(+), 218 deletions(-) diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index 4d09f31436..54ecfb80b2 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -17,7 +17,7 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "PAGXExporterImpl.h" -#include +#include #include "PAGXStringUtils.h" #include "pagx/nodes/BackgroundBlurStyle.h" #include "pagx/nodes/BlendFilter.h" @@ -62,80 +62,111 @@ namespace pagx { class XMLBuilder { public: void appendDeclaration() { - buffer << "\n"; + buffer += "\n"; } void openElement(const std::string& tag) { writeIndent(); - buffer << "<" << tag; + buffer += "<"; + buffer += tag; tagStack.push_back(tag); } void addAttribute(const std::string& name, const std::string& value) { if (!value.empty()) { - buffer << " " << name << "=\"" << escapeXML(value) << "\""; + buffer += " "; + buffer += name; + buffer += "=\""; + buffer += escapeXML(value); + buffer += "\""; } } void addAttribute(const std::string& name, float value, float defaultValue = 0) { if (value != defaultValue) { - buffer << " " << name << "=\"" << formatFloat(value) << "\""; + buffer += " "; + buffer += name; + buffer += "=\""; + buffer += FloatToString(value); + buffer += "\""; } } void addRequiredAttribute(const std::string& name, float value) { - buffer << " " << name << "=\"" << formatFloat(value) << "\""; + buffer += " "; + buffer += name; + buffer += "=\""; + buffer += FloatToString(value); + buffer += "\""; } void addRequiredAttribute(const std::string& name, const std::string& value) { - buffer << " " << name << "=\"" << escapeXML(value) << "\""; + buffer += " "; + buffer += name; + buffer += "=\""; + buffer += escapeXML(value); + buffer += "\""; } void addAttribute(const std::string& name, int value, int defaultValue = 0) { if (value != defaultValue) { - buffer << " " << name << "=\"" << value << "\""; + char buf[32] = {}; + snprintf(buf, sizeof(buf), "%d", value); + buffer += " "; + buffer += name; + buffer += "=\""; + buffer += buf; + buffer += "\""; } } void addAttribute(const std::string& name, bool value, bool defaultValue = false) { if (value != defaultValue) { - buffer << " " << name << "=\"" << (value ? "true" : "false") << "\""; + buffer += " "; + buffer += name; + buffer += "=\""; + buffer += (value ? "true" : "false"); + buffer += "\""; } } void closeElementStart() { - buffer << ">\n"; + buffer += ">\n"; indentLevel++; } void closeElementSelfClosing() { - buffer << "/>\n"; + buffer += "/>\n"; tagStack.pop_back(); } void closeElement() { indentLevel--; writeIndent(); - buffer << "\n"; + buffer += "\n"; tagStack.pop_back(); } void addTextContent(const std::string& text) { - buffer << ""; + buffer += ""; } - std::string str() const { - return buffer.str(); + const std::string& str() const { + return buffer; } private: - std::ostringstream buffer = {}; + std::string buffer = {}; std::vector tagStack = {}; int indentLevel = 0; void writeIndent() { for (int i = 0; i < indentLevel; i++) { - buffer << " "; + buffer += " "; } } @@ -165,21 +196,6 @@ class XMLBuilder { } return result; } - - static std::string formatFloat(float value) { - std::ostringstream oss = {}; - oss << value; - auto str = oss.str(); - if (str.find('.') != std::string::npos) { - while (str.back() == '0') { - str.pop_back(); - } - if (str.back() == '.') { - str.pop_back(); - } - } - return str; - } }; //============================================================================== @@ -187,32 +203,34 @@ class XMLBuilder { //============================================================================== static std::string pointToString(const Point& p) { - std::ostringstream oss = {}; - oss << p.x << "," << p.y; - return oss.str(); + char buf[64] = {}; + snprintf(buf, sizeof(buf), "%g,%g", p.x, p.y); + return std::string(buf); } static std::string sizeToString(const Size& s) { - std::ostringstream oss = {}; - oss << s.width << "," << s.height; - return oss.str(); + char buf[64] = {}; + snprintf(buf, sizeof(buf), "%g,%g", s.width, s.height); + return std::string(buf); } static std::string rectToString(const Rect& r) { - std::ostringstream oss = {}; - oss << r.x << "," << r.y << "," << r.width << "," << r.height; - return oss.str(); + char buf[128] = {}; + snprintf(buf, sizeof(buf), "%g,%g,%g,%g", r.x, r.y, r.width, r.height); + return std::string(buf); } static std::string floatListToString(const std::vector& values) { - std::ostringstream oss = {}; + std::string result; + char buf[32] = {}; for (size_t i = 0; i < values.size(); i++) { if (i > 0) { - oss << ","; + result += ","; } - oss << values[i]; + snprintf(buf, sizeof(buf), "%g", values[i]); + result += buf; } - return oss.str(); + return result; } //============================================================================== diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index d9035bc939..43ea7503fa 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -17,8 +17,8 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "PAGXImporterImpl.h" +#include #include -#include #include "PAGXStringUtils.h" namespace pagx { @@ -1086,17 +1086,7 @@ bool PAGXImporterImpl::getBoolAttribute(const XMLNode* node, const std::string& Point PAGXImporterImpl::parsePoint(const std::string& str) { Point point = {}; - std::istringstream iss(str); - std::string token = {}; - std::vector values = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - values.push_back(std::stof(trimmed)); - } - } + auto values = ParseFloatList(str); if (values.size() >= 2) { point.x = values[0]; point.y = values[1]; @@ -1106,17 +1096,7 @@ Point PAGXImporterImpl::parsePoint(const std::string& str) { Size PAGXImporterImpl::parseSize(const std::string& str) { Size size = {}; - std::istringstream iss(str); - std::string token = {}; - std::vector values = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - values.push_back(std::stof(trimmed)); - } - } + auto values = ParseFloatList(str); if (values.size() >= 2) { size.width = values[0]; size.height = values[1]; @@ -1126,17 +1106,7 @@ Size PAGXImporterImpl::parseSize(const std::string& str) { Rect PAGXImporterImpl::parseRect(const std::string& str) { Rect rect = {}; - std::istringstream iss(str); - std::string token = {}; - std::vector values = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - values.push_back(std::stof(trimmed)); - } - } + auto values = ParseFloatList(str); if (values.size() >= 4) { rect.x = values[0]; rect.y = values[1]; @@ -1208,17 +1178,7 @@ Color PAGXImporterImpl::parseColor(const std::string& str) { auto end = str.find(')'); if (start != std::string::npos && end != std::string::npos) { auto inner = str.substr(start + 1, end - start - 1); - std::istringstream iss(inner); - std::string token = {}; - std::vector components = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - components.push_back(std::stof(trimmed)); - } - } + auto components = ParseFloatList(inner); if (components.size() >= 3) { Color color = {}; color.red = components[0]; @@ -1236,17 +1196,7 @@ Color PAGXImporterImpl::parseColor(const std::string& str) { auto end = str.find(')'); if (start != std::string::npos && end != std::string::npos) { auto inner = str.substr(start + 1, end - start - 1); - std::istringstream iss(inner); - std::string token = {}; - std::vector components = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - components.push_back(std::stof(trimmed)); - } - } + auto components = ParseFloatList(inner); if (components.size() >= 3) { Color color = {}; color.red = components[0]; @@ -1262,21 +1212,7 @@ Color PAGXImporterImpl::parseColor(const std::string& str) { } std::vector PAGXImporterImpl::parseFloatList(const std::string& str) { - std::vector values = {}; - std::istringstream iss(str); - std::string token = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - try { - values.push_back(std::stof(trimmed)); - } catch (...) { - } - } - } - return values; + return ParseFloatList(str); } } // namespace pagx diff --git a/pagx/src/PAGXImporterAPI.cpp b/pagx/src/PAGXImporterAPI.cpp index 362e9b7e91..ac536923db 100644 --- a/pagx/src/PAGXImporterAPI.cpp +++ b/pagx/src/PAGXImporterAPI.cpp @@ -17,20 +17,36 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "pagx/PAGXImporter.h" -#include -#include +#include #include "PAGXImporterImpl.h" namespace pagx { std::shared_ptr PAGXImporter::FromFile(const std::string& filePath) { - std::ifstream file(filePath, std::ios::binary); - if (!file.is_open()) { + FILE* file = fopen(filePath.c_str(), "rb"); + if (!file) { return nullptr; } - std::stringstream buffer = {}; - buffer << file.rdbuf(); - auto doc = FromXML(buffer.str()); + + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + if (fileSize <= 0) { + fclose(file); + return nullptr; + } + + std::string content; + content.resize(static_cast(fileSize)); + size_t bytesRead = fread(&content[0], 1, static_cast(fileSize), file); + fclose(file); + + if (bytesRead != static_cast(fileSize)) { + return nullptr; + } + + auto doc = FromXML(content); if (doc) { auto lastSlash = filePath.find_last_of("/\\"); if (lastSlash != std::string::npos) { diff --git a/pagx/src/PAGXStringUtils.cpp b/pagx/src/PAGXStringUtils.cpp index 61c274226e..1fab3970db 100644 --- a/pagx/src/PAGXStringUtils.cpp +++ b/pagx/src/PAGXStringUtils.cpp @@ -19,7 +19,8 @@ #include "PAGXStringUtils.h" #include #include -#include +#include +#include #include namespace pagx { @@ -269,13 +270,14 @@ ColorSpace ColorSpaceFromString(const std::string& str) { std::string ColorToHexString(const Color& color, bool withAlpha) { if (color.colorSpace == ColorSpace::DisplayP3) { - std::ostringstream oss = {}; - oss << "p3(" << color.red << ", " << color.green << ", " << color.blue; + char buf[64] = {}; if (withAlpha && color.alpha < 1.0f) { - oss << ", " << color.alpha; + snprintf(buf, sizeof(buf), "p3(%g, %g, %g, %g)", color.red, color.green, color.blue, + color.alpha); + } else { + snprintf(buf, sizeof(buf), "p3(%g, %g, %g)", color.red, color.green, color.blue); } - oss << ")"; - return oss.str(); + return std::string(buf); } auto toHex = [](float v) -> std::string { int i = static_cast(std::round(v * 255.0f)); @@ -296,25 +298,15 @@ std::string ColorToHexString(const Color& color, bool withAlpha) { //============================================================================== std::string MatrixToString(const Matrix& matrix) { - std::ostringstream oss = {}; - oss << matrix.a << "," << matrix.b << "," << matrix.c << "," << matrix.d << "," << matrix.tx - << "," << matrix.ty; - return oss.str(); + char buf[256] = {}; + snprintf(buf, sizeof(buf), "%g,%g,%g,%g,%g,%g", matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, + matrix.ty); + return std::string(buf); } Matrix MatrixFromString(const std::string& str) { Matrix m = {}; - std::istringstream iss(str); - std::string token = {}; - std::vector values = {}; - while (std::getline(iss, token, ',')) { - auto trimmed = token; - trimmed.erase(0, trimmed.find_first_not_of(" \t")); - trimmed.erase(trimmed.find_last_not_of(" \t") + 1); - if (!trimmed.empty()) { - values.push_back(std::stof(trimmed)); - } - } + auto values = ParseFloatList(str); if (values.size() >= 6) { m.a = values[0]; m.b = values[1]; @@ -331,37 +323,42 @@ Matrix MatrixFromString(const std::string& str) { //============================================================================== std::string PathDataToSVGString(const PathData& pathData) { - std::ostringstream oss; - oss.precision(6); + std::string result; + result.reserve(256); size_t pointIndex = 0; const auto& verbs = pathData.verbs(); const auto& points = pathData.points(); + char buf[64] = {}; for (auto verb : verbs) { const float* pts = points.data() + pointIndex; switch (verb) { case PathVerb::Move: - oss << "M" << pts[0] << " " << pts[1]; + snprintf(buf, sizeof(buf), "M%g %g", pts[0], pts[1]); + result += buf; break; case PathVerb::Line: - oss << "L" << pts[0] << " " << pts[1]; + snprintf(buf, sizeof(buf), "L%g %g", pts[0], pts[1]); + result += buf; break; case PathVerb::Quad: - oss << "Q" << pts[0] << " " << pts[1] << " " << pts[2] << " " << pts[3]; + snprintf(buf, sizeof(buf), "Q%g %g %g %g", pts[0], pts[1], pts[2], pts[3]); + result += buf; break; case PathVerb::Cubic: - oss << "C" << pts[0] << " " << pts[1] << " " << pts[2] << " " << pts[3] << " " << pts[4] - << " " << pts[5]; + snprintf(buf, sizeof(buf), "C%g %g %g %g %g %g", pts[0], pts[1], pts[2], pts[3], pts[4], + pts[5]); + result += buf; break; case PathVerb::Close: - oss << "Z"; + result += "Z"; break; } pointIndex += PathData::PointsPerVerb(verb) * 2; } - return oss.str(); + return result; } // SVG path parser helper functions @@ -760,6 +757,80 @@ PathData PathDataFromSVGString(const std::string& d) { return path; } +//============================================================================== +// String parsing utilities +//============================================================================== + +static std::string TrimString(const std::string& str) { + auto start = str.find_first_not_of(" \t\n\r"); + if (start == std::string::npos) { + return ""; + } + auto end = str.find_last_not_of(" \t\n\r"); + return str.substr(start, end - start + 1); +} + +std::vector SplitString(const std::string& str, char delimiter) { + std::vector tokens; + size_t start = 0; + size_t end = 0; + while ((end = str.find(delimiter, start)) != std::string::npos) { + auto token = TrimString(str.substr(start, end - start)); + if (!token.empty()) { + tokens.push_back(token); + } + start = end + 1; + } + auto lastToken = TrimString(str.substr(start)); + if (!lastToken.empty()) { + tokens.push_back(lastToken); + } + return tokens; +} + +std::vector ParseFloatList(const std::string& str) { + std::vector values; + auto tokens = SplitString(str, ','); + values.reserve(tokens.size()); + for (const auto& token : tokens) { + char* endPtr = nullptr; + float val = strtof(token.c_str(), &endPtr); + if (endPtr != token.c_str()) { + values.push_back(val); + } + } + return values; +} + +std::vector ParseSpaceSeparatedFloats(const std::string& str) { + std::vector values; + const char* ptr = str.c_str(); + const char* end = ptr + str.size(); + while (ptr < end) { + // Skip whitespace. + while (ptr < end && std::isspace(*ptr)) { + ++ptr; + } + if (ptr >= end) { + break; + } + char* endPtr = nullptr; + float val = strtof(ptr, &endPtr); + if (endPtr == ptr) { + break; + } + values.push_back(val); + ptr = endPtr; + } + return values; +} + +std::string FloatToString(float value) { + char buf[32] = {}; + snprintf(buf, sizeof(buf), "%g", value); + return std::string(buf); +} + #undef DEFINE_ENUM_CONVERSION } // namespace pagx diff --git a/pagx/src/PAGXStringUtils.h b/pagx/src/PAGXStringUtils.h index 5f6b95c6b6..7cd6620928 100644 --- a/pagx/src/PAGXStringUtils.h +++ b/pagx/src/PAGXStringUtils.h @@ -19,6 +19,7 @@ #pragma once #include +#include #include "pagx/nodes/ColorSource.h" #include "pagx/nodes/Element.h" #include "pagx/nodes/Fill.h" @@ -168,4 +169,20 @@ Matrix MatrixFromString(const std::string& str); PathData PathDataFromSVGString(const std::string& d); std::string PathDataToSVGString(const PathData& path); +//============================================================================== +// String parsing utilities (avoid std::stringstream for smaller binary size) +//============================================================================== + +// Split a string by delimiter and return a vector of trimmed tokens. +std::vector SplitString(const std::string& str, char delimiter); + +// Parse comma-separated float values. +std::vector ParseFloatList(const std::string& str); + +// Parse space-separated float values. +std::vector ParseSpaceSeparatedFloats(const std::string& str); + +// Format a float value to string (removes trailing zeros). +std::string FloatToString(float value); + } // namespace pagx diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index b958b86a96..af10b82f55 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -19,8 +19,8 @@ #include "pagx/SVGImporter.h" #include #include +#include #include -#include #include "PAGXStringUtils.h" #include "pagx/PAGXDocument.h" #include "pagx/nodes/SolidColor.h" @@ -1248,12 +1248,15 @@ Color SVGParserImpl::parseColor(const std::string& value) { size_t end = value.find(')'); if (start != std::string::npos && end != std::string::npos) { std::string inner = value.substr(start + 1, end - start - 1); - std::istringstream iss(inner); + auto components = ParseFloatList(inner); float r = 0, g = 0, b = 0, a = 1.0f; - char comma = 0; - iss >> r >> comma >> g >> comma >> b; - if (value.find("rgba") == 0) { - iss >> comma >> a; + if (components.size() >= 3) { + r = components[0]; + g = components[1]; + b = components[2]; + if (components.size() >= 4) { + a = components[3]; + } } return {r / 255.0f, g / 255.0f, b / 255.0f, a, ColorSpace::SRGB}; } @@ -1291,22 +1294,36 @@ Color SVGParserImpl::parseColor(const std::string& value) { inner.erase(inner.find_last_not_of(" \t") + 1); // Parse space-separated values and optional "/ alpha" - std::istringstream iss(inner); - std::vector components = {}; - std::string token = {}; + std::vector components; float alpha = 1.0f; + const char* ptr = inner.c_str(); + const char* endPtr = ptr + inner.size(); bool foundSlash = false; - while (iss >> token) { - if (token == "/") { + while (ptr < endPtr) { + // Skip whitespace. + while (ptr < endPtr && std::isspace(*ptr)) { + ++ptr; + } + if (ptr >= endPtr) { + break; + } + // Check for slash separator. + if (*ptr == '/') { foundSlash = true; + ++ptr; continue; } - float val = std::stof(token); + char* numEnd = nullptr; + float val = strtof(ptr, &numEnd); + if (numEnd == ptr) { + break; + } if (foundSlash) { alpha = val; } else { components.push_back(val); } + ptr = numEnd; } if (components.size() >= 3) { Color color = {}; @@ -1521,22 +1538,34 @@ std::string SVGParserImpl::colorToHex(const std::string& value) { inner.erase(0, inner.find_first_not_of(" \t")); inner.erase(inner.find_last_not_of(" \t") + 1); // Parse space-separated values and optional "/ alpha" - std::istringstream iss(inner); - std::vector components = {}; - std::string token = {}; + std::vector components; float alpha = 1.0f; + const char* ptr = inner.c_str(); + const char* endPtr = ptr + inner.size(); bool foundSlash = false; - while (iss >> token) { - if (token == "/") { + while (ptr < endPtr) { + while (ptr < endPtr && std::isspace(*ptr)) { + ++ptr; + } + if (ptr >= endPtr) { + break; + } + if (*ptr == '/') { foundSlash = true; + ++ptr; continue; } - float val = std::stof(token); + char* numEnd = nullptr; + float val = strtof(ptr, &numEnd); + if (numEnd == ptr) { + break; + } if (foundSlash) { alpha = val; } else { components.push_back(val); } + ptr = numEnd; } if (components.size() >= 3) { char buf[64] = {}; @@ -1603,18 +1632,10 @@ float SVGParserImpl::parseLength(const std::string& value, float containerSize) } std::vector SVGParserImpl::parseViewBox(const std::string& value) { - std::vector result; if (value.empty()) { - return result; - } - - std::istringstream iss(value); - float num = 0; - while (iss >> num) { - result.push_back(num); + return {}; } - - return result; + return ParseSpaceSeparatedFloats(value); } PathData SVGParserImpl::parsePoints(const std::string& value, bool closed) { @@ -1623,16 +1644,25 @@ PathData SVGParserImpl::parsePoints(const std::string& value, bool closed) { return path; } + // Parse space/comma-separated float values. std::vector points; - std::istringstream iss(value); - float num = 0; - // The >> operator automatically skips whitespace, so we just need to handle commas. - while (iss >> num) { - points.push_back(num); - // Skip optional comma separator (whitespace is handled automatically by >>). - if (iss.peek() == ',') { - iss.get(); + const char* ptr = value.c_str(); + const char* end = ptr + value.size(); + while (ptr < end) { + // Skip whitespace and commas. + while (ptr < end && (std::isspace(*ptr) || *ptr == ',')) { + ++ptr; + } + if (ptr >= end) { + break; } + char* numEnd = nullptr; + float num = strtof(ptr, &numEnd); + if (numEnd == ptr) { + break; + } + points.push_back(num); + ptr = numEnd; } if (points.size() >= 2) { @@ -1902,32 +1932,28 @@ bool SVGParserImpl::convertFilterElement( float offsetX = 0, offsetY = 0; std::string dx = getAttribute(primitives[i + 1], "dx", "0"); std::string dy = getAttribute(primitives[i + 1], "dy", "0"); - offsetX = std::stof(dx); - offsetY = std::stof(dy); + offsetX = strtof(dx.c_str(), nullptr); + offsetY = strtof(dy.c_str(), nullptr); // Extract blur from feGaussianBlur. - float blurX = 0, blurY = 0; std::string stdDeviation = getAttribute(primitives[i + 2], "stdDeviation", "0"); - std::istringstream iss(stdDeviation); - iss >> blurX; - if (!(iss >> blurY)) { - blurY = blurX; - } + auto blurValues = ParseSpaceSeparatedFloats(stdDeviation); + float blurX = blurValues.empty() ? 0 : blurValues[0]; + float blurY = blurValues.size() > 1 ? blurValues[1] : blurX; // Extract color from the second feColorMatrix. // Format: "0 0 0 0 R 0 0 0 0 G 0 0 0 0 B 0 0 0 A 0" where R,G,B are 0-1 and A is alpha. Color shadowColor = {0, 0, 0, 1.0f}; std::string colorMatrix = getAttribute(primitives[i + 4], "values"); if (!colorMatrix.empty()) { - std::istringstream cms(colorMatrix); - float values[20] = {}; - for (int j = 0; j < 20 && cms >> values[j]; j++) { - } + auto values = ParseSpaceSeparatedFloats(colorMatrix); // R is at index 4, G at index 9, B at index 14, A at index 18. - shadowColor.red = values[4]; - shadowColor.green = values[9]; - shadowColor.blue = values[14]; - shadowColor.alpha = values[18]; + if (values.size() >= 19) { + shadowColor.red = values[4]; + shadowColor.green = values[9]; + shadowColor.blue = values[14]; + shadowColor.alpha = values[18]; + } } auto dropShadow = std::make_unique(); @@ -1968,12 +1994,9 @@ bool SVGParserImpl::convertFilterElement( if (isSourceGraphic) { std::string stdDeviation = getAttribute(node, "stdDeviation", "0"); - std::istringstream iss(stdDeviation); - float devX = 0, devY = 0; - iss >> devX; - if (!(iss >> devY)) { - devY = devX; - } + auto devValues = ParseSpaceSeparatedFloats(stdDeviation); + float devX = devValues.empty() ? 0 : devValues[0]; + float devY = devValues.size() > 1 ? devValues[1] : devX; auto blurFilter = std::make_unique(); blurFilter->blurrinessX = devX; blurFilter->blurrinessY = devY; diff --git a/pagx/src/xml/XMLParser.cpp b/pagx/src/xml/XMLParser.cpp index 3f5d497ec8..8fce30d582 100644 --- a/pagx/src/xml/XMLParser.cpp +++ b/pagx/src/xml/XMLParser.cpp @@ -17,10 +17,9 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "XMLParser.h" +#include #include -#include #include -#include #include #include "expat.h" @@ -151,14 +150,28 @@ bool XMLParser::parse(const uint8_t* data, size_t length) { } bool XMLParser::parseFile(const std::string& filePath) { - std::ifstream file(filePath, std::ios::binary); - if (!file.is_open()) { + FILE* file = fopen(filePath.c_str(), "rb"); + if (!file) { return false; } - std::stringstream buffer; - buffer << file.rdbuf(); - std::string content = buffer.str(); + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + if (fileSize <= 0) { + fclose(file); + return false; + } + + std::string content; + content.resize(static_cast(fileSize)); + size_t bytesRead = fread(&content[0], 1, static_cast(fileSize), file); + fclose(file); + + if (bytesRead != static_cast(fileSize)) { + return false; + } return parse(reinterpret_cast(content.data()), content.size()); } From eaa0719426e398f0514bf137bffc22bd2972dec6 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 22 Jan 2026 19:14:51 +0800 Subject: [PATCH 141/678] Fix SVG shadow-only filter rendering and add tspan text support. --- pagx/src/svg/SVGImporter.cpp | 287 +++++++++++++++++++++++++++++-- pagx/src/svg/SVGParserInternal.h | 8 +- pagx/src/tgfx/LayerBuilder.cpp | 3 +- 3 files changed, 285 insertions(+), 13 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index af10b82f55..cbe93fcf41 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -83,8 +83,79 @@ std::shared_ptr SVGParserImpl::parseFile(const std::string& filePa std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, const std::string& name, const std::string& defaultValue) const { + // CSS priority: style attribute > presentation attribute (direct attribute) + // Check style attribute first for CSS property. + // Style attribute format: "property1: value1; property2: value2; ..." + // CSS cascade rule: later properties override earlier ones. + auto [hasStyle, styleStr] = node->findAttribute("style"); + if (hasStyle && !styleStr.empty()) { + std::string propName = name; + std::string lastValue; + size_t pos = 0; + while (pos < styleStr.size()) { + // Skip whitespace. + while (pos < styleStr.size() && std::isspace(styleStr[pos])) { + ++pos; + } + // Find property name end (colon). + size_t colonPos = styleStr.find(':', pos); + if (colonPos == std::string::npos) { + break; + } + // Extract property name and trim whitespace. + std::string currentProp = styleStr.substr(pos, colonPos - pos); + size_t propStart = currentProp.find_first_not_of(" \t"); + size_t propEnd = currentProp.find_last_not_of(" \t"); + if (propStart != std::string::npos && propEnd != std::string::npos) { + currentProp = currentProp.substr(propStart, propEnd - propStart + 1); + } + // Find value end (semicolon or end of string). + // Special case: CSS color() function may contain parentheses, + // so find the next semicolon that's not inside parentheses. + size_t searchStart = colonPos + 1; + size_t semicolonPos = std::string::npos; + int parenDepth = 0; + for (size_t i = searchStart; i < styleStr.size(); i++) { + if (styleStr[i] == '(') { + parenDepth++; + } else if (styleStr[i] == ')') { + parenDepth--; + } else if (styleStr[i] == ';' && parenDepth == 0) { + semicolonPos = i; + break; + } + } + if (semicolonPos == std::string::npos) { + semicolonPos = styleStr.size(); + } + // Check if this is the property we're looking for. + if (currentProp == propName) { + // Extract and trim the value. + std::string propValue = styleStr.substr(colonPos + 1, semicolonPos - colonPos - 1); + size_t valStart = propValue.find_first_not_of(" \t"); + size_t valEnd = propValue.find_last_not_of(" \t"); + if (valStart != std::string::npos && valEnd != std::string::npos) { + lastValue = propValue.substr(valStart, valEnd - valStart + 1); + } else { + lastValue = propValue; + } + // Continue searching for later occurrences (CSS cascade). + } + // Move to next property. + pos = semicolonPos + 1; + } + if (!lastValue.empty()) { + return lastValue; + } + } + + // Fallback: check for direct attribute (presentation attribute). auto [found, value] = node->findAttribute(name); - return found ? value : defaultValue; + if (found) { + return value; + } + + return defaultValue; } std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& dom) { @@ -319,11 +390,13 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrsecond, layer->filters, layer->styles); + bool filterConverted = convertFilterElement(filterIt->second, layer->filters, layer->styles, + &hasShadowOnlyFilter); if (!filterConverted) { // Filter could not be converted to PAGX format. Skip this element entirely to avoid // rendering incorrect results (e.g., black fill instead of shadow effect). @@ -345,7 +418,9 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptrcontents, inheritedStyle); + // If this element has a shadow-only filter, skip adding fill because the fill is only + // used as shadow source, not to be rendered directly. + convertChildren(element, layer->contents, inheritedStyle, hasShadowOnlyFilter); } return layer; @@ -353,7 +428,8 @@ std::unique_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, std::vector>& contents, - const InheritedStyle& inheritedStyle) { + const InheritedStyle& inheritedStyle, + bool skipFillForShadow) { const auto& tag = element->name; // Handle text element specially - it returns a Group with TextSpan. @@ -365,12 +441,29 @@ void SVGParserImpl::convertChildren(const std::shared_ptr& element, return; } + // Check if this is a use element referencing an image. + // In that case, we don't add fill/stroke because the image already has its own fill. + bool skipFillStroke = skipFillForShadow; + if (tag == "use") { + std::string href = getAttribute(element, "xlink:href"); + if (href.empty()) { + href = getAttribute(element, "href"); + } + std::string refId = resolveUrl(href); + auto it = _defs.find(refId); + if (it != _defs.end() && it->second->name == "image") { + skipFillStroke = true; + } + } + auto shapeElement = convertElement(element); if (shapeElement) { contents.push_back(std::move(shapeElement)); } - addFillStroke(element, contents, inheritedStyle); + if (!skipFillStroke) { + addFillStroke(element, contents, inheritedStyle); + } } std::unique_ptr SVGParserImpl::convertElement( @@ -537,12 +630,21 @@ std::unique_ptr SVGParserImpl::convertText(const std::shared_ptr // SVG values: start (default), middle, end. std::string anchor = getAttribute(element, "text-anchor"); - // Get text content from child text nodes. + // Get text content from child text nodes and tspan elements. std::string textContent; auto child = element->getFirstChild(); while (child) { if (child->type == DOMNodeType::Text) { textContent += child->name; + } else if (child->name == "tspan") { + // Handle tspan element: extract text content from its children. + auto tspanChild = child->getFirstChild(); + while (tspanChild) { + if (tspanChild->type == DOMNodeType::Text) { + textContent += tspanChild->name; + } + tspanChild = tspanChild->getNextSibling(); + } } child = child->getNextSibling(); } @@ -599,11 +701,59 @@ std::unique_ptr SVGParserImpl::convertUse( return nullptr; } + // Parse position offset from use element. + float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); + float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); + + // Check if referenced element is an image. + if (it->second->name == "image") { + std::string imageHref = getAttribute(it->second, "xlink:href"); + if (imageHref.empty()) { + imageHref = getAttribute(it->second, "href"); + } + if (imageHref.empty()) { + return nullptr; + } + + // Get image dimensions from the referenced image element. + // Note: The transform on the use element is handled by convertToLayer, + // which sets it on the Layer's matrix. So here we just use the original + // image dimensions without applying the transform again. + float imageWidth = parseLength(getAttribute(it->second, "width"), _viewBoxWidth); + float imageHeight = parseLength(getAttribute(it->second, "height"), _viewBoxHeight); + + // Register the image resource. + std::string resourceId = registerImageResource(imageHref); + + // Create a rectangle to display the image at original size. + // The transform will be applied by the parent Layer's matrix. + auto rect = std::make_unique(); + rect->center.x = x + imageWidth / 2; + rect->center.y = y + imageHeight / 2; + rect->size.width = imageWidth; + rect->size.height = imageHeight; + + // Create an ImagePattern fill for the rectangle. + auto pattern = std::make_unique(); + pattern->image = "#" + resourceId; + // Position the pattern at the rectangle's origin. + pattern->matrix = Matrix::Translate(x, y); + + // Create a fill with the image pattern. + auto fill = std::make_unique(); + fill->color = std::move(pattern); + + // Create a group containing the rectangle and fill. + auto group = std::make_unique(); + group->elements.push_back(std::move(rect)); + group->elements.push_back(std::move(fill)); + + return group; + } + if (_options.expandUseReferences) { auto node = convertElement(it->second); if (node) { - float x = parseLength(getAttribute(element, "x"), _viewBoxWidth); - float y = parseLength(getAttribute(element, "y"), _viewBoxHeight); if (x != 0 || y != 0) { // Wrap in a group with translation. auto group = std::make_unique(); @@ -1901,19 +2051,32 @@ std::unique_ptr SVGParserImpl::convertMaskElement( bool SVGParserImpl::convertFilterElement( const std::shared_ptr& filterElement, std::vector>& filters, - std::vector>& styles) { + std::vector>& styles, + bool* outShadowOnly) { size_t initialFilterCount = filters.size(); size_t initialStyleCount = styles.size(); // Collect all filter primitives for analysis. std::vector> primitives; + bool hasMerge = false; auto child = filterElement->getFirstChild(); while (child) { if (!child->name.empty() && child->name.substr(0, 2) == "fe") { primitives.push_back(child); + // Check if filter merges shadow with SourceGraphic. + // If feMerge or feBlend references SourceGraphic, shadow is composited with original. + if (child->name == "feMerge" || child->name == "feBlend") { + hasMerge = true; + } } child = child->getNextSibling(); } + // If filter only produces shadow without merging with original content, + // we should set shadowOnly=true. + bool shadowOnly = !hasMerge; + if (outShadowOnly != nullptr) { + *outShadowOnly = shadowOnly; + } // Detect Figma-style drop shadow pattern and extract shadow parameters. // Pattern: feColorMatrix(in=SourceAlpha) → feOffset → feGaussianBlur → feComposite → feColorMatrix @@ -1962,7 +2125,7 @@ bool SVGParserImpl::convertFilterElement( dropShadow->blurrinessX = blurX; dropShadow->blurrinessY = blurY; dropShadow->color = shadowColor; - dropShadow->shadowOnly = false; + dropShadow->shadowOnly = shadowOnly; filters.push_back(std::move(dropShadow)); // Skip the consumed primitives (5 elements) plus the feBlend that follows. @@ -1974,6 +2137,110 @@ bool SVGParserImpl::convertFilterElement( } } + // Check for standard drop shadow pattern starting with feGaussianBlur in="SourceAlpha". + // Pattern: feGaussianBlur(in=SourceAlpha) → feOffset → ... + if (node->name == "feGaussianBlur" && getAttribute(node, "in") == "SourceAlpha") { + // Look for feOffset following the blur. + if (i + 1 < primitives.size() && primitives[i + 1]->name == "feOffset") { + // Extract blur from feGaussianBlur. + std::string stdDeviation = getAttribute(node, "stdDeviation", "0"); + auto blurValues = ParseSpaceSeparatedFloats(stdDeviation); + float blurX = blurValues.empty() ? 0 : blurValues[0]; + float blurY = blurValues.size() > 1 ? blurValues[1] : blurX; + + // Extract offset from feOffset. + float offsetX = 0, offsetY = 0; + std::string dx = getAttribute(primitives[i + 1], "dx", "0"); + std::string dy = getAttribute(primitives[i + 1], "dy", "0"); + offsetX = strtof(dx.c_str(), nullptr); + offsetY = strtof(dy.c_str(), nullptr); + + // Look for feColorMatrix after feOffset for shadow color. + Color shadowColor = {0, 0, 0, 1.0f}; + size_t colorMatrixIdx = i + 2; + if (colorMatrixIdx < primitives.size() && primitives[colorMatrixIdx]->name == "feColorMatrix") { + std::string colorMatrix = getAttribute(primitives[colorMatrixIdx], "values"); + if (!colorMatrix.empty()) { + auto values = ParseSpaceSeparatedFloats(colorMatrix); + if (values.size() >= 19) { + shadowColor.red = values[4]; + shadowColor.green = values[9]; + shadowColor.blue = values[14]; + shadowColor.alpha = values[18]; + } + } + } + + auto dropShadow = std::make_unique(); + dropShadow->offsetX = offsetX; + dropShadow->offsetY = offsetY; + dropShadow->blurrinessX = blurX; + dropShadow->blurrinessY = blurY; + dropShadow->color = shadowColor; + dropShadow->shadowOnly = shadowOnly; + filters.push_back(std::move(dropShadow)); + + // Skip consumed primitives. + i += 2; + if (i < primitives.size() && primitives[i]->name == "feColorMatrix") { + i++; + } + continue; + } + } + + // Check for another standard drop shadow pattern: feOffset(in=SourceAlpha) → feGaussianBlur → feColorMatrix + // Used by Sketch and other tools. + if (node->name == "feOffset" && getAttribute(node, "in") == "SourceAlpha") { + // Look for feGaussianBlur following. + if (i + 1 < primitives.size() && primitives[i + 1]->name == "feGaussianBlur") { + // Extract offset from feOffset. + float offsetX = 0, offsetY = 0; + std::string dx = getAttribute(node, "dx", "0"); + std::string dy = getAttribute(node, "dy", "0"); + offsetX = strtof(dx.c_str(), nullptr); + offsetY = strtof(dy.c_str(), nullptr); + + // Extract blur from feGaussianBlur. + std::string stdDeviation = getAttribute(primitives[i + 1], "stdDeviation", "0"); + auto blurValues = ParseSpaceSeparatedFloats(stdDeviation); + float blurX = blurValues.empty() ? 0 : blurValues[0]; + float blurY = blurValues.size() > 1 ? blurValues[1] : blurX; + + // Look for feColorMatrix after feGaussianBlur for shadow color. + Color shadowColor = {0, 0, 0, 1.0f}; + size_t colorMatrixIdx = i + 2; + if (colorMatrixIdx < primitives.size() && primitives[colorMatrixIdx]->name == "feColorMatrix") { + std::string colorMatrix = getAttribute(primitives[colorMatrixIdx], "values"); + if (!colorMatrix.empty()) { + auto values = ParseSpaceSeparatedFloats(colorMatrix); + if (values.size() >= 19) { + shadowColor.red = values[4]; + shadowColor.green = values[9]; + shadowColor.blue = values[14]; + shadowColor.alpha = values[18]; + } + } + } + + auto dropShadow = std::make_unique(); + dropShadow->offsetX = offsetX; + dropShadow->offsetY = offsetY; + dropShadow->blurrinessX = blurX; + dropShadow->blurrinessY = blurY; + dropShadow->color = shadowColor; + dropShadow->shadowOnly = shadowOnly; + filters.push_back(std::move(dropShadow)); + + // Skip consumed primitives. + i += 2; + if (i < primitives.size() && primitives[i]->name == "feColorMatrix") { + i++; + } + continue; + } + } + // Check for blur filter that should apply to the source graphic. // This includes: // 1. feGaussianBlur with in="SourceGraphic" diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 8fa156f68c..48ca93fc10 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -74,7 +74,8 @@ class SVGParserImpl { const InheritedStyle& parentStyle); void convertChildren(const std::shared_ptr& element, std::vector>& contents, - const InheritedStyle& inheritedStyle); + const InheritedStyle& inheritedStyle, + bool skipFillForShadow = false); std::unique_ptr convertElement(const std::shared_ptr& element); std::unique_ptr convertG(const std::shared_ptr& element, const InheritedStyle& inheritedStyle); @@ -100,9 +101,12 @@ class SVGParserImpl { const InheritedStyle& parentStyle); // Converts SVG filter element to PAGX filters/styles. // Returns true if the filter was successfully converted, false otherwise. + // If outShadowOnly is provided, it will be set to true if all converted filters are shadow-only + // (i.e., they produce only shadow without the original content). bool convertFilterElement(const std::shared_ptr& filterElement, std::vector>& filters, - std::vector>& styles); + std::vector>& styles, + bool* outShadowOnly = nullptr); void addFillStroke(const std::shared_ptr& element, std::vector>& contents, diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index b689e95a6f..d36879823a 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -868,7 +868,8 @@ class LayerBuilderImpl { case NodeType::DropShadowFilter: { auto filter = static_cast(node); return tgfx::DropShadowFilter::Make(filter->offsetX, filter->offsetY, filter->blurrinessX, - filter->blurrinessY, ToTGFX(filter->color)); + filter->blurrinessY, ToTGFX(filter->color), + filter->shadowOnly); } default: return nullptr; From 8f9eebaeb0bae7ba89f5653406309a5c2d462061 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Thu, 22 Jan 2026 20:38:26 +0800 Subject: [PATCH 142/678] Add WeChat Mini Program support for PAGX viewer. --- pagx/wechat/.gitignore | 12 + pagx/wechat/CMakeLists.txt | 72 ++++ pagx/wechat/package.json | 32 ++ pagx/wechat/script/cmake.wx.js | 45 ++ pagx/wechat/script/copy-files.js | 192 +++++++++ pagx/wechat/script/plugin/replace-config.js | 318 ++++++++++++++ .../script/plugin/rollup-plugin-replace.js | 30 ++ pagx/wechat/script/rollup.wx.js | 78 ++++ pagx/wechat/script/setup.emsdk.wx.js | 42 ++ pagx/wechat/server.js | 48 +++ pagx/wechat/src/GridBackground.cpp | 65 +++ pagx/wechat/src/GridBackground.h | 42 ++ pagx/wechat/src/PAGXView.cpp | 213 ++++++++++ pagx/wechat/src/PAGXView.h | 80 ++++ pagx/wechat/src/binding.cpp | 34 ++ pagx/wechat/ts/babel.ts | 11 + pagx/wechat/ts/backend-context.ts | 99 +++++ pagx/wechat/ts/binding.ts | 36 ++ pagx/wechat/ts/gesture-manager.ts | 370 ++++++++++++++++ pagx/wechat/ts/interfaces.ts | 81 ++++ pagx/wechat/ts/pagx-view.ts | 234 ++++++++++ pagx/wechat/ts/pagx.ts | 28 ++ pagx/wechat/ts/render-canvas.ts | 115 +++++ pagx/wechat/ts/types.ts | 22 + pagx/wechat/tsconfig.json | 27 ++ pagx/wechat/tsconfig.type.json | 22 + pagx/wechat/wx_demo/.gitignore | 3 + pagx/wechat/wx_demo/BUILD.md | 162 +++++++ pagx/wechat/wx_demo/QUICKSTART.md | 142 +++++++ pagx/wechat/wx_demo/README.md | 175 ++++++++ pagx/wechat/wx_demo/app.js | 9 + pagx/wechat/wx_demo/app.json | 12 + pagx/wechat/wx_demo/pages/viewer/viewer.js | 400 ++++++++++++++++++ pagx/wechat/wx_demo/pages/viewer/viewer.json | 5 + pagx/wechat/wx_demo/pages/viewer/viewer.wxml | 51 +++ pagx/wechat/wx_demo/pages/viewer/viewer.wxss | 131 ++++++ pagx/wechat/wx_demo/project.config.json | 56 +++ .../wx_demo/project.private.config.json | 23 + pagx/wechat/wx_demo/sitemap.json | 7 + 39 files changed, 3524 insertions(+) create mode 100644 pagx/wechat/.gitignore create mode 100644 pagx/wechat/CMakeLists.txt create mode 100644 pagx/wechat/package.json create mode 100644 pagx/wechat/script/cmake.wx.js create mode 100644 pagx/wechat/script/copy-files.js create mode 100644 pagx/wechat/script/plugin/replace-config.js create mode 100644 pagx/wechat/script/plugin/rollup-plugin-replace.js create mode 100644 pagx/wechat/script/rollup.wx.js create mode 100644 pagx/wechat/script/setup.emsdk.wx.js create mode 100644 pagx/wechat/server.js create mode 100644 pagx/wechat/src/GridBackground.cpp create mode 100644 pagx/wechat/src/GridBackground.h create mode 100644 pagx/wechat/src/PAGXView.cpp create mode 100644 pagx/wechat/src/PAGXView.h create mode 100644 pagx/wechat/src/binding.cpp create mode 100644 pagx/wechat/ts/babel.ts create mode 100644 pagx/wechat/ts/backend-context.ts create mode 100644 pagx/wechat/ts/binding.ts create mode 100644 pagx/wechat/ts/gesture-manager.ts create mode 100644 pagx/wechat/ts/interfaces.ts create mode 100644 pagx/wechat/ts/pagx-view.ts create mode 100644 pagx/wechat/ts/pagx.ts create mode 100644 pagx/wechat/ts/render-canvas.ts create mode 100644 pagx/wechat/ts/types.ts create mode 100644 pagx/wechat/tsconfig.json create mode 100644 pagx/wechat/tsconfig.type.json create mode 100644 pagx/wechat/wx_demo/.gitignore create mode 100644 pagx/wechat/wx_demo/BUILD.md create mode 100644 pagx/wechat/wx_demo/QUICKSTART.md create mode 100644 pagx/wechat/wx_demo/README.md create mode 100644 pagx/wechat/wx_demo/app.js create mode 100644 pagx/wechat/wx_demo/app.json create mode 100644 pagx/wechat/wx_demo/pages/viewer/viewer.js create mode 100644 pagx/wechat/wx_demo/pages/viewer/viewer.json create mode 100644 pagx/wechat/wx_demo/pages/viewer/viewer.wxml create mode 100644 pagx/wechat/wx_demo/pages/viewer/viewer.wxss create mode 100644 pagx/wechat/wx_demo/project.config.json create mode 100644 pagx/wechat/wx_demo/project.private.config.json create mode 100644 pagx/wechat/wx_demo/sitemap.json diff --git a/pagx/wechat/.gitignore b/pagx/wechat/.gitignore new file mode 100644 index 0000000000..4807e5b198 --- /dev/null +++ b/pagx/wechat/.gitignore @@ -0,0 +1,12 @@ +# Build outputs +wasm-mt/ +wasm/ +dist/ + +# Build cache +script/build-pagx-viewer/ +script/wasm-mt/ +script/.build.lock +.*.md5 +package-lock.json +ts/wasm diff --git a/pagx/wechat/CMakeLists.txt b/pagx/wechat/CMakeLists.txt new file mode 100644 index 0000000000..103cc2f4a9 --- /dev/null +++ b/pagx/wechat/CMakeLists.txt @@ -0,0 +1,72 @@ +cmake_minimum_required(VERSION 3.13) +project(PAGXViewer) + +# Include vendor tools from libpag root +include(${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/vendor_tools/vendor.cmake) + +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 17) + +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") +endif () + +# Set TGFX_DIR for libpag structure (tgfx is in third_party/tgfx) +if (NOT TGFX_DIR) + set(TGFX_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/tgfx) +endif () +get_filename_component(TGFX_DIR "${TGFX_DIR}" REALPATH) + +# Add tgfx and pagx as dependencies +if (NOT TARGET tgfx) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + set(TGFX_BUILD_SVG ON CACHE BOOL "" FORCE) + set(TGFX_BUILD_LAYERS ON CACHE BOOL "" FORCE) + add_subdirectory(${TGFX_DIR} ${CMAKE_CURRENT_BINARY_DIR}/tgfx) +endif () + +if (NOT TARGET pagx) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/pagx) +endif () + +# For Emscripten builds, pagx needs the same compile options as tgfx +if (DEFINED EMSCRIPTEN) + target_compile_options(pagx PRIVATE -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0) +endif () + +file(GLOB_RECURSE PAGX_VIEWER_FILES src/*.cpp) + +if (DEFINED EMSCRIPTEN) + add_executable(pagx-viewer ${PAGX_VIEWER_FILES}) + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0) + list(APPEND PAGX_VIEWER_LINK_OPTIONS --no-entry -lembind -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 -sEXPORT_NAME='PAGXWasm' -sWASM=1 + -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['GL','HEAPU8'] -sMODULARIZE=1 + -sENVIRONMENT=web,worker -sEXPORT_ES6=1) + set(unsupported_emsdk_versions "4.0.11") + foreach (unsupported_version IN LISTS unsupported_emsdk_versions) + if (${EMSCRIPTEN_VERSION} VERSION_EQUAL ${unsupported_version}) + message(FATAL_ERROR "Emscripten version ${EMSCRIPTEN_VERSION} is not supported.") + endif () + endforeach () + if (EMSCRIPTEN_PTHREADS) + list(APPEND PAGX_VIEWER_LINK_OPTIONS -sUSE_PTHREADS=1 -sINITIAL_MEMORY=32MB -sALLOW_MEMORY_GROWTH=1 + -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency + -sEXIT_RUNTIME=0 -sINVOKE_RUN=0 -sMALLOC=mimalloc) + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -fPIC -pthread) + else () + list(APPEND PAGX_VIEWER_LINK_OPTIONS -sALLOW_MEMORY_GROWTH=1) + endif () + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -O0 -g3) + list(APPEND PAGX_VIEWER_LINK_OPTIONS -O0 -g3 -sSAFE_HEAP=1 -Wno-limited-postlink-optimizations) + else () + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -Oz) + list(APPEND PAGX_VIEWER_LINK_OPTIONS -Oz) + endif () +else () + add_library(pagx-viewer SHARED ${PAGX_VIEWER_FILES}) +endif () + +target_compile_options(pagx-viewer PUBLIC ${PAGX_VIEWER_COMPILE_OPTIONS}) +target_link_options(pagx-viewer PUBLIC ${PAGX_VIEWER_LINK_OPTIONS}) +target_link_libraries(pagx-viewer pagx) diff --git a/pagx/wechat/package.json b/pagx/wechat/package.json new file mode 100644 index 0000000000..3d967d1661 --- /dev/null +++ b/pagx/wechat/package.json @@ -0,0 +1,32 @@ +{ + "name": "pagx-viewer", + "version": "1.0.0", + "description": "PAGX File Viewer", + "type": "module", + "scripts": { + "clean": "rimraf --glob .pagx-viewer.wasm.md5", + "build:wechat:js": "rollup -c ./script/rollup.wx.js && node script/copy-files.js", + "build:wechat": "npm run clean && node script/cmake.wx.js -a wasm && brotli -f ./ts/wasm/pagx-viewer.wasm && npm run build:wechat:js", + "server": "node server.js" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "~28.0.3", + "@rollup/plugin-json": "~6.1.0", + "@rollup/plugin-node-resolve": "~16.0.1", + "@rollup/plugin-alias": "^5.0.0", + "@types/emscripten": "~1.39.6", + "esbuild": "~0.15.14", + "rimraf": "~5.0.10", + "rollup": "~2.79.1", + "rollup-plugin-esbuild": "~4.10.3", + "rollup-plugin-terser": "~7.0.2", + "tslib": "~2.4.1", + "typescript": "~5.0.3" + }, + "dependencies": { + "express": "^4.21.1" + }, + "license": "BSD-3-Clause", + "author": "Tencent", + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} \ No newline at end of file diff --git a/pagx/wechat/script/cmake.wx.js b/pagx/wechat/script/cmake.wx.js new file mode 100644 index 0000000000..e159923ce8 --- /dev/null +++ b/pagx/wechat/script/cmake.wx.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import { fileURLToPath } from 'url'; +import path from 'path'; +import fs, {copyFileSync} from "fs"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +process.chdir(__dirname); + +process.argv.push("-s"); +process.argv.push("../"); +process.argv.push("-o"); +process.argv.push("../"); +process.argv.push("-p"); +process.argv.push("web"); +process.argv.push("pagx-viewer"); + +await import('./setup.emsdk.wx.js'); +await import("../../../third_party/vendor_tools/lib-build"); + + +if (!fs.existsSync("../ts/wasm")) { + fs.mkdirSync("../ts/wasm", {recursive: true}); +} + +fs.copyFileSync("../wasm/pagx-viewer.wasm", "../ts/wasm/pagx-viewer.wasm") diff --git a/pagx/wechat/script/copy-files.js b/pagx/wechat/script/copy-files.js new file mode 100644 index 0000000000..ee6a7ac09b --- /dev/null +++ b/pagx/wechat/script/copy-files.js @@ -0,0 +1,192 @@ +#!/usr/bin/env node + +import fs from 'fs'; +import path from 'path'; + +/** + * Copy files from source directory to target directory based on filename patterns + * @param {string} sourceDir - Source directory path + * @param {string} targetDir - Target directory path + * @param {string|Array} filenamePatterns - Filename patterns to match (supports wildcards and regex) + * @param {Object} options - Additional options + * @param {boolean} options.recursive - Whether to search subdirectories recursively + * @param {boolean} options.overwrite - Whether to overwrite existing files + * @param {boolean} options.preserveStructure - Whether to preserve directory structure + */ +function copyFiles(sourceDir, targetDir, filenamePatterns, options = {}) { + const { + recursive = false, + overwrite = false, + preserveStructure = false + } = options; + + // Validate directories + if (!fs.existsSync(sourceDir)) { + throw new Error(`Source directory does not exist: ${sourceDir}`); + } + + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + // Convert single pattern to array + const patterns = Array.isArray(filenamePatterns) ? filenamePatterns : [filenamePatterns]; + + // Create regex patterns from wildcard patterns + const regexPatterns = patterns.map(pattern => { + // Convert wildcard pattern to regex + const regexStr = pattern + .replace(/\*/g, '.*') + .replace(/\?/g, '.') + .replace(/([.+^${}()|[\\]\])/g, '\\$1'); + return new RegExp(`^${regexStr}$`); + }); + + /** + * Recursively search for files matching patterns + */ + function searchFiles(dir, currentDepth = 0) { + const files = []; + + try { + const items = fs.readdirSync(dir); + + for (const item of items) { + const fullPath = path.join(dir, item); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + if (recursive) { + files.push(...searchFiles(fullPath, currentDepth + 1)); + } + } else if (stat.isFile()) { + // Check if filename matches any pattern + const matches = regexPatterns.some(regex => regex.test(item)); + if (matches) { + files.push({ + sourcePath: fullPath, + filename: item, + relativePath: path.relative(sourceDir, fullPath) + }); + } + } + } + } catch (error) { + console.error(`Error reading directory ${dir}:`, error.message); + } + + return files; + } + + // Find matching files + const matchingFiles = searchFiles(sourceDir); + + if (matchingFiles.length === 0) { + console.log('No files found matching the specified patterns.'); + return []; + } + + console.log(`Found ${matchingFiles.length} file(s) to copy:`); + + const copiedFiles = []; + + // Copy files + for (const file of matchingFiles) { + try { + let targetPath; + + if (preserveStructure && file.relativePath !== file.filename) { + // Preserve directory structure + const targetSubDir = path.dirname(file.relativePath); + const fullTargetDir = path.join(targetDir, targetSubDir); + + if (!fs.existsSync(fullTargetDir)) { + fs.mkdirSync(fullTargetDir, { recursive: true }); + } + + targetPath = path.join(fullTargetDir, file.filename); + } else { + // Copy to root of target directory + targetPath = path.join(targetDir, file.filename); + } + + // Check if target file exists + if (fs.existsSync(targetPath)) { + if (!overwrite) { + console.log(`Skipping ${file.filename} (already exists)`); + continue; + } + console.log(`Overwriting ${file.filename}`); + } else { + console.log(`Copying ${file.filename}`); + } + + // Copy file + fs.copyFileSync(file.sourcePath, targetPath); + copiedFiles.push({ + source: file.sourcePath, + target: targetPath, + filename: file.filename + }); + + } catch (error) { + console.error(`Error copying ${file.filename}:`, error.message); + } + } + + console.log(`Successfully copied ${copiedFiles.length} file(s)`); + return copiedFiles; +} + +/** + * Command line interface + */ +function main() { + const args = process.argv.slice(2); + + if (args.length < 3) { + console.log('Usage: node copy-files.js [options]'); + console.log(''); + console.log('Examples:'); + console.log(' node copy-files.js ./src ./dist "*.js"'); + console.log(' node copy-files.js ./src ./dist "*.ts" --recursive --overwrite'); + console.log(' node copy-files.js ./src ./dist "*.js;*.ts" --preserve-structure'); + console.log(''); + console.log('Options:'); + console.log(' --recursive Search subdirectories recursively'); + console.log(' --overwrite Overwrite existing files'); + console.log(' --preserve-structure Preserve directory structure'); + return; + } + + const sourceDir = args[0]; + const targetDir = args[1]; + const filenamePatterns = args[2].split(';'); // Support multiple patterns separated by semicolon + + const options = { + recursive: args.includes('--recursive'), + overwrite: args.includes('--overwrite'), + preserveStructure: args.includes('--preserve-structure') + }; + + try { + copyFiles(sourceDir, targetDir, filenamePatterns, options); + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +// Export for module usage +// if (require.main === module) { +// main(); +// } else { +// module.exports = { copyFiles }; +// } + +copyFiles('/Users/billyjin/Desktop/project/tgfx_new/pagx/wechat/ts/wasm', '/Users/billyjin/Desktop/project/tgfx_new/pagx/wechat/wx_demo/utils', + ['*.js', '*.br'], { + recursive: true, + overwrite: true, + preserveStructure: true +}); \ No newline at end of file diff --git a/pagx/wechat/script/plugin/replace-config.js b/pagx/wechat/script/plugin/replace-config.js new file mode 100644 index 0000000000..52bca9a712 --- /dev/null +++ b/pagx/wechat/script/plugin/replace-config.js @@ -0,0 +1,318 @@ +/* eslint-disable */ +export const replaceFunctionConfig = [ + { + name: 'replace __emval_get_method_caller', + start: 'function __emval_get_method_caller(argCount, argTypes)', + end: 'function __emval_get_module_property(name)', + type: 'function', + replaceStr: function __emval_get_method_caller(argCount, argTypes) { + var types; + try { + types = __emval_lookupTypes(argCount, argTypes); + } catch (e) { + types = emval_lookupTypes(argCount, argTypes); + } + var retType = types[0]; + var signatureName = + retType.name + + '_$' + + types + .slice(1) + .map(function (t) { + return t.name; + }) + .join('_') + + '$'; + var returnId = emval_registeredMethods[signatureName]; + if (returnId !== void 0) { + return returnId; + } + var params = ['retType']; + var args = [retType]; + var argsList = ''; + for (var i2 = 0; i2 < argCount - 1; ++i2) { + argsList += (i2 !== 0 ? ', ' : '') + 'arg' + i2; + params.push('argType' + i2); + args.push(types[1 + i2]); + } + var functionName = makeLegalFunctionName('methodCaller_' + signatureName); + var functionBody = 'return function ' + functionName + '(handle, name, destructors, args) {\n'; + var offset = 0; + for (var i2 = 0; i2 < argCount - 1; ++i2) { + functionBody += + ' var arg' + i2 + ' = argType' + i2 + '.readValueFromPointer(args' + (offset ? '+' + offset : '') + ');\n'; + offset += types[i2 + 1]['argPackAdvance']; + } + functionBody += ' var rv = handle[name](' + argsList + ');\n'; + for (var i2 = 0; i2 < argCount - 1; ++i2) { + if (types[i2 + 1]['deleteObject']) { + functionBody += ' argType' + i2 + '.deleteObject(arg' + i2 + ');\n'; + } + } + if (!retType.isVoid) { + functionBody += ' return retType.toWireType(destructors, rv);\n'; + } + functionBody += '};\n'; + params.push(functionBody); + var anonymous = function (retType) { + var parentargs = Array.from(arguments); + parentargs.shift(); + return (this[makeLegalFunctionName('methodCaller_' + signatureName)] = function ( + handle, + name, + destructors, + args, + ) { + var paramList = []; + var offset = 0; + for (var i = 0; i < parentargs.length; i++) { + paramList.push(parentargs[i].readValueFromPointer(args + offset)); + offset += types[i + 1]['argPackAdvance']; + } + var rv = handle[name](...paramList); + if (!retType.isVoid) { + return retType.toWireType(destructors, rv); + } + }); + }; + var invokerFunction = anonymous.apply({}, args); + try { + returnId = __emval_addMethodCaller(invokerFunction); + } catch (e) { + returnId = emval_addMethodCaller(invokerFunction); + } + emval_registeredMethods[signatureName] = returnId; + return returnId; + }.toString(), + }, + { + name: 'replace craftEmvalAllocator', + start: 'function craftEmvalAllocator(argCount)', + end: 'var emval_newers = {};', + type: 'function', + replaceStr: function craftEmvalAllocator(argCount) { + var argsList = ''; + for (var i2 = 0; i2 < argCount; ++i2) { + argsList += (i2 !== 0 ? ', ' : '') + 'arg' + i2; + } + var functionBody = 'return function emval_allocator_' + argCount + '(constructor, argTypes, args) {\n'; + for (var i2 = 0; i2 < argCount; ++i2) { + functionBody += + 'var argType' + + i2 + + " = requireRegisteredType(Module['HEAP32'][(argTypes >>> 2) + " + + i2 + + '], "parameter ' + + i2 + + '");\nvar arg' + + i2 + + ' = argType' + + i2 + + '.readValueFromPointer(args);\nargs += argType' + + i2 + + "['argPackAdvance'];\n"; + } + functionBody += 'var obj = new constructor(' + argsList + ');\nreturn valueToHandle(obj);\n}\n'; + function anonymous(requireRegisteredType, Module, valueToHandle) { + return function (constructor, argTypes, args) { + var resultList = []; + for (var i2 = 0; i2 < argCount; ++i2) { + var currentArg = requireRegisteredType(Module['HEAP32'][(argTypes >>> 2) + i2], `parameter ${i2}`); + var res = currentArg.readValueFromPointer(args); + resultList.push(res); + args += currentArg['argPackAdvance']; + } + var obj = new constructor(...resultList); + return valueToHandle(obj); + }; + } + var invokerFunction = anonymous.apply({}, [requireRegisteredType, Module, Emval.toHandle]); + return invokerFunction; + }.toString(), + }, + { + name: 'replace craftInvokerFunction', + start: 'function craftInvokerFunction(humanName, argTypes, classType, cppInvokerFunc, cppTargetFunc)', + end: 'function heap32VectorToArray(count, firstElement)', + type: 'funtcion', + replaceStr: function craftInvokerFunction(humanName, argTypes, classType, cppInvokerFunc, cppTargetFunc) { + var createOption = {}; + createOption.argCount = argTypes.length; + var argCount = argTypes.length; + if (argCount < 2) { + throwBindingError("argTypes array size mismatch! Must at least get return value and 'this' types!"); + } + createOption.isClassMethodFunc = argTypes[1] !== null && classType !== null; + var isClassMethodFunc = argTypes[1] !== null && classType !== null; + var needsDestructorStack = false; + for (var i2 = 1; i2 < argTypes.length; ++i2) { + if (argTypes[i2] !== null && argTypes[i2].destructorFunction === void 0) { + needsDestructorStack = true; + break; + } + } + createOption.needsDestructorStack = needsDestructorStack; + var returns = argTypes[0].name !== 'void'; + createOption.returns = argTypes[0].name !== 'void'; + createOption.childArgs = []; + createOption.childDtorFunc = []; + for (var i2 = 0; i2 < argCount - 2; ++i2) { + createOption.childArgs.push(i2); + } + var args1 = ['throwBindingError', 'invoker', 'fn', 'runDestructors', 'retType', 'classParam']; + var args2 = [throwBindingError, cppInvokerFunc, cppTargetFunc, runDestructors, argTypes[0], argTypes[1]]; + for (var i2 = 0; i2 < argCount - 2; ++i2) { + args1.push('argType' + i2); + args2.push(argTypes[i2 + 2]); + } + if (!needsDestructorStack) { + for (var i2 = isClassMethodFunc ? 1 : 2; i2 < argTypes.length; ++i2) { + var paramName = i2 === 1 ? 'thisWired' : 'arg' + (i2 - 2) + 'Wired'; + if (argTypes[i2].destructorFunction !== null) { + createOption.childDtorFunc.push({ + paramName, + func: argTypes[i2].destructorFunction, + index: i2, + }); + } + } + } + function anonymous(throwBindingError, invoker, fn, runDestructors, retType, classParam) { + const anonymousArg = Array.from(arguments); + + return (this[makeLegalFunctionName(humanName)] = function () { + var parentargs = Array.from(arguments); + var argumentLen = createOption.childArgs.length; + if (parentargs.length !== argCount - 2) { + throwBindingError( + 'function _PAGPlayer._getComposition called with ' + parentargs.length + ' arguments, expected 0 args!', + ); + } + const argArr = anonymousArg.slice(6, 6 + argumentLen); + var destructors = []; + var dtorStack = needsDestructorStack ? destructors : null; + var thisWired; + var argWiredList = []; + var rv; + if (isClassMethodFunc) { + thisWired = classParam.toWireType(dtorStack, this); + } + for (var i = 0; i < createOption.childArgs.length; i++) { + argWiredList.push(argArr[i].toWireType(dtorStack, parentargs[i])); + } + if (isClassMethodFunc) { + rv = invoker(fn, thisWired, ...argWiredList); + } else { + rv = invoker(fn, ...argWiredList); + } + function onDone(crv) { + if (needsDestructorStack) { + runDestructors(destructors); + } else { + const funcOption = createOption.childDtorFunc; + for (var i = 0; i < funcOption.length; i++) { + const currentOption = funcOption[i]; + if (currentOption.index === 1) { + currentOption.func(thisWired); + } else { + const ci = createOption.childArgs.indexOf(currentOption.index); + if (ci >= 0) { + currentOption.func(argWiredList[ci]); + } + } + } + } + if (returns) { + var ret = retType.fromWireType(crv); + return ret; + } + } + return onDone(rv); + }); + } + var invokerFunction = anonymous.apply({}, args2); + return invokerFunction; + }.toString(), + }, + { + name: 'replace createNamedFunction', + start: 'function createNamedFunction(name, body)', + end: 'function extendError(baseErrorType, errorName)', + replaceStr: function createNamedFunction(name, body) { + name = makeLegalFunctionName(name); + return function () { + return body.apply(this, arguments); + }; + }.toString(), + }, + { + name: 'replace getBinaryPromise', + start: 'function getBinaryPromise()', + end: 'function createWasm()', + replaceStr: function getBinaryPromise() { + return new Promise((resolve, reject) => { + if (globalThis.isWxWebAssembly) { + resolve(wasmBinaryFile); + } else { + const fs = wx.getFileSystemManager(); + fs.readFile({ + filePath: wasmBinaryFile, + position: 0, + success(res) { + resolve(res.data); + }, + fail(res) { + reject(res); + }, + }); + } + }); + }.toString(), + }, + { + name: 'replace WebAssembly Runtime error', + type: 'string', + start: 'var e = new WebAssembly.RuntimeError(what);', + replaceStr: 'var e = "run time error";', + }, + { + name: 'replace performance', + type: 'string', + start: 'performance.now();', + replaceStr: 'Date.now();', + }, + { + name: 'replace pagx-viewer.wasm name', + start: 'var wasmBinaryFile;', + end: 'function getBinary(file)', + replaceStr:` + var wasmBinaryFile; + wasmBinaryFile = "pagx-viewer.wasm.br"; + if (!isDataURI(wasmBinaryFile)) { + wasmBinaryFile = locateFile(wasmBinaryFile); + } + `, + }, + { + name: 'fix get gl get framebuffer', + type: 'string', + start: 'var result = GLctx.getParameter(name_);', + replaceStr: `var result = GLctx.getParameter(name_); + if(result === null || result === undefined) { + return 0; + } + `, + }, + { + name: 'fix gl initExtensions', + type: 'string', + start: `if (!ext.includes("lose_context") && !ext.includes("debug"))`, + replaceStr: `if (!ext.includes("lose_context") && !ext.includes("debug") && !ext.includes("WEBGL_webcodecs_video_frame"))`, + }, + { + name: 'replace _scriptDir', + type: 'string', + start: `var _scriptDir = import_meta.url;`, + replaceStr: `var _scriptDir = typeof document !== "undefined" && document.currentScript ? document.currentScript.src : void 0;`, + }, +]; diff --git a/pagx/wechat/script/plugin/rollup-plugin-replace.js b/pagx/wechat/script/plugin/rollup-plugin-replace.js new file mode 100644 index 0000000000..96864e917a --- /dev/null +++ b/pagx/wechat/script/plugin/rollup-plugin-replace.js @@ -0,0 +1,30 @@ +import MagicString from 'magic-string'; +import { replaceFunctionConfig } from './replace-config'; + +export default function replaceFunc() { + return { + name: 'replaceFunc', + transform(code, id) { + let codeStr = `${code}`; + const magic = new MagicString(codeStr); + replaceFunctionConfig.forEach((item) => { + if (item.type === 'string') { + const startOffset = codeStr.indexOf(item.start); + if (startOffset > -1) { + magic.overwrite(startOffset, startOffset + item.start.length, item.replaceStr); + } + } else { + const startOffset = codeStr.indexOf(item.start); + const endOffset = codeStr.indexOf(item.end); + if (startOffset > -1 && endOffset > startOffset && item.replaceStr) { + magic.overwrite(startOffset, endOffset, item.replaceStr); + } + } + }); + return { + code: magic.toString(), + map: magic.generateMap({ hires: true }), + }; + }, + }; +} diff --git a/pagx/wechat/script/rollup.wx.js b/pagx/wechat/script/rollup.wx.js new file mode 100644 index 0000000000..85e6a50566 --- /dev/null +++ b/pagx/wechat/script/rollup.wx.js @@ -0,0 +1,78 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import esbuild from 'rollup-plugin-esbuild'; +import resolve from '@rollup/plugin-node-resolve'; +import commonJs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import alias from '@rollup/plugin-alias'; +import path from "path"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from 'url'; +import replaceFunc from './plugin/rollup-plugin-replace'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const fileHeaderPath = path.resolve(__dirname, '../../../.idea/fileTemplates/includes/PAG File Header.h'); +const banner = readFileSync(fileHeaderPath, 'utf-8'); +const isRelease = process.env.BUILD_MODE === 'release'; + +const plugins = [ + esbuild({ tsconfig: path.resolve(__dirname, "../tsconfig.json"), minify: isRelease }), + json(), + alias({ + entries: [{ find: '@tgfx', replacement: path.resolve(__dirname, '../../../third_party/tgfx/web/src') }], + }), + resolve({ extensions: ['.mjs', '.js', '.ts', '.json'] }), + commonJs(), + replaceFunc(), + { + name: 'preserve-import-meta-url', + resolveImportMeta(property, options) { + // Preserve the original behavior of `import.meta.url`. + if (property === 'url') { + return 'import.meta.url'; + } + return null; + }, + }, +]; + +export default [ + { + input: path.resolve(__dirname, '../ts/pagx.ts'), + output: { + banner, + file: path.resolve(__dirname, '../ts/wasm/pagx-viewer.js'), + format: 'esm', + sourcemap: !isRelease + }, + plugins: plugins, + }, + { + input: path.resolve(__dirname, '../ts/gesture-manager.ts'), + output: { + banner, + file: path.resolve(__dirname, '../wx_demo/utils/gesture-manager.js'), + format: 'esm', + sourcemap: !isRelease + }, + plugins: plugins, + } +]; \ No newline at end of file diff --git a/pagx/wechat/script/setup.emsdk.wx.js b/pagx/wechat/script/setup.emsdk.wx.js new file mode 100644 index 0000000000..4dfb60e43b --- /dev/null +++ b/pagx/wechat/script/setup.emsdk.wx.js @@ -0,0 +1,42 @@ +/* +* WeChat Mini Program compilation depends on emsdk version 3.1.20. +* This script is used to download and activate the specified version of emsdk. +*/ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import Utils from "../../../third_party/vendor_tools/lib/Utils.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// const emsdkPath = path.resolve(__dirname, '../../third_party/emsdk'); +const emsdkPath = "/Users/billyjin/Desktop/project/tgfx/third_party/emsdk"; +if (!fs.existsSync(emsdkPath)) { + try { + Utils.exec(`git clone https://github.com/emscripten-core/emsdk.git ${emsdkPath}`); + } catch (error) { + console.error('clone emsdk failed:', error); + process.exit(1); + } +} else { + Utils.exec(`git -C ${emsdkPath} pull`); +} +const emscriptenPath = path.resolve(emsdkPath, 'upstream/emscripten'); +process.env.PATH = process.platform === 'win32' + ? `${emsdkPath};${emscriptenPath};${process.env.PATH}` + : `${emsdkPath}:${emscriptenPath}:${process.env.PATH}`; +Utils.exec("emsdk install 3.1.20", emsdkPath); +Utils.exec("emsdk activate 3.1.20", emsdkPath); +const emsdkEnv = process.platform === 'win32' ? "emsdk_env.bat" : "source emsdk_env.sh"; +let result = Utils.execSafe(emsdkEnv, emsdkPath); +let lines = result.split("\n"); +for (let line of lines) { + let values = line.split("="); + if (values.length > 1) { + process.stdout.write(line); + let key = values[0].trim(); + process.env[key] = values[1].trim(); + } +} diff --git a/pagx/wechat/server.js b/pagx/wechat/server.js new file mode 100644 index 0000000000..13bfd700af --- /dev/null +++ b/pagx/wechat/server.js @@ -0,0 +1,48 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import express from 'express'; +import path from 'path'; +import { exec } from 'child_process'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); + +// Enable SharedArrayBuffer +app.use((req, res, next) => { + res.set('Cross-Origin-Opener-Policy', 'same-origin'); + res.set('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); + +app.use('', express.static(__dirname)); + +app.get('/', (req, res) => { + res.redirect('/index.html'); +}); + +const port = 8082; +app.listen(port, () => { + const url = `http://localhost:${port}/`; + const start = (process.platform === 'darwin' ? 'open' : 'start'); + exec(start + ' ' + url); + console.log(`PAGX Viewer running at ${url}`); +}); diff --git a/pagx/wechat/src/GridBackground.cpp b/pagx/wechat/src/GridBackground.cpp new file mode 100644 index 0000000000..ece6481ede --- /dev/null +++ b/pagx/wechat/src/GridBackground.cpp @@ -0,0 +1,65 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "GridBackground.h" +#include "tgfx/layers/LayerRecorder.h" + +namespace pagx { + +std::shared_ptr GridBackgroundLayer::Make(int width, int height, + float density) { + return std::shared_ptr(new GridBackgroundLayer(width, height, density)); +} + +GridBackgroundLayer::GridBackgroundLayer(int width, int height, float density) + : width(width), height(height), density(density) { + invalidateContent(); +} + +void GridBackgroundLayer::onUpdateContent(tgfx::LayerRecorder* recorder) { + tgfx::LayerPaint backgroundPaint(tgfx::Color::White()); + recorder->addRect(tgfx::Rect::MakeWH(static_cast(width), static_cast(height)), + backgroundPaint); + + tgfx::LayerPaint tilePaint(tgfx::Color{0.8f, 0.8f, 0.8f, 1.f}); + // Use fixed logical size (32px) so the grid looks the same on all screens. + int logicalTileSize = 32; + int tileSize = static_cast(static_cast(logicalTileSize) * density); + if (tileSize <= 0) { + tileSize = logicalTileSize; + } + for (int y = 0; y < height; y += tileSize) { + bool draw = (y / tileSize) % 2 == 1; + for (int x = 0; x < width; x += tileSize) { + if (draw) { + recorder->addRect( + tgfx::Rect::MakeXYWH(static_cast(x), static_cast(y), + static_cast(tileSize), static_cast(tileSize)), + tilePaint); + } + draw = !draw; + } + } +} + +void DrawBackground(tgfx::Canvas* canvas, int width, int height, float density) { + auto layer = GridBackgroundLayer::Make(width, height, density); + layer->draw(canvas); +} + +} // namespace pagx diff --git a/pagx/wechat/src/GridBackground.h b/pagx/wechat/src/GridBackground.h new file mode 100644 index 0000000000..1942bbbda6 --- /dev/null +++ b/pagx/wechat/src/GridBackground.h @@ -0,0 +1,42 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "tgfx/layers/Layer.h" + +namespace pagx { + +class GridBackgroundLayer : public tgfx::Layer { + public: + static std::shared_ptr Make(int width, int height, float density); + + protected: + void onUpdateContent(tgfx::LayerRecorder* recorder) override; + + private: + GridBackgroundLayer(int width, int height, float density); + + int width = 0; + int height = 0; + float density = 1.f; +}; + +void DrawBackground(tgfx::Canvas* canvas, int width, int height, float density); + +} // namespace pagx diff --git a/pagx/wechat/src/PAGXView.cpp b/pagx/wechat/src/PAGXView.cpp new file mode 100644 index 0000000000..2e4e54a458 --- /dev/null +++ b/pagx/wechat/src/PAGXView.cpp @@ -0,0 +1,213 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PAGXView.h" +#include + +#include +#include +#include "GridBackground.h" +#include "tgfx/core/Data.h" +#include "tgfx/core/Typeface.h" + +using namespace emscripten; + +namespace pagx { + +std::shared_ptr PAGXView::MakeFrom(int width, int height) { + if (width <= 0 || height <= 0) { + return nullptr; + } + + auto device = tgfx::GLDevice::Current(); + if (device == nullptr) { + return nullptr; + } + + return std::make_shared(device, width, height); +} + +static std::vector> fallbackTypefaces; + +static std::shared_ptr GetDataFromEmscripten(const val& emscriptenData) { + if (emscriptenData.isUndefined()) { + return nullptr; + } + unsigned int length = emscriptenData["length"].as(); + if (length == 0) { + return nullptr; + } + auto buffer = new (std::nothrow) uint8_t[length]; + if (buffer) { + auto memory = val::module_property("HEAPU8")["buffer"]; + auto memoryView = emscriptenData["constructor"].new_( + memory, static_cast(reinterpret_cast(buffer)), length); + memoryView.call("set", emscriptenData); + return tgfx::Data::MakeAdopted(buffer, length, tgfx::Data::DeleteProc); + } + return nullptr; +} + +PAGXView::PAGXView(std::shared_ptr device, int width, int height) +: device(std::move(device)), _width(width), _height(height) { + displayList.setRenderMode(tgfx::RenderMode::Tiled); + displayList.setAllowZoomBlur(true); + displayList.setMaxTileCount(512); +} + + +void PAGXView::loadPAGX(const val& pagxData) { + auto data = GetDataFromEmscripten(pagxData); + if (!data) { + return; + } + LayerBuilder::Options options; + options.fallbackTypefaces = fallbackTypefaces; + auto content = pagx::LayerBuilder::FromData(data->bytes(), data->size(), options); + if (!content.root) { + return; + } + contentLayer = content.root; + pagxWidth = content.width; + pagxHeight = content.height; + displayList.root()->removeChildren(); + displayList.root()->addChild(contentLayer); + applyCenteringTransform(); +} + +void PAGXView::updateSize(int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + if (_width == width && _height == height) { + return; + } + _width = width; + _height = height; + surface = nullptr; + lastSurfaceWidth = 0; + lastSurfaceHeight = 0; +} + +void PAGXView::applyCenteringTransform() { + if (lastSurfaceWidth <= 0 || lastSurfaceHeight <= 0 || !contentLayer) { + return; + } + if (pagxWidth <= 0 || pagxHeight <= 0) { + return; + } + float scaleX = static_cast(lastSurfaceWidth) / pagxWidth; + float scaleY = static_cast(lastSurfaceHeight) / pagxHeight; + float scale = std::min(scaleX, scaleY); + float offsetX = (static_cast(lastSurfaceWidth) - pagxWidth * scale) * 0.5f; + float offsetY = (static_cast(lastSurfaceHeight) - pagxHeight * scale) * 0.5f; + auto matrix = tgfx::Matrix::MakeTrans(offsetX, offsetY); + matrix.preScale(scale, scale); + contentLayer->setMatrix(matrix); +} + +void PAGXView::updateZoomScaleAndOffset(float zoom, float offsetX, float offsetY) { + displayList.setZoomScale(zoom); + displayList.setContentOffset(offsetX, offsetY); +} + +// void PAGXView::draw() { +// if (device == nullptr) { +// return; +// } +// bool hasContentChanged = displayList.hasContentChanged(); +// bool hasLastRecording = (lastRecording != nullptr); +// if (!hasContentChanged && !hasLastRecording) { +// return; +// } +// auto context = device->lockContext(); +// if (context == nullptr) { +// return; +// } +// if (surface == nullptr) { +// surface = tgfx::Surface::Make(context, _width, _height); +// if (surface != nullptr) { +// lastSurfaceWidth = surface->width(); +// lastSurfaceHeight = surface->height(); +// applyCenteringTransform(); +// presentImmediately = true; +// } +// } +// if (surface == nullptr) { +// device->unlock(); +// return; +// } +// auto canvas = surface->getCanvas(); +// canvas->clear(); +// auto density = 1.0f; +// pagx::DrawBackground(canvas, surface->width(), surface->height(), density); +// displayList.render(surface.get(), false); +// auto recording = context->flush(); +// if (presentImmediately) { +// presentImmediately = false; +// if (recording) { +// context->submit(std::move(recording)); +// } +// } else { +// std::swap(lastRecording, recording); +// if (recording) { +// context->submit(std::move(recording)); +// } +// } +// device->unlock(); +// } + +bool PAGXView::draw() { + if (device == nullptr) { + return false; + } + + auto context = device->lockContext(); + if (context == nullptr) { + return false; + } + + if (surface == nullptr || surface->width() != _width || surface->height() != _height) { + tgfx::GLFrameBufferInfo glInfo = {}; + glInfo.id = 0; + glInfo.format = 0x8058; + tgfx::BackendRenderTarget renderTarget(glInfo, _width, _height); + surface = tgfx::Surface::MakeFrom(context, renderTarget, tgfx::ImageOrigin::BottomLeft); + if (surface == nullptr) { + device->unlock(); + return false; + } + } + + auto canvas = surface->getCanvas(); + canvas->clear(); + + DrawBackground(canvas, _width, _height, 1.0f); + + displayList.render(surface.get(), false); + + auto recording = context->flush(); + if (recording) { + context->submit(std::move(recording)); + } + device->unlock(); + + return true; +} + +} // namespace pagx diff --git a/pagx/wechat/src/PAGXView.h b/pagx/wechat/src/PAGXView.h new file mode 100644 index 0000000000..4ea47e840d --- /dev/null +++ b/pagx/wechat/src/PAGXView.h @@ -0,0 +1,80 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "tgfx/gpu/Recording.h" +#include "tgfx/layers/DisplayList.h" +#include "pagx/LayerBuilder.h" + +namespace pagx { + +class PAGXView { + public: + + /** +* Creates a PAGXView instance for WeChat Mini Program rendering. +* @param width The width of the canvas in pixels. +* @param height The height of the canvas in pixels. +* @return A shared pointer to the created PAGXView, or nullptr if creation fails. +* +* Note: Before calling this method, the JavaScript code must: +* 1. Get the Canvas object from WeChat API +* 2. Call canvas.getContext('webgl') to get WebGLRenderingContext +* 3. Register the context via GL.registerContext(gl) +*/ + static std::shared_ptr MakeFrom(int width, int height); + + + PAGXView(std::shared_ptr device, int width, int height); + + void loadPAGX(const emscripten::val& pagxData); + + void updateSize(int width, int height); + + void updateZoomScaleAndOffset(float zoom, float offsetX, float offsetY); + + bool draw(); + + float contentWidth() const { + return pagxWidth; + } + + float contentHeight() const { + return pagxHeight; + } + + private: + void applyCenteringTransform(); + + std::shared_ptr device = nullptr; + std::shared_ptr surface = nullptr; + tgfx::DisplayList displayList = {}; + std::shared_ptr contentLayer = nullptr; + std::unique_ptr lastRecording = nullptr; + int lastSurfaceWidth = 0; + int lastSurfaceHeight = 0; + bool presentImmediately = true; + float pagxWidth = 0.0f; + float pagxHeight = 0.0f; + int _width = 0; + int _height = 0; +}; + +} // namespace pagx diff --git a/pagx/wechat/src/binding.cpp b/pagx/wechat/src/binding.cpp new file mode 100644 index 0000000000..3c47595c7f --- /dev/null +++ b/pagx/wechat/src/binding.cpp @@ -0,0 +1,34 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include "PAGXView.h" + +using namespace emscripten; + +EMSCRIPTEN_BINDINGS(PAGXViewer) { + class_("PAGXView") + .smart_ptr>("PAGXView") + .class_function("MakeFrom", &pagx::PAGXView::MakeFrom) + .function("loadPAGX", &pagx::PAGXView::loadPAGX) + .function("updateSize", &pagx::PAGXView::updateSize) + .function("updateZoomScaleAndOffset", &pagx::PAGXView::updateZoomScaleAndOffset) + .function("draw", &pagx::PAGXView::draw) + .function("contentWidth", &pagx::PAGXView::contentWidth) + .function("contentHeight", &pagx::PAGXView::contentHeight); +} diff --git a/pagx/wechat/ts/babel.ts b/pagx/wechat/ts/babel.ts new file mode 100644 index 0000000000..4740b74681 --- /dev/null +++ b/pagx/wechat/ts/babel.ts @@ -0,0 +1,11 @@ +/* global globalThis */ +import type { wx } from './interfaces'; + +declare const WXWebAssembly: typeof WebAssembly; +declare const wx: wx; +declare const globalThis: any; + +globalThis.WebAssembly = WXWebAssembly; +globalThis.isWxWebAssembly = true; +// eslint-disable-next-line no-global-assign +window = globalThis; diff --git a/pagx/wechat/ts/backend-context.ts b/pagx/wechat/ts/backend-context.ts new file mode 100644 index 0000000000..aa00c30b8a --- /dev/null +++ b/pagx/wechat/ts/backend-context.ts @@ -0,0 +1,99 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import type { PAGX } from './types'; + +const WEBGL_CONTEXT_ATTRIBUTES = { + depth: false, + stencil: false, + antialias: false, +}; + +/** + * BackendContext wraps Emscripten GL context handle and manages context switching. + */ +export class BackendContext { + public handle: number; + private externallyOwned: boolean; + private oldHandle: number = 0; + + /** + * Create BackendContext from WebGL2RenderingContext. + * Registers the context to Emscripten if not already registered. + */ + public static from(module: PAGX, gl: WebGL2RenderingContext): BackendContext { + const { GL } = module; + let id = 0; + + // Check if context is already registered + if (GL.contexts.length > 0) { + id = GL.contexts.findIndex((context: any) => context?.GLctx === gl); + } + + if (id < 1) { + // Register new context to Emscripten (WebGL 2 only) + id = GL.registerContext(gl, { + majorVersion: 2, + minorVersion: 0, + ...WEBGL_CONTEXT_ATTRIBUTES, + }); + return new BackendContext(id, false); + } + return new BackendContext(id, true); + } + + private constructor(handle: number, externallyOwned: boolean = false) { + this.handle = handle; + this.externallyOwned = externallyOwned; + } + + /** + * Make this context current for the calling thread. + * Returns true if successful. + */ + public makeCurrent(module: PAGX): boolean { + this.oldHandle = module.GL.currentContext?.handle || 0; + if (this.oldHandle === this.handle) { + return true; + } + return module.GL.makeContextCurrent(this.handle); + } + + /** + * Restore the previous context. + */ + public clearCurrent(module: PAGX): void { + if (this.oldHandle === this.handle) { + return; + } + module.GL.makeContextCurrent(0); + if (this.oldHandle) { + module.GL.makeContextCurrent(this.oldHandle); + } + } + + /** + * Destroy the context if it's not externally owned. + */ + public destroy(module: PAGX): void { + if (this.externallyOwned) { + return; + } + module.GL.deleteContext(this.handle); + } +} diff --git a/pagx/wechat/ts/binding.ts b/pagx/wechat/ts/binding.ts new file mode 100644 index 0000000000..cc9f616e8d --- /dev/null +++ b/pagx/wechat/ts/binding.ts @@ -0,0 +1,36 @@ +import { PAGX } from './types'; +import { View } from './pagx-view'; +import { TGFXBind } from '@tgfx/wechat/binding'; +import type { EmscriptenGL, WindowColorSpace } from '@tgfx/types'; + +/** + * Set color space for WebGL context. + * This function is required by WebGLDevice but missing in TGFXBind for WeChat. + */ +const setColorSpace = (GL: EmscriptenGL, colorSpace: WindowColorSpace): boolean => { + // WindowColorSpace: None=0, SRGB=1, DisplayP3=2, Others=3 + if (colorSpace === 3) { // WindowColorSpace.Others + return false; + } + const gl = GL.currentContext?.GLctx as WebGLRenderingContext; + if ('drawingBufferColorSpace' in gl) { + if (colorSpace === 0 || colorSpace === 1) { // WindowColorSpace.None or SRGB + (gl as any).drawingBufferColorSpace = 'srgb'; + } else { // WindowColorSpace.DisplayP3 + (gl as any).drawingBufferColorSpace = 'display-p3'; + } + return true; + } else if (colorSpace === 2) { // WindowColorSpace.DisplayP3 + return false; + } + return true; +}; + +/** + * Binding pag js module on pag webassembly module. + */ +export const binding = (module: PAGX) => { + TGFXBind(module); + module.View = View; + module.tgfx.setColorSpace = setColorSpace; +}; diff --git a/pagx/wechat/ts/gesture-manager.ts b/pagx/wechat/ts/gesture-manager.ts new file mode 100644 index 0000000000..cce4d339de --- /dev/null +++ b/pagx/wechat/ts/gesture-manager.ts @@ -0,0 +1,370 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +const MIN_ZOOM = 0.1; +const MAX_ZOOM = 10.0; +const TAP_TIMEOUT = 300; +const TAP_DISTANCE_THRESHOLD = 50; + +// Offset limits to prevent content from moving too far outside viewport +// These are in content space units (larger values allow more pan freedom) +const MAX_OFFSET_MULTIPLIER = 5.0; // Allow panning up to 5x content size in each direction + +export interface GestureState { + action: 'none' | 'update' | 'reset'; + zoom: number; + offsetX: number; + offsetY: number; +} + +export class WXGestureManager { + // Current state + private zoom: number = 1.0; + private offsetX: number = 0; // offset passed to C++: offset = contentOffset / zoom + private offsetY: number = 0; + + // Content space offset (physical pixels / canvasToContentScale) + // This represents the offset in content coordinate space, maintaining visual position + private contentOffsetX: number = 0; + private contentOffsetY: number = 0; + + // Content dimensions for offset limiting + private contentWidth: number = 0; + private contentHeight: number = 0; + + // Canvas dimensions in physical pixels and scale factor + private canvasWidth: number = 0; // physical pixels (rect.width * dpr) + private canvasHeight: number = 0; // physical pixels (rect.height * dpr) + private canvasToContentScale: number = 1.0; // canvasPixels / contentPixels + + // Single-finger drag state + private lastTouchX: number = 0; + private lastTouchY: number = 0; + + // Pinch zoom state + private initialDistance: number = 0; + private initialZoom: number = 1.0; + private initialContentOffsetX: number = 0; + private initialContentOffsetY: number = 0; + private pinchCenterX: number = 0; // physical pixels + private pinchCenterY: number = 0; // physical pixels + + // Double-tap detection + private lastTapTime: number = 0; + private lastTapX: number = 0; + private lastTapY: number = 0; + + /** + * Initialize gesture manager with canvas and content dimensions. + * Must be called after loading PAGX file. + * + * NOTE: This method resets all transforms (zoom and offsets) to initial state. + * Returns the reset state that should be applied to the view. + * + * @param canvasWidth - Canvas width in physical pixels (rect.width * dpr) + * @param canvasHeight - Canvas height in physical pixels (rect.height * dpr) + * @param contentWidth - PAGX content width in content pixels + * @param contentHeight - PAGX content height in content pixels + */ + public init( + canvasWidth: number, + canvasHeight: number, + contentWidth: number, + contentHeight: number + ): GestureState | null { + if (canvasWidth <= 0 || canvasHeight <= 0 || + contentWidth <= 0 || contentHeight <= 0) { + console.error( + `Invalid dimensions: canvas=${canvasWidth}x${canvasHeight}, ` + + `content=${contentWidth}x${contentHeight}` + ); + return null; + } + + this.canvasWidth = canvasWidth; + this.canvasHeight = canvasHeight; + this.contentWidth = contentWidth; + this.contentHeight = contentHeight; + this.canvasToContentScale = Math.min( + canvasWidth / contentWidth, + canvasHeight / contentHeight + ); + + // Reset all transforms when (re)initializing + this.zoom = 1.0; + this.offsetX = 0; + this.offsetY = 0; + this.contentOffsetX = 0; + this.contentOffsetY = 0; + + return { + action: 'reset', + zoom: this.zoom, + offsetX: this.offsetX, + offsetY: this.offsetY + }; + } + + /** + * Update canvas size (e.g., after orientation change or window resize). + * + * NOTE: When canvas size changes, canvasToContentScale and centerOffset both change, + * making it impossible to maintain the exact visual position. The simplest + * and most predictable approach is to reset all transforms. + */ + public updateSize( + canvasWidth: number, + canvasHeight: number, + contentWidth: number, + contentHeight: number + ): void { + const oldScale = this.canvasToContentScale; + + this.canvasWidth = canvasWidth; + this.canvasHeight = canvasHeight; + this.contentWidth = contentWidth; + this.contentHeight = contentHeight; + this.canvasToContentScale = Math.min( + canvasWidth / contentWidth, + canvasHeight / contentHeight + ); + + if (oldScale > 0 && Math.abs(oldScale - this.canvasToContentScale) > 0.001) { + // Reset all transforms when scale changes + this.zoom = 1.0; + this.offsetX = 0; + this.offsetY = 0; + this.contentOffsetX = 0; + this.contentOffsetY = 0; + } + } + + /** + * Get current gesture state. + */ + public getState(): GestureState { + return { + action: 'none', + zoom: this.zoom, + offsetX: this.offsetX, + offsetY: this.offsetY + }; + } + + /** + * Reset to initial centered state. + */ + public reset(): GestureState { + this.zoom = 1.0; + this.offsetX = 0; + this.offsetY = 0; + this.contentOffsetX = 0; + this.contentOffsetY = 0; + return { + action: 'reset', + zoom: this.zoom, + offsetX: this.offsetX, + offsetY: this.offsetY + }; + } + + /** + * Clamp content offset to prevent extreme values that cause rendering issues. + * Limits offset to a reasonable range based on content size. + */ + private clampContentOffset(): void { + if (this.contentWidth <= 0 || this.contentHeight <= 0) { + return; + } + + // Allow panning up to MAX_OFFSET_MULTIPLIER times the content size + const maxOffsetX = this.contentWidth * MAX_OFFSET_MULTIPLIER; + const maxOffsetY = this.contentHeight * MAX_OFFSET_MULTIPLIER; + + this.contentOffsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, this.contentOffsetX)); + this.contentOffsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, this.contentOffsetY)); + } + + /** + * Handle touch start event. + * @param touches - Touch array from WeChat event (coordinates in logical pixels) + * @param dpr - Device pixel ratio for converting to physical pixels + */ + public onTouchStart(touches: any[], dpr: number): GestureState { + if (touches.length === 1) { + // Single finger: prepare for drag + this.lastTouchX = touches[0].x; + this.lastTouchY = touches[0].y; + + // Double-tap detection (in logical pixels) + const now = Date.now(); + const distance = Math.hypot( + touches[0].x - this.lastTapX, + touches[0].y - this.lastTapY + ); + + if (now - this.lastTapTime < TAP_TIMEOUT && + distance < TAP_DISTANCE_THRESHOLD) { + this.lastTapTime = 0; + return this.reset(); + } + + this.lastTapX = touches[0].x; + this.lastTapY = touches[0].y; + this.lastTapTime = now; + + } else if (touches.length === 2) { + // Two fingers: prepare for pinch zoom + const dx = touches[1].x - touches[0].x; + const dy = touches[1].y - touches[0].y; + this.initialDistance = Math.hypot(dx, dy); + this.initialZoom = this.zoom; + + // IMPORTANT: Save current content offsets before zoom starts + this.initialContentOffsetX = this.contentOffsetX; + this.initialContentOffsetY = this.contentOffsetY; + + // Record pinch center in physical pixels (touches are in logical pixels) + this.pinchCenterX = (touches[0].x + touches[1].x) * 0.5 * dpr; + this.pinchCenterY = (touches[0].y + touches[1].y) * 0.5 * dpr; + } + + return this.getState(); + } + + /** + * Handle touch move event. + * @param touches - Touch array from WeChat event (coordinates in logical pixels) + * @param dpr - Device pixel ratio for converting to physical pixels + */ + public onTouchMove(touches: any[], dpr: number): GestureState { + if (touches.length === 1) { + // Single-finger drag + // Guard against finger count transition: if we just went from 2+ fingers to 1, + // reset lastTouch to current position to avoid delta spike + if (this.initialDistance > 0) { + // We were in pinch mode, now transitioning to drag + this.lastTouchX = touches[0].x; + this.lastTouchY = touches[0].y; + this.initialDistance = 0; + return this.getState(); + } + + // Convert logical pixel delta to physical pixel delta + const deltaX = (touches[0].x - this.lastTouchX) * dpr; + const deltaY = (touches[0].y - this.lastTouchY) * dpr; + + // Accumulate content space offset + // deltaX is in physical pixels, canvasToContentScale is physicalPixels/contentPixels + // so deltaX / canvasToContentScale gives us content space units + if (this.canvasToContentScale > 0) { + this.contentOffsetX += deltaX / this.canvasToContentScale; + this.contentOffsetY += deltaY / this.canvasToContentScale; + } + + // Clamp offset to prevent extreme values + this.clampContentOffset(); + + // Convert to C++ offset space: offset = contentOffset / zoom + if (this.zoom > 0) { + this.offsetX = this.contentOffsetX / this.zoom; + this.offsetY = this.contentOffsetY / this.zoom; + } + + this.lastTouchX = touches[0].x; + this.lastTouchY = touches[0].y; + + return { + action: 'update', + zoom: this.zoom, + offsetX: this.offsetX, + offsetY: this.offsetY + }; + + } else if (touches.length === 2) { + // Pinch zoom + // Ensure initialized (guard against gesture transition) + if (this.initialDistance <= 0) { + return this.getState(); + } + + const dx = touches[1].x - touches[0].x; + const dy = touches[1].y - touches[0].y; + const currentDistance = Math.hypot(dx, dy); + + const scaleChange = currentDistance / this.initialDistance; + const newZoom = Math.max( + MIN_ZOOM, + Math.min(MAX_ZOOM, this.initialZoom * scaleChange) + ); + + // Focal point calculation in content space: + // Canvas position (physical pixels): C = centerOffset + canvasToContentScale * zoom * (offset + P) + // where P is point in content space + // To find P: P = (C - centerOffset) / (canvasToContentScale * zoom) - offset + // = (C - centerOffset) / canvasToContentScale / zoom - contentOffset / zoom + // = ((C - centerOffset) / canvasToContentScale - contentOffset) / zoom + // At zoom start: P = ((pinchCenter - center) / canvasToContentScale - initialContentOffset) / initialZoom + + // Convert focal point from physical pixels to content space + if (this.canvasToContentScale > 0 && this.initialZoom > 0) { + const focalContent = (this.pinchCenterX - this.canvasWidth * 0.5) / this.canvasToContentScale; + const focalContentY = (this.pinchCenterY - this.canvasHeight * 0.5) / this.canvasToContentScale; + + // Content point at focal: P = (focalContent - initialContentOffset) / initialZoom + // Keep P fixed: focalContent - initialContentOffset = P * initialZoom + // newContentOffset = focalContent - P * newZoom + // = focalContent - (focalContent - initialContentOffset) * (newZoom / initialZoom) + this.contentOffsetX = focalContent - (focalContent - this.initialContentOffsetX) * (newZoom / this.initialZoom); + this.contentOffsetY = focalContentY - (focalContentY - this.initialContentOffsetY) * (newZoom / this.initialZoom); + } + + // Clamp offset to prevent extreme values + this.clampContentOffset(); + + this.zoom = newZoom; + if (this.zoom > 0) { + this.offsetX = this.contentOffsetX / this.zoom; + this.offsetY = this.contentOffsetY / this.zoom; + } + + return { + action: 'update', + zoom: this.zoom, + offsetX: this.offsetX, + offsetY: this.offsetY + }; + } + + return this.getState(); + } + + /** + * Handle touch end event. + * @param remainingTouches - Array of touches that are still on screen (e.touches, not e.changedTouches) + */ + public onTouchEnd(remainingTouches: any[]): GestureState { + // Reset drag state if all fingers lifted + if (remainingTouches.length === 0) { + this.lastTouchX = 0; + this.lastTouchY = 0; + this.initialDistance = 0; + } + return this.getState(); + } +} diff --git a/pagx/wechat/ts/interfaces.ts b/pagx/wechat/ts/interfaces.ts new file mode 100644 index 0000000000..1d2bb99ef3 --- /dev/null +++ b/pagx/wechat/ts/interfaces.ts @@ -0,0 +1,81 @@ +export interface wx { + env: { + USER_DATA_PATH: string; + }; + getFileSystemManager: () => FileSystemManager; + getFileInfo: (object: { filePath: string; success?: () => void; fail?: () => void; complete?: () => void }) => void; + createVideoDecoder: () => VideoDecoder; + getSystemInfoSync: () => SystemInfo; + createOffscreenCanvas: ( + object?: { type: 'webgl' | '2d' }, + width?: number, + height?: number, + compInst?: any, + ) => OffscreenCanvas; + getPerformance: () => Performance; +} + +export interface FileSystemManager { + accessSync: (path: string) => void; + mkdirSync: (path: string) => void; + writeFileSync: (path: string, data: string | ArrayBuffer, encoding: string) => void; + unlinkSync: (path: string) => void; + readdirSync: (path: string) => string[]; +} + +export interface VideoDecoder { + getFrameData: () => FrameDataOptions; + seek: ( + /** 跳转的解码位置,单位 ms */ + position: number, + ) => Promise; + start: (option: VideoDecoderStartOption) => Promise; + remove: () => Promise; + off: ( + /** 事件名 */ + eventName: string, + /** 事件触发时执行的回调函数 */ + callback: (...args: any[]) => any, + ) => void; + on: ( + /** 事件名 + * + * 参数 eventName 可选值: + * - 'start': 开始事件。返回 \{ width, height \}; + * - 'stop': 结束事件。; + * - 'seek': seek 完成事件。; + * - 'bufferchange': 缓冲区变化事件。; + * - 'ended': 解码结束事件。; */ + eventName: 'start' | 'stop' | 'seek' | 'bufferchange' | 'ended', + /** 事件触发时执行的回调函数 */ + callback: (...args: any[]) => any, + ) => void; +} + +/** 视频帧数据,若取不到则返回 null。当缓冲区为空的时候可能暂停取不到数据。 */ +export interface FrameDataOptions { + /** 帧数据 */ + data: ArrayBuffer; + /** 帧数据高度 */ + height: number; + /** 帧原始 dts */ + pkDts: number; + /** 帧原始 pts */ + pkPts: number; + /** 帧数据宽度 */ + width: number; +} + +export interface VideoDecoderStartOption { + /** 需要解码的视频源文件。基础库 2.13.0 以下的版本只支持本地路径。 2.13.0 开始支持 http:// 和 https:// 协议的远程路径。 */ + source: string; + /** 解码模式。0:按 pts 解码;1:以最快速度解码 */ + mode?: number; +} + +export interface SystemInfo { + /** 客户端平台 */ + platform: 'ios' | 'android' | 'windows' | 'mac' | 'devtools'; + /** 设备像素比 */ + pixelRatio: number; +} diff --git a/pagx/wechat/ts/pagx-view.ts b/pagx/wechat/ts/pagx-view.ts new file mode 100644 index 0000000000..418e8b0ab4 --- /dev/null +++ b/pagx/wechat/ts/pagx-view.ts @@ -0,0 +1,234 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import { RenderCanvas, WxCanvas } from './render-canvas'; +import { BackendContext } from './backend-context'; +import type { PAGX, PAGXViewNative } from './types'; +import type { wx } from './interfaces'; + +declare const wx: wx; + +export interface PAGXViewOptions { + /** + * Use style to scale canvas. default false. + * When target canvas is offscreen canvas, useScale is false. + */ + useScale?: boolean; + /** + * Render first frame when view init. default true. + */ + firstFrame?: boolean; +} + +/** + * PAGXView for WeChat MiniProgram. + * Manages canvas, WebGL context, and C++ PAGXViewWechat instance. + */ +export class View { + /** + * Create PAGXView. + * @param module PAGX module instance + * @param canvas WeChat canvas object + * @param options PAGXView options + */ + public static async init( + module: PAGX, + canvas: WxCanvas, + options: PAGXViewOptions = {} + ): Promise { + const view = new View(module, canvas); + view.pagViewOptions = { ...view.pagViewOptions, ...options }; + + // Reset canvas size if needed + view.resetSize(view.pagViewOptions.useScale); + + // Create RenderCanvas + view.renderCanvas = RenderCanvas.from(module, canvas); + view.renderCanvas.retain(); + view.backendContext = view.renderCanvas.backendContext; + + if (!view.backendContext) { + throw new Error('Failed to create backend context'); + } + + // Make context current + if (!view.backendContext.makeCurrent(module)) { + throw new Error('Failed to make context current'); + } + + // Create C++ PAGXViewWechat instance + view.nativeView = module.PAGXView.MakeFrom(canvas.width, canvas.height); + + view.backendContext.clearCurrent(module); + + if (!view.nativeView) { + throw new Error('Failed to create PAGXViewWechat'); + } + + return view; + } + + private module: PAGX; + private nativeView: PAGXViewNative | null = null; + private renderCanvas: RenderCanvas | null = null; + private backendContext: BackendContext | null = null; + private canvas: WxCanvas | null = null; + private pagViewOptions: PAGXViewOptions = { + useScale: false, + firstFrame: true, + }; + private isDestroyed = false; + + private constructor(module: PAGX, canvas: WxCanvas) { + this.module = module; + this.canvas = canvas; + } + + /** + * Load PAGX file data. + * @param pagxData PAGX file data + */ + public loadPAGX(pagxData: Uint8Array): void { + this.checkDestroyed(); + if (!this.nativeView) { + throw new Error('Native view not initialized'); + } + this.nativeView.loadPAGX(pagxData); + } + + /** + * Update canvas size. + * @param width New width + * @param height New height + */ + public updateSize(width?: number, height?: number): void { + this.checkDestroyed(); + + if (!this.canvas) { + throw new Error('Canvas element is not found!'); + } + + if (width !== undefined && height !== undefined) { + this.canvas.width = width; + this.canvas.height = height; + } + + if (!this.backendContext) { + return; + } + + this.backendContext.makeCurrent(this.module); + this.nativeView!.updateSize(this.canvas.width, this.canvas.height); + this.backendContext.clearCurrent(this.module); + } + + /** + * Update zoom scale and content offset for display list. + * @param zoom Zoom scale + * @param offsetX X offset + * @param offsetY Y offset + */ + public updateZoomScaleAndOffset(zoom: number, offsetX: number, offsetY: number): void { + this.checkDestroyed(); + this.nativeView!.updateZoomScaleAndOffset(zoom, offsetX, offsetY); + } + + /** + * Draw current frame. + */ + public draw(): void { + this.checkDestroyed(); + + if (!this.backendContext) { + return; + } + + this.backendContext.makeCurrent(this.module); + this.nativeView!.draw(); + this.backendContext.clearCurrent(this.module); + } + + /** + * Get content width. + */ + public contentWidth(): number { + this.checkDestroyed(); + return this.nativeView!.contentWidth(); + } + + /** + * Get content height. + */ + public contentHeight(): number { + this.checkDestroyed(); + return this.nativeView!.contentHeight(); + } + + /** + * Destroy PAGXView and release all resources. + */ + public destroy(): void { + if (this.isDestroyed) { + return; + } + + if (this.nativeView) { + if (this.backendContext) { + this.backendContext.makeCurrent(this.module); + } + this.nativeView.delete(); + this.nativeView = null; + if (this.backendContext) { + this.backendContext.clearCurrent(this.module); + } + } + + if (this.renderCanvas) { + this.renderCanvas.release(); + this.renderCanvas = null; + } + + this.backendContext = null; + this.canvas = null; + this.isDestroyed = true; + } + + private resetSize(useScale = false): void { + if (!this.canvas) { + throw new Error('Canvas element is not found!'); + } + + if (!useScale) { + return; + } + + // Calculate display size for WeChat MiniProgram + const displayWidth = (this.canvas as any).displayWidth || this.canvas.width; + const displayHeight = (this.canvas as any).displayHeight || this.canvas.height; + const dpr = wx.getSystemInfoSync().pixelRatio; + + this.canvas.width = displayWidth * dpr; + this.canvas.height = displayHeight * dpr; + } + + private checkDestroyed(): void { + if (this.isDestroyed) { + throw new Error('PAGXView has been destroyed'); + } + } +} diff --git a/pagx/wechat/ts/pagx.ts b/pagx/wechat/ts/pagx.ts new file mode 100644 index 0000000000..3fabfc07cd --- /dev/null +++ b/pagx/wechat/ts/pagx.ts @@ -0,0 +1,28 @@ +import './babel'; +import * as types from './types'; +import createPAG from '../wasm/pagx-viewer'; +import { PAGX } from './types'; +import { binding } from './binding'; + +export interface moduleOption { + /** + * Link to wasm file. + */ + locateFile?: (file: string) => string; +} + +/** + * Initialize PAGX webassembly module. + */ +const PAGXInit = (moduleOption: moduleOption = {}): Promise => + createPAG(moduleOption) + .then((module: types.PAGX) => { + binding(module); + return module; + }) + .catch((error: any) => { + console.error(error); + throw new Error('PAGXInit fail! Please check .wasm file path valid.'); + }); + +export { PAGXInit, types }; \ No newline at end of file diff --git a/pagx/wechat/ts/render-canvas.ts b/pagx/wechat/ts/render-canvas.ts new file mode 100644 index 0000000000..67dac9ba8b --- /dev/null +++ b/pagx/wechat/ts/render-canvas.ts @@ -0,0 +1,115 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +import { BackendContext } from './backend-context'; +import type { PAGX } from './types'; + +export type WxCanvas = (HTMLCanvasElement | OffscreenCanvas) & { + requestAnimationFrame?: (callback: () => void) => number; + cancelAnimationFrame?: (requestID: number) => void; +}; + +const renderCanvasList: RenderCanvas[] = []; + +/** + * RenderCanvas manages Canvas and WebGL context lifecycle. + */ +export class RenderCanvas { + /** + * Get or create RenderCanvas from canvas object. + * Reuses existing RenderCanvas if the same canvas is provided. + */ + public static from(module: PAGX, canvas: WxCanvas): RenderCanvas { + let renderCanvas = renderCanvasList.find((rc) => rc.canvas === canvas); + if (renderCanvas) { + return renderCanvas; + } + renderCanvas = new RenderCanvas(module, canvas); + renderCanvasList.push(renderCanvas); + return renderCanvas; + } + + private _canvas: WxCanvas | null = null; + private _backendContext: BackendContext | null = null; + private retainCount = 0; + private module: PAGX; + + private constructor(module: PAGX, canvas: WxCanvas) { + this.module = module; + this._canvas = canvas; + + const contextAttributes: WebGLContextAttributes = { + alpha: true, + depth: false, + stencil: false, + antialias: false, + powerPreference: 'high-performance', + preserveDrawingBuffer: false, + failIfMajorPerformanceCaveat: false, + }; + + // Try standard WebGL 2 context first + let gl = canvas.getContext('webgl2', contextAttributes) as WebGL2RenderingContext | null; + + if (!gl) { + throw new Error( + 'Failed to get WebGL 2 context from canvas. ' + + 'Please ensure your browser/environment supports WebGL 2.', + ); + } + + // Register context to Emscripten + this._backendContext = BackendContext.from(module, gl); + } + + /** + * Increase retain count. + */ + public retain(): void { + this.retainCount += 1; + } + + /** + * Decrease retain count and release resources when count reaches zero. + */ + public release(): void { + this.retainCount -= 1; + if (this.retainCount === 0) { + // Remove from global list + const index = renderCanvasList.indexOf(this); + if (index >= 0) { + renderCanvasList.splice(index, 1); + } + + // Clean up + if (this._backendContext) { + this._backendContext.destroy(this.module); + this._backendContext = null; + } + this._canvas = null; + } + } + + public get canvas(): WxCanvas | null { + return this._canvas; + } + + public get backendContext(): BackendContext | null { + return this._backendContext; + } +} diff --git a/pagx/wechat/ts/types.ts b/pagx/wechat/ts/types.ts new file mode 100644 index 0000000000..3ece91b040 --- /dev/null +++ b/pagx/wechat/ts/types.ts @@ -0,0 +1,22 @@ +import type {TGFX} from '@tgfx/types'; + +export interface PAGXViewNative { + width: () => number; + height: () => number; + loadPAGX: (data: Uint8Array) => boolean; + updateSize: (width: number, height: number) => void; + updateZoomScaleAndOffset: (zoom: number, offsetX: number, offsetY: number) => void; + draw: () => boolean; + contentWidth: () => number; + contentHeight: () => number; + delete: () => void; +} + +export interface PAGX extends TGFX { + PAGXView: { + MakeFrom: (width: number, height: number) => PAGXViewNative | null; + }; + VectorString: any; + module: PAGX; +} + diff --git a/pagx/wechat/tsconfig.json b/pagx/wechat/tsconfig.json new file mode 100644 index 0000000000..ae1b1a5d10 --- /dev/null +++ b/pagx/wechat/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": [ + "ES5", + "ES6", + "DOM", + "DOM.Iterable" + ], + "target": "ES2020", + "module": "ES2020", + "allowJs": true, + "sourceMap": true, + "esModuleInterop": true, + "moduleResolution": "Node", + "experimentalDecorators": true, + "strict": true, + "outDir": "./wasm-mt", + "baseUrl": ".", + "resolveJsonModule": true, + "paths": { + "@tgfx/*": [ + "../../third_party/tgfx/web/src/*" + ], + } + }, + "include": ["*.ts", "ts/**/*.ts"] +} \ No newline at end of file diff --git a/pagx/wechat/tsconfig.type.json b/pagx/wechat/tsconfig.type.json new file mode 100644 index 0000000000..7f170191c5 --- /dev/null +++ b/pagx/wechat/tsconfig.type.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ES5", "ES6", "DOM", "DOM.Iterable"], + "target": "ES2020", + "module": "ES2020", + "allowJs": true, + "sourceMap": true, + "esModuleInterop": true, + "moduleResolution": "Node", + "experimentalDecorators": true, + "strict": true, + "outDir": "./wasm-mt", + "baseUrl": ".", + "resolveJsonModule": true, + "paths": { + "@tgfx/*": [ + "../../third_party/tgfx/web/src/*" + ], + } + }, + "include": ["*.ts", "ts/**/*.ts"] +} diff --git a/pagx/wechat/wx_demo/.gitignore b/pagx/wechat/wx_demo/.gitignore new file mode 100644 index 0000000000..49fcb9e154 --- /dev/null +++ b/pagx/wechat/wx_demo/.gitignore @@ -0,0 +1,3 @@ +utils/* + + diff --git a/pagx/wechat/wx_demo/BUILD.md b/pagx/wechat/wx_demo/BUILD.md new file mode 100644 index 0000000000..6551ddd9b7 --- /dev/null +++ b/pagx/wechat/wx_demo/BUILD.md @@ -0,0 +1,162 @@ +# 微信小程序构建指南 + +## 🚀 快速开始 + +### 一键构建 + +```bash +cd pagx +npm run build:wechat +``` + +这个命令会: +1. 构建 TGFX 库(`npm run build:tgfx`) +2. 编译单线程 WASM(`node script/cmake.wx.js`) +3. 打包到 `wechat/wasm/`(`node script/rollup.wx.js`) + +--- + +## ⚠️ 重要说明 + +### 为什么需要单独构建? + +微信小程序**不支持 SharedArrayBuffer**,必须使用**单线程 WASM**。 + +| 特性 | Web 版本 | 微信版本 | +|------|---------|---------| +| 多线程 | ✅ | ❌ | +| SharedArrayBuffer | ✅ | ❌ | +| 构建命令 | `npm run build` | `npm run build:wechat` | +| 输出目录 | `wasm-mt/` | `wechat/wasm/` | + +--- + +## 📦 构建流程 + +### Step 1: 配置环境 + +```bash +# 确保 Emscripten 已安装 +source $EMSDK/emsdk_env.sh + +# 验证版本 +emcc --version +``` + +### Step 2: 运行构建 + +```bash +cd pagx +npm run build:wechat +``` + +### Step 3: 验证输出 + +```bash +ls -lh wechat/wasm/ +``` + +应该看到: +- `pagx.wasm` (~1.8 MB) +- `pagx.js` (~150-200 KB) + +--- + +## 🎯 成功标志 + +``` +✅ Build artifacts: + - pagx.wasm: 1.85 MB + - pagx.js: 168 KB + - Location: build-wechat + +✅ WeChat Miniprogram package completed! +``` + +--- + +## 🔧 故障排查 + +### 问题 1: 编译错误 + +``` +WebTypeface.cpp:119:35: error +``` + +**原因:** TGFX 源码错误 +**解决:** 修复源码后重新构建 + +--- + +### 问题 2: 找不到 WASM + +``` +❌ pagx.wasm not found +``` + +**检查:** +```bash +# 查看构建目录 +ls build-wechat/ + +# 如果不存在,重新构建 +rm -rf build-wechat +npm run build:wechat +``` + +--- + +### 问题 3: 微信小程序运行失败 + +``` +SharedArrayBuffer is not defined +``` + +**原因:** 使用了多线程版本 +**解决:** 确保运行 `npm run build:wechat` + +--- + +## 🧪 测试验证 + +### 1. 导入微信开发者工具 + +- 项目目录:`pagx/wechat` +- AppID:`touristappid` + +### 2. 检查文件加载 + +控制台应显示: +``` +✓ WASM loaded successfully +✓ PAGXViewer initialized +``` + +### 3. 测试功能 + +- [ ] PAGX 文件加载成功 +- [ ] Canvas 显示内容(非黑屏) +- [ ] 拖动移动功能正常 +- [ ] 重置功能正常 +- [ ] 无控制台错误 + +--- + +## 📚 相关文档 + +- **完整设计方案:** `.codebuddy/designs/pagx_wechat_design.md` +- **构建原理:** `.codebuddy/designs/pagx_wechat_build.md` +- **快速启动:** `QUICKSTART.md` + +--- + +## 🎉 下一步 + +构建成功后: + +1. 打开微信开发者工具 +2. 导入项目:`pagx/wechat` +3. 编译运行 +4. 测试功能 + +Good luck! 🚀 diff --git a/pagx/wechat/wx_demo/QUICKSTART.md b/pagx/wechat/wx_demo/QUICKSTART.md new file mode 100644 index 0000000000..03e2fab9f5 --- /dev/null +++ b/pagx/wechat/wx_demo/QUICKSTART.md @@ -0,0 +1,142 @@ +# 快速上手指南 + +## 问题修复说明 + +如果你遇到了以下错误: +``` +Initialization failed: ReferenceError: WebAssembly is not defined +``` + +这是因为微信小程序使用 `WXWebAssembly` 而非标准 `WebAssembly`。 + +**解决方案**:已通过适配器自动处理,只需重新构建即可。 + +## 快速构建(推荐) + +在 `pagx` 目录下执行: + +```bash +npm run build:wechat:quick +``` + +这个命令会: +1. 复用现有的 WASM 文件 +2. 自动适配微信小程序环境 +3. 生成到 `wechat/wasm/` 目录 + +**执行时间**:约 5 秒 + +## 验证构建 + +```bash +npm run verify:wechat +``` + +如果看到 `✅ Verification PASSED`,说明构建成功。 + +## 在微信开发者工具中测试 + +### 1. 打开项目 + +1. 启动「微信开发者工具」 +2. 点击「导入项目」 +3. 项目目录:`pagx/wechat` +4. AppID:`touristappid`(测试用) + +### 2. 编译运行 + +点击工具栏的「编译」按钮。 + +### 3. 验证成功 + +如果看到以下日志(在「控制台」中),说明初始化成功: + +``` +Starting initialization... +Loading WASM module... +WASM module loaded +PAGXView created successfully +Loading sample from CDN: https://... +Downloaded file size: ... bytes +Sample loaded successfully +Initialization completed successfully! +Rendering started +``` + +## 常见问题 + +### Q: 看到 "WebAssembly is not defined" 错误 + +**A**: 重新运行构建命令: +```bash +cd pagx +npm run build:wechat:quick +``` + +### Q: 下载 PAGX 文件失败 + +**A**: 检查网络连接,或修改 `pages/viewer/viewer.js` 中的 `SAMPLE_URL`: +```javascript +const SAMPLE_URL = 'https://your-cdn.com/your-file.pagx'; +``` + +### Q: 黑屏或无内容显示 + +**A**: +1. 检查控制台是否有错误 +2. 确认基础库版本 ≥ 2.15.0(在「详情」-「本地设置」中查看) +3. 确认 PAGX 文件格式正确 + +### Q: 如何修改示例文件? + +**A**: 编辑 `pages/viewer/viewer.js`: +```javascript +// 修改第 7 行 +const SAMPLE_URL = 'https://your-new-url.pagx'; +``` + +然后重新「编译」。 + +## 技术说明 + +### 为什么需要适配? + +微信小程序的 WebAssembly API 叫 `WXWebAssembly`,而标准浏览器使用 `WebAssembly`。构建脚本会自动: + +1. 在 `pagx.js` 开头注入 Polyfill +2. 替换所有 API 调用为兼容版本 +3. 处理不支持的 API(如 `instantiateStreaming`) + +### 关键配置 + +- **WASM 版本**:单线程(`-a wasm`) +- **基础库**:最低 2.15.0 +- **域名配置**:如使用自定义 CDN,需在后台配置 + +## 下一步 + +### 自定义 CDN + +1. 上传 PAGX 文件到你的 CDN +2. 修改 `SAMPLE_URL` +3. 在小程序后台配置域名: + - 登录 [微信小程序后台](https://mp.weixin.qq.com/) + - 「开发」-「开发管理」-「开发设置」 + - 添加 `downloadFile` 合法域名 + +### 添加更多功能 + +参考 `README.md` 了解如何添加: +- 多个示例切换 +- 缩放手势 +- 播放控制 + +## 获取帮助 + +- 查看完整文档:`README.md` +- 技术细节:`../../../.codebuddy/designs/wechat_webassembly_fix.md` +- TGFX 项目:https://github.com/Tencent/tgfx + +--- + +**提示**:如果一切正常,你应该能看到一个可以拖动的 PAGX 动画预览界面! diff --git a/pagx/wechat/wx_demo/README.md b/pagx/wechat/wx_demo/README.md new file mode 100644 index 0000000000..cb7fd034b2 --- /dev/null +++ b/pagx/wechat/wx_demo/README.md @@ -0,0 +1,175 @@ +# PAGX 微信小程序预览工具 + +基于 TGFX 和 WebAssembly 的 PAGX 文件预览工具微信小程序版本。 + +## 快速开始 + +### 1. 构建项目 + +在 `pagx` 目录下执行: + +```bash +# 快速构建(使用现有 WASM 文件) +npm run build:wechat:quick + +# 或完整构建(重新编译 WASM) +npm run build:wechat +``` + +### 2. 打开微信开发者工具 + +1. 打开「微信开发者工具」 +2. 选择「导入项目」 +3. 项目目录选择:`pagx/wechat` +4. AppID 设置为 `touristappid`(测试)或你的 AppID + +### 3. 编译运行 + +点击「编译」按钮,查看预览效果。 + +## 项目结构 + +``` +wechat/ +├── app.json # 小程序配置 +├── app.js # 小程序入口 +├── app.wxss # 全局样式 +├── project.config.json # 工具配置 +├── pages/ +│ └── viewer/ # 预览页面 +│ ├── viewer.js # 页面逻辑 +│ ├── viewer.wxml # 页面结构 +│ ├── viewer.wxss # 页面样式 +│ └── viewer.json # 页面配置 +└── wasm/ # WASM 文件(构建生成) + ├── pagx.wasm # WASM 模块 + └── pagx.js # WASM 加载器(已适配微信环境) +``` + +## 功能说明 + +### 当前功能(MVP) + +- ✅ 从 CDN 加载 PAGX 文件 +- ✅ 在 WebGL Canvas 上渲染 +- ✅ 触摸拖动手势 +- ✅ 重置按钮 + +### 后续计划 + +- [ ] 多个示例文件切换 +- [ ] 缩放手势 +- [ ] 播放控制(暂停/播放/进度条) +- [ ] 截图分享 + +## 配置说明 + +### 修改示例文件 URL + +编辑 `pages/viewer/viewer.js`: + +```javascript +// 修改这个 URL 为你的 CDN 地址 +const SAMPLE_URL = 'https://your-cdn.com/samples/your-file.pagx'; +``` + +### 配置合法域名 + +如果使用自己的 CDN,需要在微信小程序后台配置: + +1. 登录 [微信小程序后台](https://mp.weixin.qq.com/) +2. 进入「开发」-「开发管理」-「开发设置」 +3. 在「服务器域名」中添加 `downloadFile` 合法域名 +4. 示例:`https://your-cdn.com` + +## 常见问题 + +### 1. 报错:WebAssembly is not defined + +**原因**:微信小程序使用 `WXWebAssembly` 而非标准 `WebAssembly`。 + +**解决**:确保已运行 `npm run build:wechat:quick` 或 `npm run build:wechat`,这些命令会自动适配微信环境。 + +### 2. 报错:Failed to load WASM module + +**可能原因**: +- WASM 文件不存在或路径错误 +- 使用了多线程版本的 WASM(小程序不支持) + +**解决**: +- 检查 `wasm/` 目录下是否有 `pagx.wasm` 和 `pagx.js` +- 确保使用单线程版本构建(`-a wasm`) + +### 3. 下载文件失败 + +**可能原因**: +- CDN URL 不可访问 +- 未配置合法域名 +- CDN 未启用 HTTPS + +**解决**: +- 检查 CDN URL 是否正确 +- 在小程序后台配置 `downloadFile` 合法域名 +- 确保 CDN 支持 HTTPS 和 CORS + +### 4. 渲染黑屏 + +**可能原因**: +- WebGL 初始化失败 +- PAGX 文件格式错误 +- Canvas 尺寸问题 + +**解决**: +- 检查基础库版本(需要 2.15.0+) +- 验证 PAGX 文件是否正常 +- 查看控制台错误日志 + +## 技术限制 + +### 微信小程序限制 + +1. **不支持多线程 WASM**:必须使用单线程版本(`-a wasm`) +2. **包体积限制**:单个分包不超过 2MB,总包不超过 20MB +3. **域名白名单**:需要在后台配置合法域名 +4. **基础库版本**:最低 2.15.0(支持 `WXWebAssembly`) + +### 性能限制 + +1. **内存限制**:iOS 约 300MB,Android 约 500MB +2. **渲染性能**:取决于设备性能 +3. **文件大小**:建议 PAGX 文件不超过 10MB + +## 开发调试 + +### 启用调试信息 + +在 `pages/viewer/viewer.js` 中,已包含 `console.log` 输出: + +```javascript +console.log('Starting initialization...'); +console.log('WASM module loaded'); +console.log('PAGXView created successfully'); +// ... +``` + +在微信开发者工具的「控制台」中查看日志。 + +### 性能监控 + +使用微信开发者工具的「性能」面板: +1. 点击「性能」标签 +2. 选择「渲染性能」 +3. 查看 FPS、内存使用情况 + +## 参考资料 + +- [微信小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/) +- [WXWebAssembly API](https://developers.weixin.qq.com/miniprogram/dev/framework/performance/wasm.html) +- [TGFX 项目](https://github.com/Tencent/tgfx) +- [PAG 官网](https://pag.io) + +## 许可证 + +BSD 3-Clause License + +Copyright (C) 2026 Tencent. All rights reserved. diff --git a/pagx/wechat/wx_demo/app.js b/pagx/wechat/wx_demo/app.js new file mode 100644 index 0000000000..b1ae94a88f --- /dev/null +++ b/pagx/wechat/wx_demo/app.js @@ -0,0 +1,9 @@ +/** + * Copyright (C) 2026 Tencent. All Rights Reserved. + */ + +App({ + onLaunch() { + console.log('PAGX Viewer MVP launched'); + } +}); diff --git a/pagx/wechat/wx_demo/app.json b/pagx/wechat/wx_demo/app.json new file mode 100644 index 0000000000..6e2e62839e --- /dev/null +++ b/pagx/wechat/wx_demo/app.json @@ -0,0 +1,12 @@ +{ + "pages": [ + "pages/viewer/viewer" + ], + "window": { + "navigationBarTitleText": "PAGX Viewer", + "navigationBarBackgroundColor": "#1a1a1a", + "navigationBarTextStyle": "white", + "backgroundColor": "#2a2a2a" + }, + "sitemapLocation": "sitemap.json" +} diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.js b/pagx/wechat/wx_demo/pages/viewer/viewer.js new file mode 100644 index 0000000000..dab4920c15 --- /dev/null +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.js @@ -0,0 +1,400 @@ +/** + * Copyright (C) 2026 Tencent. All Rights Reserved. + * PAGX Viewer MVP for WeChat Miniprogram + */ + +import { + PAGXInit, +} from '../../utils/pagx-viewer'; +import { WXGestureManager } from '../../utils/gesture-manager'; + +// PAGX sample files configuration +const SAMPLE_FILES = [ + { + name: 'ColorPicker', + url: 'https://pag.io/pagx/samples/ColorPicker.pagx' + }, + { + name: 'complex3', + url: 'https://pag.io/pagx/testFiles/complex3.pagx' + }, + { + name: 'complex6', + url: 'https://pag.io/pagx/testFiles/complex6.pagx' + }, + { + name: 'path', + url: 'https://pag.io/pagx/testFiles/path.pagx' + }, + { + name: 'refStyle', + url: 'https://pag.io/pagx/testFiles/refStyle.pagx' + } +]; + +Page({ + data: { + loading: true, + zoom: 1.0, + offsetX: 0, + offsetY: 0, + samples: SAMPLE_FILES, + sampleNames: SAMPLE_FILES.map(item => item.name), + currentIndex: 0, + loadingFile: false + }, + + // State + View: null, + module: null, + canvas: null, + animationFrameId: 0, + gestureManager: null, + dpr: 2, + + // UI update throttling + lastUIUpdateTime: 0, + pendingUIUpdate: null, + UI_UPDATE_INTERVAL: 100, // Update UI at most every 100ms + + async onLoad(options) { + try { + // Get device pixel ratio for converting logical pixels to physical pixels + this.dpr = wx.getSystemInfoSync().pixelRatio || 2; + + // Create gesture manager + this.gestureManager = new WXGestureManager(); + + // Support custom file index from query params + if (options && options.index) { + const index = parseInt(options.index); + if (index >= 0 && index < SAMPLE_FILES.length) { + this.setData({ currentIndex: index }); + } + } + + await this.initializeViewer(); + await this.loadCurrentFile(); + + // Initialize gesture manager with canvas (physical pixels) and content dimensions + const contentWidth = this.View.contentWidth(); + const contentHeight = this.View.contentHeight(); + const initState = this.gestureManager.init( + this.canvas.width, // physical pixels (rect.width * dpr) + this.canvas.height, // physical pixels (rect.height * dpr) + contentWidth, + contentHeight + ); + if (!initState) { + throw new Error('Failed to initialize gesture manager'); + } + + // Apply initial state to ensure C++ side is synchronized + this.applyGestureState(initState); + + this.startRendering(); + this.setData({ loading: false }); + } catch (error) { + console.error('Initialization failed:', error); + wx.showModal({ + title: 'Error', + content: 'Failed to initialize viewer: ' + error.message, + showCancel: false + }); + } + }, + + onUnload() { + this.stopRendering(); + + // Clear pending UI update + if (this.pendingUIUpdate) { + clearTimeout(this.pendingUIUpdate); + this.pendingUIUpdate = null; + } + + if (this.View) { + this.View.destroy(); + this.View = null; + } + }, + + async initializeViewer() { + // Load WASM module + this.module = await PAGXInit({ + locateFile: (file) => '/utils/' + file + }); + + // Get canvas instance and size + return new Promise((resolve, reject) => { + const query = wx.createSelectorQuery(); + query.select('#pagx-canvas') + .node() + .exec(async (res) => { + try { + if (!res || !res[0]) { + throw new Error('Failed to get canvas node'); + } + + this.canvas = res[0].node; + + // Get canvas display size + const query2 = wx.createSelectorQuery(); + query2.select('#pagx-canvas') + .boundingClientRect() + .exec(async (rectRes) => { + try { + if (!rectRes || !rectRes[0]) { + throw new Error('Failed to get canvas rect'); + } + + const rect = rectRes[0]; + + // Set canvas physical pixel size based on display size and device pixel ratio + // This ensures sharp rendering on high-DPI displays + const dpr = wx.getSystemInfoSync().pixelRatio || 2; + this.canvas.width = Math.floor(rect.width * dpr); + this.canvas.height = Math.floor(rect.height * dpr); + + // Create View + this.View = await this.module.View.init(this.module, this.canvas, { + useScale: false, + firstFrame: false + }); + + if (!this.View) { + throw new Error('Failed to create View'); + } + + resolve(); + } catch (error) { + reject(error); + } + }); + } catch (error) { + reject(error); + } + }); + }); + }, + + async loadCurrentFile() { + const { currentIndex } = this.data; + const sample = SAMPLE_FILES[currentIndex]; + + // Download from CDN + const data = await this.downloadFile(sample.url); + + // Load into View + this.View.loadPAGX(data); + + // Re-initialize gesture manager with new content dimensions + // NOTE: init() automatically resets all transforms and returns the reset state + const contentWidth = this.View.contentWidth(); + const contentHeight = this.View.contentHeight(); + const resetState = this.gestureManager.init( + this.canvas.width, // physical pixels + this.canvas.height, // physical pixels + contentWidth, + contentHeight + ); + + // Apply reset state to synchronize C++ side + if (resetState) { + this.applyGestureState(resetState); + } + }, + + async switchFile(index) { + if (index === this.data.currentIndex || !this.View) { + return; + } + + try { + // Update UI (non-blocking) and load file (parallel) + this.setData({ + loadingFile: true, + currentIndex: index + }); + + // Load new file immediately (don't wait for UI render) + await this.loadCurrentFile(); + + wx.showToast({ + title: 'Loaded', + icon: 'success', + duration: 1000 + }); + } catch (error) { + console.error('Failed to switch file:', error); + wx.showModal({ + title: 'Error', + content: 'Failed to load file: ' + error.message, + showCancel: false + }); + } finally { + this.setData({ loadingFile: false }); + } + }, + + downloadFile(url) { + return new Promise((resolve, reject) => { + wx.showLoading({ title: 'Downloading...' }); + + wx.request({ + url: url, + responseType: 'arraybuffer', + success: (res) => { + wx.hideLoading(); + if (res.statusCode === 200) { + const uint8Array = new Uint8Array(res.data); + resolve(uint8Array); + } else { + reject(new Error(`HTTP ${res.statusCode}`)); + } + }, + fail: (err) => { + wx.hideLoading(); + reject(new Error(`Download failed: ${err.errMsg}`)); + } + }); + }); + }, + + resetTransform() { + if (!this.gestureManager || !this.View) { + return; + } + + const state = this.gestureManager.reset(); + this.applyGestureState(state); + }, + + applyGestureState(state) { + if (!state || state.action === 'none') { + return; + } + + if (!this.View) { + return; + } + + // Always update C++ side immediately for smooth rendering + try { + this.View.updateZoomScaleAndOffset(state.zoom, state.offsetX, state.offsetY); + } catch (error) { + console.error('Failed to update C++ view:', error); + return; + } + + // Throttle UI updates to avoid setData flooding + const now = Date.now(); + + if (state.action === 'reset') { + // Reset should update UI immediately + this.updateUIState(state); + return; + } + + // For 'update' action, throttle UI updates + if (now - this.lastUIUpdateTime >= this.UI_UPDATE_INTERVAL) { + this.updateUIState(state); + this.lastUIUpdateTime = now; + + // Clear any pending update since we just updated + if (this.pendingUIUpdate) { + clearTimeout(this.pendingUIUpdate); + this.pendingUIUpdate = null; + } + } else { + // Schedule a delayed update if not already scheduled + if (!this.pendingUIUpdate) { + this.pendingUIUpdate = setTimeout(() => { + this.updateUIState(state); + this.lastUIUpdateTime = Date.now(); + this.pendingUIUpdate = null; + }, this.UI_UPDATE_INTERVAL - (now - this.lastUIUpdateTime)); + } + } + }, + + updateUIState(state) { + this.setData({ + zoom: state.zoom.toFixed(2), + offsetX: Math.round(state.offsetX), + offsetY: Math.round(state.offsetY) + }); + }, + + startRendering() { + const render = () => { + if (!this.View) return; + this.View.draw(); + + // Use Canvas.requestAnimationFrame in WeChat Miniprogram + if (this.canvas && this.canvas.requestAnimationFrame) { + this.animationFrameId = this.canvas.requestAnimationFrame(render); + } else { + // Fallback to setTimeout if canvas not ready + this.animationFrameId = setTimeout(render, 16); + } + }; + render(); + }, + + stopRendering() { + if (this.animationFrameId) { + if (this.canvas && this.canvas.cancelAnimationFrame) { + this.canvas.cancelAnimationFrame(this.animationFrameId); + } else { + clearTimeout(this.animationFrameId); + } + this.animationFrameId = 0; + } + }, + + // Touch Events + onTouchStart(e) { + if (!this.gestureManager) return; + // Pass dpr to convert touch coordinates (logical pixels) to physical pixels + const state = this.gestureManager.onTouchStart(e.touches, this.dpr); + this.applyGestureState(state); + }, + + onTouchMove(e) { + if (!this.gestureManager) return; + // Pass dpr to convert touch coordinates (logical pixels) to physical pixels + const state = this.gestureManager.onTouchMove(e.touches, this.dpr); + this.applyGestureState(state); + }, + + onTouchEnd(e) { + if (!this.gestureManager) return; + // IMPORTANT: Pass e.touches (remaining touches), not e.changedTouches (ended touches) + const state = this.gestureManager.onTouchEnd(e.touches); + this.applyGestureState(state); + }, + + // Reset Button + onReset() { + // Prevent multiple rapid clicks + if (this.data.loadingFile) { + return; + } + + this.resetTransform(); + wx.showToast({ + title: 'Reset', + icon: 'success', + duration: 1000 + }); + }, + + // Picker Selection + onPickerChange(e) { + const index = parseInt(e.detail.value); + if (index !== this.data.currentIndex) { + this.switchFile(index); + } + }, +}); diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.json b/pagx/wechat/wx_demo/pages/viewer/viewer.json new file mode 100644 index 0000000000..0a4de4731a --- /dev/null +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "PAGX Viewer", + "disableScroll": true, + "usingComponents": {} +} diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.wxml b/pagx/wechat/wx_demo/pages/viewer/viewer.wxml new file mode 100644 index 0000000000..9c1de3a07b --- /dev/null +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.wxml @@ -0,0 +1,51 @@ + + + + + PAGX File: + + + {{samples[currentIndex].name}} + + + + + + + + + + + + + + Loading... + + + + + Switching... + + + + + Zoom: {{zoom}}x | Offset: ({{offsetX}}, {{offsetY}}) + + + diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.wxss b/pagx/wechat/wx_demo/pages/viewer/viewer.wxss new file mode 100644 index 0000000000..035f6fb69c --- /dev/null +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.wxss @@ -0,0 +1,131 @@ +.container { + width: 100%; + height: 100vh; + background-color: #2a2a2a; + display: flex; + flex-direction: column; +} + +.file-selector { + background: rgba(0, 0, 0, 0.9); + padding: 25rpx 30rpx; + border-bottom: 1rpx solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: space-between; + gap: 20rpx; + flex-shrink: 0; + z-index: 100; +} + +.selector-left { + display: flex; + align-items: center; + flex: 1; + min-width: 0; +} + +.selector-label { + color: #999; + font-size: 28rpx; + margin-right: 20rpx; + flex-shrink: 0; +} + +.picker { + flex: 1; + max-width: 500rpx; +} + +.picker-display { + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(255, 255, 255, 0.1); + padding: 20rpx 30rpx; + border-radius: 8rpx; + border: 1rpx solid rgba(255, 255, 255, 0.2); + transition: all 0.3s; +} + +.picker-display:active { + background: rgba(255, 255, 255, 0.15); +} + +.picker-text { + color: #fff; + font-size: 28rpx; + font-weight: 500; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.picker-arrow { + color: #07c160; + font-size: 24rpx; + margin-left: 20rpx; + flex-shrink: 0; +} + +.reset-btn { + padding: 20rpx 35rpx; + font-size: 28rpx; + background: #07c160; + color: #fff; + border: none; + border-radius: 8rpx; + flex-shrink: 0; + line-height: 1; +} + +.reset-btn::after { + border: none; +} + +.canvas-container { + flex: 1; + position: relative; + overflow: hidden; +} + +.canvas { + width: 100%; + height: 100%; +} + +.loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + padding: 40rpx; + border-radius: 8rpx; + color: #fff; + font-size: 28rpx; +} + +.loading-file { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.7); + padding: 30rpx 50rpx; + border-radius: 8rpx; + color: #fff; + font-size: 24rpx; +} + +.info { + position: absolute; + top: 20rpx; + right: 20rpx; + background: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 10rpx 20rpx; + border-radius: 4rpx; + font-size: 24rpx; +} diff --git a/pagx/wechat/wx_demo/project.config.json b/pagx/wechat/wx_demo/project.config.json new file mode 100644 index 0000000000..9dc2955b7c --- /dev/null +++ b/pagx/wechat/wx_demo/project.config.json @@ -0,0 +1,56 @@ +{ + "description": "PAGX Viewer MVP", + "setting": { + "bundle": false, + "userConfirmedBundleSwitch": false, + "urlCheck": true, + "scopeDataCheck": false, + "coverView": true, + "es6": true, + "postcss": true, + "compileHotReLoad": false, + "lazyloadPlaceholderEnable": false, + "preloadBackgroundData": false, + "minified": false, + "autoAudits": false, + "newFeature": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "useIsolateContext": true, + "nodeModules": false, + "enhance": true, + "useMultiFrameRuntime": true, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "packNpmManually": false, + "enableEngineNative": false, + "packNpmRelationList": [], + "minifyWXSS": true, + "showES6CompileOption": false, + "minifyWXML": true, + "compileWorklet": false, + "localPlugins": false, + "disableUseStrict": false, + "useCompilerPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + } + }, + "compileType": "miniprogram", + "libVersion": "3.14.0", + "appid": "wx0b5489422d4aeaf4", + "projectname": "pagx-viewer-mvp", + "condition": {}, + "simulatorPluginLibVersion": {}, + "packOptions": { + "ignore": [], + "include": [] + }, + "editorSetting": {} +} \ No newline at end of file diff --git a/pagx/wechat/wx_demo/project.private.config.json b/pagx/wechat/wx_demo/project.private.config.json new file mode 100644 index 0000000000..b6ed544261 --- /dev/null +++ b/pagx/wechat/wx_demo/project.private.config.json @@ -0,0 +1,23 @@ +{ + "libVersion": "3.14.0", + "projectname": "wx_demo", + "condition": {}, + "setting": { + "urlCheck": true, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "showShadowRootInWxmlPanel": true, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "compileHotReLoad": true, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true, + "bigPackageSizeSupport": false, + "useIsolateContext": true + } +} \ No newline at end of file diff --git a/pagx/wechat/wx_demo/sitemap.json b/pagx/wechat/wx_demo/sitemap.json new file mode 100644 index 0000000000..55d1d29ed4 --- /dev/null +++ b/pagx/wechat/wx_demo/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ + "action": "allow", + "page": "*" + }] +} From e933584aa5afab0f9d64fed687b3c9121dc81e0e Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 23 Jan 2026 15:14:49 +0800 Subject: [PATCH 143/678] Update project coding rules and dependencies. --- .codebuddy/rules/Code.md | 1 + DEPS | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.codebuddy/rules/Code.md b/.codebuddy/rules/Code.md index 821759e4c4..9387f351be 100644 --- a/.codebuddy/rules/Code.md +++ b/.codebuddy/rules/Code.md @@ -16,6 +16,7 @@ alwaysApply: true - 变量声明时一律赋初始值(即使是 `={}`),智能指针初始值使用 nullptr - 避免 lambda 表达式,改用显式方法或函数 - 禁止使用 `dynamic_cast` 和 C++ 异常(`throw`/`try`/`catch`) +- 尽量避免声明 mutable 声明变量,有线程安全问题,应该把调用的方法去掉 const - CPP 文件里的函数实现顺序与头文件中定义的顺序尽可能一致 - `include/` 目录 API 需详细注释含参数描述,其他公开方法一段话描述主要功能,私有方法不加注释 - 函数内代码不加行注释,除非只看代码无法理解设计意图 diff --git a/DEPS b/DEPS index 136ce97170..53c0eecc38 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "c1463b7cc765348f098f9640d31a6bc9c1c37bce", + "commit": "0c6cf4c26fc522c1b756d22d7a694c9ae2e329d0", "dir": "third_party/tgfx" }, { From 0f82bc2e6774426ac22b55c8025b2e0365a0db51 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 23 Jan 2026 15:34:58 +0800 Subject: [PATCH 144/678] Fix SVG rendering to use same canvas size as PAGX by setting containerSize to match PAGX document dimensions. --- test/src/PAGXTest.cpp | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 8f8eab84fb..8c166d7034 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -39,6 +39,7 @@ #include "tgfx/core/Surface.h" #include "tgfx/core/Typeface.h" #include "tgfx/layers/DisplayList.h" +#include "tgfx/layers/Layer.h" #include "tgfx/svg/SVGDOM.h" #include "tgfx/svg/TextShaper.h" #include "utils/Baseline.h" @@ -146,21 +147,16 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { continue; } - // Get tgfx's container size (respects width/height unit conversion). - auto containerSize = svgDOM->getContainerSize(); - int svgWidth = static_cast(containerSize.width); - int svgHeight = static_cast(containerSize.height); - - // Only compare SVG rendering when sizes match (no unit conversion difference). - // When viewBox is present with non-pixel width/height (e.g., "1080pt"), tgfx will scale - // content based on the unit conversion, but PAGX uses viewBox coordinates directly. - if (svgWidth == static_cast(pagxWidth) && svgHeight == static_cast(pagxHeight)) { - auto svgSurface = Surface::Make(context, canvasWidth, canvasHeight); - auto svgCanvas = svgSurface->getCanvas(); - svgCanvas->scale(scale, scale); - svgDOM->render(svgCanvas); - EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); - } + // Set container size to match PAGX document size, ensuring SVG renders at the same coordinate + // space as PAGX regardless of original width/height units (e.g., pt vs px). + svgDOM->setContainerSize({pagxWidth, pagxHeight}); + + // Render SVG with the same canvas size and scale as PAGX. + auto svgSurface = Surface::Make(context, canvasWidth, canvasHeight); + auto svgCanvas = svgSurface->getCanvas(); + svgCanvas->scale(scale, scale); + svgDOM->render(svgCanvas); + EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); // Save PAGX file to output directory pagx::SVGImporter::Options parserOptions; @@ -172,10 +168,13 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { } // Render PAGX using DisplayList (required for mask to work). + // Create a container layer for scaling to preserve content.root's original matrix. auto pagxSurface = Surface::Make(context, canvasWidth, canvasHeight); DisplayList displayList; - content.root->setMatrix(tgfx::Matrix::MakeScale(scale, scale)); - displayList.root()->addChild(content.root); + auto container = tgfx::Layer::Make(); + container->setMatrix(tgfx::Matrix::MakeScale(scale, scale)); + container->addChild(content.root); + displayList.root()->addChild(container); displayList.render(pagxSurface.get(), false); EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName + "_pagx")); } From a5b94aab884baf35b9fe81eb2b70e9bf5f7dbb19 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 23 Jan 2026 15:37:30 +0800 Subject: [PATCH 145/678] Fix SVG scaling to use container size instead of PAGX document size for correct aspect ratio. --- test/src/PAGXTest.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 8c166d7034..32a3f9a9eb 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -147,14 +147,20 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { continue; } - // Set container size to match PAGX document size, ensuring SVG renders at the same coordinate - // space as PAGX regardless of original width/height units (e.g., pt vs px). - svgDOM->setContainerSize({pagxWidth, pagxHeight}); + // Get SVG container size (may differ from PAGX size due to unit conversion, e.g., pt -> px). + auto containerSize = svgDOM->getContainerSize(); + float svgWidth = containerSize.width; + float svgHeight = containerSize.height; - // Render SVG with the same canvas size and scale as PAGX. + // Calculate scale to fit SVG into the same canvas as PAGX. + // SVG needs to scale from its container size to the canvas size. + float svgScaleX = static_cast(canvasWidth) / svgWidth; + float svgScaleY = static_cast(canvasHeight) / svgHeight; + + // Render SVG with calculated scale to match PAGX canvas. auto svgSurface = Surface::Make(context, canvasWidth, canvasHeight); auto svgCanvas = svgSurface->getCanvas(); - svgCanvas->scale(scale, scale); + svgCanvas->scale(svgScaleX, svgScaleY); svgDOM->render(svgCanvas); EXPECT_TRUE(Baseline::Compare(svgSurface, "PAGXTest/" + baseName + "_svg")); From 0f41529e97ad3acbeed38e023edf5e912f5046b9 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Fri, 23 Jan 2026 15:50:32 +0800 Subject: [PATCH 146/678] src --> src_bak --- pagx/wechat/CMakeLists.txt | 2 +- pagx/wechat/script/copy-files.js | 2 +- pagx/wechat/src/CMakeLists.txt | 65 +++++++++++++ pagx/wechat/src/GridBackground.cpp | 8 +- pagx/wechat/src/GridBackground.h | 8 +- pagx/wechat/src/PAGXView.cpp | 103 ++++++++------------- pagx/wechat/src/PAGXView.h | 45 +++++---- pagx/wechat/src/binding.cpp | 27 +++--- pagx/wechat/ts/binding.ts | 10 ++ pagx/wechat/wx_demo/pages/viewer/viewer.js | 26 +++--- 10 files changed, 180 insertions(+), 116 deletions(-) create mode 100644 pagx/wechat/src/CMakeLists.txt diff --git a/pagx/wechat/CMakeLists.txt b/pagx/wechat/CMakeLists.txt index 103cc2f4a9..2669c5d779 100644 --- a/pagx/wechat/CMakeLists.txt +++ b/pagx/wechat/CMakeLists.txt @@ -41,7 +41,7 @@ if (DEFINED EMSCRIPTEN) list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0) list(APPEND PAGX_VIEWER_LINK_OPTIONS --no-entry -lembind -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 -sEXPORT_NAME='PAGXWasm' -sWASM=1 -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['GL','HEAPU8'] -sMODULARIZE=1 - -sENVIRONMENT=web,worker -sEXPORT_ES6=1) + -sENVIRONMENT=web,worker) set(unsupported_emsdk_versions "4.0.11") foreach (unsupported_version IN LISTS unsupported_emsdk_versions) if (${EMSCRIPTEN_VERSION} VERSION_EQUAL ${unsupported_version}) diff --git a/pagx/wechat/script/copy-files.js b/pagx/wechat/script/copy-files.js index ee6a7ac09b..8a0c591397 100644 --- a/pagx/wechat/script/copy-files.js +++ b/pagx/wechat/script/copy-files.js @@ -184,7 +184,7 @@ function main() { // module.exports = { copyFiles }; // } -copyFiles('/Users/billyjin/Desktop/project/tgfx_new/pagx/wechat/ts/wasm', '/Users/billyjin/Desktop/project/tgfx_new/pagx/wechat/wx_demo/utils', +copyFiles('./ts/wasm', './wx_demo/utils', ['*.js', '*.br'], { recursive: true, overwrite: true, diff --git a/pagx/wechat/src/CMakeLists.txt b/pagx/wechat/src/CMakeLists.txt new file mode 100644 index 0000000000..e92be41e28 --- /dev/null +++ b/pagx/wechat/src/CMakeLists.txt @@ -0,0 +1,65 @@ +cmake_minimum_required(VERSION 3.13) +project(PAGXViewer) +# Disable error for the web platform. +SET_PROPERTY(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS true) + +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 17) + +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") +endif () + +# Add tgfx and pagx as dependencies +if (NOT TARGET tgfx) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + set(TGFX_BUILD_SVG ON CACHE BOOL "" FORCE) + set(TGFX_BUILD_LAYERS ON CACHE BOOL "" FORCE) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../.. ${CMAKE_CURRENT_BINARY_DIR}/tgfx) +endif () + +if (NOT TARGET pagx) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../.. ${CMAKE_CURRENT_BINARY_DIR}/pagx) +endif () + +# For Emscripten builds, pagx needs the same compile options as tgfx +if (DEFINED EMSCRIPTEN) + target_compile_options(pagx PRIVATE -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0) +endif () + +file(GLOB_RECURSE PAGX_VIEWER_FILES ./*.cpp) + +if (DEFINED EMSCRIPTEN) + add_executable(pagx-viewer ${PAGX_VIEWER_FILES}) + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0) + list(APPEND PAGX_VIEWER_LINK_OPTIONS --no-entry -lembind -fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 -sEXPORT_NAME='PAGXWasm' -sWASM=1 + -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['GL','HEAPU8'] -sMODULARIZE=1 + -sENVIRONMENT=web,worker) + set(unsupported_emsdk_versions "4.0.11") + foreach (unsupported_version IN LISTS unsupported_emsdk_versions) + if (${EMSCRIPTEN_VERSION} VERSION_EQUAL ${unsupported_version}) + message(FATAL_ERROR "Emscripten version ${EMSCRIPTEN_VERSION} is not supported.") + endif () + endforeach () + if (EMSCRIPTEN_PTHREADS) + list(APPEND PAGX_VIEWER_LINK_OPTIONS -sUSE_PTHREADS=1 -sINITIAL_MEMORY=32MB -sALLOW_MEMORY_GROWTH=1 + -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency + -sEXIT_RUNTIME=0 -sINVOKE_RUN=0 -sMALLOC=mimalloc) + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -fPIC -pthread) + else () + list(APPEND PAGX_VIEWER_LINK_OPTIONS -sALLOW_MEMORY_GROWTH=1) + endif () + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -O0 -g3) + list(APPEND PAGX_VIEWER_LINK_OPTIONS -O0 -g3 -sSAFE_HEAP=1 -Wno-limited-postlink-optimizations) + else () + list(APPEND PAGX_VIEWER_COMPILE_OPTIONS -Oz) + list(APPEND PAGX_VIEWER_LINK_OPTIONS -Oz) + endif () +else () + add_library(pagx-viewer SHARED ${PAGX_VIEWER_FILES}) +endif () + +target_compile_options(pagx-viewer PUBLIC ${PAGX_VIEWER_COMPILE_OPTIONS}) +target_link_options(pagx-viewer PUBLIC ${PAGX_VIEWER_LINK_OPTIONS}) +target_link_libraries(pagx-viewer pagx) diff --git a/pagx/wechat/src/GridBackground.cpp b/pagx/wechat/src/GridBackground.cpp index ece6481ede..409cc57dfc 100644 --- a/pagx/wechat/src/GridBackground.cpp +++ b/pagx/wechat/src/GridBackground.cpp @@ -1,13 +1,13 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// // -// Tencent is pleased to support the open source community by making libpag available. +// Tencent is pleased to support the open source community by making tgfx available. // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://opensource.org/licenses/BSD-3-Clause // // unless required by applicable law or agreed to in writing, software distributed under the // license is distributed on an "as is" basis, without warranties or conditions of any kind, diff --git a/pagx/wechat/src/GridBackground.h b/pagx/wechat/src/GridBackground.h index 1942bbbda6..bee83a95a9 100644 --- a/pagx/wechat/src/GridBackground.h +++ b/pagx/wechat/src/GridBackground.h @@ -1,13 +1,13 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// // -// Tencent is pleased to support the open source community by making libpag available. +// Tencent is pleased to support the open source community by making tgfx available. // // Copyright (C) 2026 Tencent. All rights reserved. // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://opensource.org/licenses/BSD-3-Clause // // unless required by applicable law or agreed to in writing, software distributed under the // license is distributed on an "as is" basis, without warranties or conditions of any kind, diff --git a/pagx/wechat/src/PAGXView.cpp b/pagx/wechat/src/PAGXView.cpp index 2e4e54a458..c478cff3a0 100644 --- a/pagx/wechat/src/PAGXView.cpp +++ b/pagx/wechat/src/PAGXView.cpp @@ -1,6 +1,6 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// // -// Tencent is pleased to support the open source community by making libpag available. +// Tencent is pleased to support the open source community by making tgfx available. // // Copyright (C) 2026 Tencent. All rights reserved. // @@ -17,12 +17,11 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "PAGXView.h" +#include #include - -#include -#include #include "GridBackground.h" #include "tgfx/core/Data.h" +#include "tgfx/core/Stream.h" #include "tgfx/core/Typeface.h" using namespace emscripten; @@ -39,11 +38,9 @@ std::shared_ptr PAGXView::MakeFrom(int width, int height) { return nullptr; } - return std::make_shared(device, width, height); + return std::shared_ptr(new PAGXView(device, width, height)); } -static std::vector> fallbackTypefaces; - static std::shared_ptr GetDataFromEmscripten(const val& emscriptenData) { if (emscriptenData.isUndefined()) { return nullptr; @@ -64,12 +61,13 @@ static std::shared_ptr GetDataFromEmscripten(const val& emscriptenDa } PAGXView::PAGXView(std::shared_ptr device, int width, int height) -: device(std::move(device)), _width(width), _height(height) { +: device(device), _width(width), _height(height) { displayList.setRenderMode(tgfx::RenderMode::Tiled); displayList.setAllowZoomBlur(true); displayList.setMaxTileCount(512); } +static std::vector> fallbackTypefaces; void PAGXView::loadPAGX(const val& pagxData) { auto data = GetDataFromEmscripten(pagxData); @@ -94,83 +92,48 @@ void PAGXView::updateSize(int width, int height) { if (width <= 0 || height <= 0) { return; } - if (_width == width && _height == height) { - return; - } + _width = width; _height = height; surface = nullptr; - lastSurfaceWidth = 0; - lastSurfaceHeight = 0; + + if (contentLayer) { + applyCenteringTransform(); + } } void PAGXView::applyCenteringTransform() { - if (lastSurfaceWidth <= 0 || lastSurfaceHeight <= 0 || !contentLayer) { + if (_width <= 0 || _height <= 0 || !contentLayer) { return; } if (pagxWidth <= 0 || pagxHeight <= 0) { return; } - float scaleX = static_cast(lastSurfaceWidth) / pagxWidth; - float scaleY = static_cast(lastSurfaceHeight) / pagxHeight; + + float scaleX = static_cast(_width) / pagxWidth; + float scaleY = static_cast(_height) / pagxHeight; float scale = std::min(scaleX, scaleY); - float offsetX = (static_cast(lastSurfaceWidth) - pagxWidth * scale) * 0.5f; - float offsetY = (static_cast(lastSurfaceHeight) - pagxHeight * scale) * 0.5f; + float offsetX = (static_cast(_width) - pagxWidth * scale) * 0.5f; + float offsetY = (static_cast(_height) - pagxHeight * scale) * 0.5f; + auto matrix = tgfx::Matrix::MakeTrans(offsetX, offsetY); matrix.preScale(scale, scale); + + matrix.preScale(zoomScale, zoomScale); + matrix.preTranslate(zoomOffsetX, zoomOffsetY); + contentLayer->setMatrix(matrix); } void PAGXView::updateZoomScaleAndOffset(float zoom, float offsetX, float offsetY) { - displayList.setZoomScale(zoom); - displayList.setContentOffset(offsetX, offsetY); -} + zoomScale = zoom; + zoomOffsetX = offsetX; + zoomOffsetY = offsetY; -// void PAGXView::draw() { -// if (device == nullptr) { -// return; -// } -// bool hasContentChanged = displayList.hasContentChanged(); -// bool hasLastRecording = (lastRecording != nullptr); -// if (!hasContentChanged && !hasLastRecording) { -// return; -// } -// auto context = device->lockContext(); -// if (context == nullptr) { -// return; -// } -// if (surface == nullptr) { -// surface = tgfx::Surface::Make(context, _width, _height); -// if (surface != nullptr) { -// lastSurfaceWidth = surface->width(); -// lastSurfaceHeight = surface->height(); -// applyCenteringTransform(); -// presentImmediately = true; -// } -// } -// if (surface == nullptr) { -// device->unlock(); -// return; -// } -// auto canvas = surface->getCanvas(); -// canvas->clear(); -// auto density = 1.0f; -// pagx::DrawBackground(canvas, surface->width(), surface->height(), density); -// displayList.render(surface.get(), false); -// auto recording = context->flush(); -// if (presentImmediately) { -// presentImmediately = false; -// if (recording) { -// context->submit(std::move(recording)); -// } -// } else { -// std::swap(lastRecording, recording); -// if (recording) { -// context->submit(std::move(recording)); -// } -// } -// device->unlock(); -// } + if (contentLayer) { + applyCenteringTransform(); + } +} bool PAGXView::draw() { if (device == nullptr) { @@ -210,4 +173,12 @@ bool PAGXView::draw() { return true; } +int PAGXView::width() const { + return _width; +} + +int PAGXView::height() const { + return _height; +} + } // namespace pagx diff --git a/pagx/wechat/src/PAGXView.h b/pagx/wechat/src/PAGXView.h index 4ea47e840d..8a1f0ee51f 100644 --- a/pagx/wechat/src/PAGXView.h +++ b/pagx/wechat/src/PAGXView.h @@ -1,6 +1,6 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// // -// Tencent is pleased to support the open source community by making libpag available. +// Tencent is pleased to support the open source community by making tgfx available. // // Copyright (C) 2026 Tencent. All rights reserved. // @@ -27,18 +27,17 @@ namespace pagx { class PAGXView { public: - /** -* Creates a PAGXView instance for WeChat Mini Program rendering. -* @param width The width of the canvas in pixels. -* @param height The height of the canvas in pixels. -* @return A shared pointer to the created PAGXView, or nullptr if creation fails. -* -* Note: Before calling this method, the JavaScript code must: -* 1. Get the Canvas object from WeChat API -* 2. Call canvas.getContext('webgl') to get WebGLRenderingContext -* 3. Register the context via GL.registerContext(gl) -*/ + * Creates a PAGXView instance for WeChat Mini Program rendering. + * @param width The width of the canvas in pixels. + * @param height The height of the canvas in pixels. + * @return A shared pointer to the created PAGXView, or nullptr if creation fails. + * + * Note: Before calling this method, the JavaScript code must: + * 1. Get the Canvas object from WeChat API + * 2. Call canvas.getContext('webgl') to get WebGLRenderingContext + * 3. Register the context via GL.registerContext(gl) + */ static std::shared_ptr MakeFrom(int width, int height); @@ -46,6 +45,11 @@ class PAGXView { void loadPAGX(const emscripten::val& pagxData); + /** + * Updates the canvas size and recreates the surface. + * @param width New width in pixels. + * @param height New height in pixels. + */ void updateSize(int width, int height); void updateZoomScaleAndOffset(float zoom, float offsetX, float offsetY); @@ -60,6 +64,16 @@ class PAGXView { return pagxHeight; } + /** + * Returns the width of the canvas in pixels. + */ + int width() const; + + /** + * Returns the height of the canvas in pixels. + */ + int height() const; + private: void applyCenteringTransform(); @@ -67,14 +81,13 @@ class PAGXView { std::shared_ptr surface = nullptr; tgfx::DisplayList displayList = {}; std::shared_ptr contentLayer = nullptr; - std::unique_ptr lastRecording = nullptr; - int lastSurfaceWidth = 0; - int lastSurfaceHeight = 0; - bool presentImmediately = true; float pagxWidth = 0.0f; float pagxHeight = 0.0f; int _width = 0; int _height = 0; + float zoomScale = 1.0f; + float zoomOffsetX = 0.0f; + float zoomOffsetY = 0.0f; }; } // namespace pagx diff --git a/pagx/wechat/src/binding.cpp b/pagx/wechat/src/binding.cpp index 3c47595c7f..b33be29951 100644 --- a/pagx/wechat/src/binding.cpp +++ b/pagx/wechat/src/binding.cpp @@ -1,6 +1,6 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// // -// Tencent is pleased to support the open source community by making libpag available. +// Tencent is pleased to support the open source community by making tgfx available. // // Copyright (C) 2026 Tencent. All rights reserved. // @@ -21,14 +21,19 @@ using namespace emscripten; -EMSCRIPTEN_BINDINGS(PAGXViewer) { - class_("PAGXView") - .smart_ptr>("PAGXView") - .class_function("MakeFrom", &pagx::PAGXView::MakeFrom) - .function("loadPAGX", &pagx::PAGXView::loadPAGX) - .function("updateSize", &pagx::PAGXView::updateSize) - .function("updateZoomScaleAndOffset", &pagx::PAGXView::updateZoomScaleAndOffset) - .function("draw", &pagx::PAGXView::draw) - .function("contentWidth", &pagx::PAGXView::contentWidth) - .function("contentHeight", &pagx::PAGXView::contentHeight); +using namespace emscripten; +using namespace pagx; + +EMSCRIPTEN_BINDINGS(PAGXView) { + class_("PAGXView") + .smart_ptr>("PAGXView") + .class_function("MakeFrom", &PAGXView::MakeFrom) + .function("width", &PAGXView::width) + .function("height", &PAGXView::height) + .function("loadPAGX", &PAGXView::loadPAGX) + .function("updateSize", &PAGXView::updateSize) + .function("updateZoomScaleAndOffset", &PAGXView::updateZoomScaleAndOffset) + .function("draw", &PAGXView::draw) + .function("contentWidth", &PAGXView::contentWidth) + .function("contentHeight", &PAGXView::contentHeight); } diff --git a/pagx/wechat/ts/binding.ts b/pagx/wechat/ts/binding.ts index cc9f616e8d..66c49b1658 100644 --- a/pagx/wechat/ts/binding.ts +++ b/pagx/wechat/ts/binding.ts @@ -26,6 +26,15 @@ const setColorSpace = (GL: EmscriptenGL, colorSpace: WindowColorSpace): boolean return true; }; +const hasWebpSupport = () => { + try { + return document.createElement('canvas').toDataURL('image/webp', 0.5).indexOf('data:image/webp') === 0; + } catch (err) { + console.log("wechat is not support webp"); + return false; + } +}; + /** * Binding pag js module on pag webassembly module. */ @@ -33,4 +42,5 @@ export const binding = (module: PAGX) => { TGFXBind(module); module.View = View; module.tgfx.setColorSpace = setColorSpace; + module.tgfx.hasWebpSupport = hasWebpSupport; }; diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.js b/pagx/wechat/wx_demo/pages/viewer/viewer.js index dab4920c15..0d2f4b51ff 100644 --- a/pagx/wechat/wx_demo/pages/viewer/viewer.js +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.js @@ -12,24 +12,24 @@ import { WXGestureManager } from '../../utils/gesture-manager'; const SAMPLE_FILES = [ { name: 'ColorPicker', - url: 'https://pag.io/pagx/samples/ColorPicker.pagx' - }, - { - name: 'complex3', - url: 'https://pag.io/pagx/testFiles/complex3.pagx' + url: 'https://pag.io/pagx/testFiles/ColorPicker.libpag.pagx' }, + // { + // name: 'complex3', + // url: 'https://pag.io/pagx/testFiles/complex3.pagx' + // }, { name: 'complex6', url: 'https://pag.io/pagx/testFiles/complex6.pagx' }, - { - name: 'path', - url: 'https://pag.io/pagx/testFiles/path.pagx' - }, - { - name: 'refStyle', - url: 'https://pag.io/pagx/testFiles/refStyle.pagx' - } + // { + // name: 'path', + // url: 'https://pag.io/pagx/testFiles/path.pagx' + // }, + // { + // name: 'refStyle', + // url: 'https://pag.io/pagx/testFiles/refStyle.pagx' + // } ]; Page({ From 5c8914e9b57f75a1087ed80bf5d5e7ed7ab3d5e4 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 23 Jan 2026 16:49:29 +0800 Subject: [PATCH 147/678] Fix SVG style inheritance for stroke properties and fill-opacity for URL fills. --- pagx/src/svg/SVGImporter.cpp | 146 ++++++++++++++++++++++++------- pagx/src/svg/SVGParserInternal.h | 16 ++-- 2 files changed, 127 insertions(+), 35 deletions(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index cbe93fcf41..e25960a41d 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -294,6 +294,36 @@ InheritedStyle SVGParserImpl::computeInheritedStyle(const std::shared_ptr SVGParserImpl::convertToLayer(const std::shared_ptr& element, std::vector>& contents, const InheritedStyle& inheritedStyle, - bool skipFillForShadow) { + bool shadowOnlyFilter) { const auto& tag = element->name; // Handle text element specially - it returns a Group with TextSpan. @@ -443,7 +473,7 @@ void SVGParserImpl::convertChildren(const std::shared_ptr& element, // Check if this is a use element referencing an image. // In that case, we don't add fill/stroke because the image already has its own fill. - bool skipFillStroke = skipFillForShadow; + bool skipFillStroke = false; if (tag == "use") { std::string href = getAttribute(element, "xlink:href"); if (href.empty()) { @@ -462,7 +492,19 @@ void SVGParserImpl::convertChildren(const std::shared_ptr& element, } if (!skipFillStroke) { - addFillStroke(element, contents, inheritedStyle); + if (shadowOnlyFilter) { + // For shadowOnly filters, we need to add a fill to provide a source for the shadow. + // The DropShadowFilter generates shadow based on the layer's content alpha channel. + // We use black (alpha=1) as the fill color since the actual shadow color is determined + // by the DropShadowFilter's color property, not the fill color. + auto fillNode = std::make_unique(); + auto solidColor = std::make_unique(); + solidColor->color = {0, 0, 0, 1, ColorSpace::SRGB}; + fillNode->color = std::move(solidColor); + contents.push_back(std::move(fillNode)); + } else { + addFillStroke(element, contents, inheritedStyle); + } } } @@ -973,36 +1015,35 @@ std::unique_ptr SVGParserImpl::convertPattern( std::string useTransform = getAttribute(child, "transform"); Matrix useMatrix = useTransform.empty() ? Matrix::Identity() : parseTransform(useTransform); - // Calculate the image's actual size in user space (considering content units and transform). - float imageSizeInUserSpaceX = 0; - float imageSizeInUserSpaceY = 0; + // Construct the complete transformation matrix for the ImagePattern. + // In SVG with patternContentUnits="objectBoundingBox", the transformation chain is: + // image pixels → useMatrix → pattern space (0-1) → shapeBounds → screen space + // + // tgfx ImagePattern matrix maps from screen coordinates to image coordinates. + // The tgfx shader internally inverts the matrix we provide, so we should provide + // the forward transformation (image pixels to screen pixels). + // + // Forward transform = T(shapeBounds.origin) * S(shapeBounds.size) * useMatrix + // + // This means: + // 1. useMatrix transforms image pixels to pattern space (0-1 coordinates) + // 2. Scale by shapeBounds.size transforms to user space dimensions + // 3. Translate by shapeBounds.origin positions the pattern at the shape location + + Matrix forwardMatrix = Matrix::Identity(); if (contentUnitsOBB) { - // When patternContentUnits is objectBoundingBox, image dimensions are 0-1 ratios. - // Apply use transform (e.g., scale(0.005)) to map image to content space, - // then scale by shape bounds to get user space size. - imageSizeInUserSpaceX = imageWidth * useMatrix.a * shapeBounds.width; - imageSizeInUserSpaceY = imageHeight * useMatrix.d * shapeBounds.height; + // Pattern content is in objectBoundingBox coordinates (0-1). + // Build the complete forward transform: image pixels → screen pixels + forwardMatrix = Matrix::Translate(shapeBounds.x, shapeBounds.y) * + Matrix::Scale(shapeBounds.width, shapeBounds.height) * + useMatrix; } else { - // When patternContentUnits is userSpaceOnUse, image dimensions are in user space. - imageSizeInUserSpaceX = imageWidth * useMatrix.a; - imageSizeInUserSpaceY = imageHeight * useMatrix.d; + // Pattern content is in userSpaceOnUse coordinates. + // useMatrix transforms image directly to user space, then translate to shape bounds. + forwardMatrix = Matrix::Translate(shapeBounds.x, shapeBounds.y) * useMatrix; } - // The ImagePattern shader tiles the original image pixels. - // We need to scale the image so it renders at the correct size within the tile. - // Since tgfx ImagePattern uses the image's original pixel dimensions as the base, - // the matrix should scale the image to match imageSizeInUserSpace. - // Note: imageWidth here is the SVG display size, which equals original pixel size - // when the image is embedded at 1:1 scale. - float scaleX = imageSizeInUserSpaceX / imageWidth; - float scaleY = imageSizeInUserSpaceY / imageHeight; - - // PAGX ImagePattern coordinates are relative to the geometry's local origin (0,0). - // SVG pattern with objectBoundingBox is relative to the shape's bounding box. - // We need to translate the pattern to align with the shape bounds. - // Matrix multiplication order: translate first, then scale (right to left). - pattern->matrix = Matrix::Translate(shapeBounds.x, shapeBounds.y) * - Matrix::Scale(scaleX, scaleY); + pattern->matrix = forwardMatrix; } } else if (child->name == "image") { // Direct image element inside pattern. @@ -1061,6 +1102,14 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, std::string refId = resolveUrl(fill); // Use getColorSourceForRef which handles reference counting. fillNode->color = getColorSourceForRef(refId, shapeBounds); + // Apply fill-opacity even for url() fills. + std::string fillOpacity = getAttribute(element, "fill-opacity"); + if (fillOpacity.empty()) { + fillOpacity = inheritedStyle.fillOpacity; + } + if (!fillOpacity.empty()) { + fillNode->alpha = std::stof(fillOpacity); + } contents.push_back(std::move(fillNode)); } else { auto fillNode = std::make_unique(); @@ -1126,26 +1175,41 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } std::string strokeWidth = getAttribute(element, "stroke-width"); + if (strokeWidth.empty()) { + strokeWidth = inheritedStyle.strokeWidth; + } if (!strokeWidth.empty()) { strokeNode->width = parseLength(strokeWidth, _viewBoxWidth); } std::string strokeLinecap = getAttribute(element, "stroke-linecap"); + if (strokeLinecap.empty()) { + strokeLinecap = inheritedStyle.strokeLinecap; + } if (!strokeLinecap.empty()) { strokeNode->cap = LineCapFromString(strokeLinecap); } std::string strokeLinejoin = getAttribute(element, "stroke-linejoin"); + if (strokeLinejoin.empty()) { + strokeLinejoin = inheritedStyle.strokeLinejoin; + } if (!strokeLinejoin.empty()) { strokeNode->join = LineJoinFromString(strokeLinejoin); } std::string strokeMiterlimit = getAttribute(element, "stroke-miterlimit"); + if (strokeMiterlimit.empty()) { + strokeMiterlimit = inheritedStyle.strokeMiterlimit; + } if (!strokeMiterlimit.empty()) { strokeNode->miterLimit = std::stof(strokeMiterlimit); } std::string dashArray = getAttribute(element, "stroke-dasharray"); + if (dashArray.empty()) { + dashArray = inheritedStyle.strokeDasharray; + } if (!dashArray.empty() && dashArray != "none") { // Parse dash array values, which may contain units (e.g., "2px,2px" or "2,2"). // Use parseLength to handle both numeric values and values with units. @@ -1164,6 +1228,9 @@ void SVGParserImpl::addFillStroke(const std::shared_ptr& element, } std::string dashOffset = getAttribute(element, "stroke-dashoffset"); + if (dashOffset.empty()) { + dashOffset = inheritedStyle.strokeDashoffset; + } if (!dashOffset.empty()) { strokeNode->dashOffset = parseLength(dashOffset, _viewBoxWidth); } @@ -2138,10 +2205,29 @@ bool SVGParserImpl::convertFilterElement( } // Check for standard drop shadow pattern starting with feGaussianBlur in="SourceAlpha". - // Pattern: feGaussianBlur(in=SourceAlpha) → feOffset → ... + // Pattern: feGaussianBlur(in=SourceAlpha) → feOffset → [feColorMatrix] + // Note: Inner shadow pattern is similar but has feComposite(arithmetic) after feOffset. + // We skip inner shadow patterns as they are not supported by PAGX conversion. if (node->name == "feGaussianBlur" && getAttribute(node, "in") == "SourceAlpha") { // Look for feOffset following the blur. if (i + 1 < primitives.size() && primitives[i + 1]->name == "feOffset") { + // Check if this is an inner shadow pattern (has feComposite with arithmetic operator). + // Inner shadow pattern: feGaussianBlur → feOffset → feComposite(arithmetic) → feColorMatrix + bool isInnerShadow = false; + if (i + 2 < primitives.size() && primitives[i + 2]->name == "feComposite") { + std::string compositeOp = getAttribute(primitives[i + 2], "operator"); + if (compositeOp == "arithmetic") { + isInnerShadow = true; + } + } + + if (isInnerShadow) { + // Skip inner shadow pattern - not supported by PAGX conversion. + // Inner shadow is a complex effect that cannot be represented as DropShadowFilter. + i++; + continue; + } + // Extract blur from feGaussianBlur. std::string stdDeviation = getAttribute(node, "stdDeviation", "0"); auto blurValues = ParseSpaceSeparatedFloats(stdDeviation); diff --git a/pagx/src/svg/SVGParserInternal.h b/pagx/src/svg/SVGParserInternal.h index 48ca93fc10..f6670df5a3 100644 --- a/pagx/src/svg/SVGParserInternal.h +++ b/pagx/src/svg/SVGParserInternal.h @@ -48,11 +48,17 @@ namespace pagx { * Inherited SVG style properties that cascade down the element tree. */ struct InheritedStyle { - std::string fill = ""; // Empty means not set, "none" means no fill. - std::string stroke = ""; // Empty means not set. - std::string fillOpacity = ""; // Empty means not set. - std::string strokeOpacity = ""; // Empty means not set. - std::string fillRule = ""; // Empty means not set. + std::string fill = ""; // Empty means not set, "none" means no fill. + std::string stroke = ""; // Empty means not set. + std::string fillOpacity = ""; // Empty means not set. + std::string strokeOpacity = ""; // Empty means not set. + std::string fillRule = ""; // Empty means not set. + std::string strokeDasharray = ""; // Empty means not set, "none" means solid line. + std::string strokeDashoffset = "";// Empty means not set. + std::string strokeWidth = ""; // Empty means not set. + std::string strokeLinecap = ""; // Empty means not set. + std::string strokeLinejoin = ""; // Empty means not set. + std::string strokeMiterlimit = "";// Empty means not set. }; /** From 8a308b7899acb178110a67af66a13d4a4fe8c15b Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 23 Jan 2026 16:58:43 +0800 Subject: [PATCH 148/678] Add support for solid drop shadow filter pattern without blur effect. --- pagx/src/svg/SVGImporter.cpp | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index e25960a41d..7e9a74e555 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -2202,6 +2202,50 @@ bool SVGParserImpl::convertFilterElement( } continue; } + + // Check for Figma-style solid drop shadow pattern (no blur): + // feColorMatrix(in=SourceAlpha) → feOffset → feColorMatrix → [feBlend] + // This creates a solid shadow without blur effect. + if (i + 2 < primitives.size() && primitives[i + 1]->name == "feOffset" && + primitives[i + 2]->name == "feColorMatrix") { + // Extract offset from feOffset. + float offsetX = 0, offsetY = 0; + std::string dx = getAttribute(primitives[i + 1], "dx", "0"); + std::string dy = getAttribute(primitives[i + 1], "dy", "0"); + offsetX = strtof(dx.c_str(), nullptr); + offsetY = strtof(dy.c_str(), nullptr); + + // Extract color from feColorMatrix. + // Format: "0 0 0 0 R 0 0 0 0 G 0 0 0 0 B 0 0 0 A 0" where R,G,B are 0-1 and A is alpha. + Color shadowColor = {0, 0, 0, 1.0f}; + std::string colorMatrix = getAttribute(primitives[i + 2], "values"); + if (!colorMatrix.empty()) { + auto values = ParseSpaceSeparatedFloats(colorMatrix); + // R is at index 4, G at index 9, B at index 14, A at index 18. + if (values.size() >= 19) { + shadowColor.red = values[4]; + shadowColor.green = values[9]; + shadowColor.blue = values[14]; + shadowColor.alpha = values[18]; + } + } + + auto dropShadow = std::make_unique(); + dropShadow->offsetX = offsetX; + dropShadow->offsetY = offsetY; + dropShadow->blurrinessX = 0; + dropShadow->blurrinessY = 0; + dropShadow->color = shadowColor; + dropShadow->shadowOnly = shadowOnly; + filters.push_back(std::move(dropShadow)); + + // Skip the consumed primitives (3 elements) plus any feBlend that follows. + i += 3; + while (i < primitives.size() && primitives[i]->name == "feBlend") { + i++; + } + continue; + } } // Check for standard drop shadow pattern starting with feGaussianBlur in="SourceAlpha". From 0440b235dd35b60f9bfcd2b29290f9dcd88cdc93 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 23 Jan 2026 17:14:31 +0800 Subject: [PATCH 149/678] Add support for CSS class style rules in SVG style element. --- pagx/src/svg/SVGImporter.cpp | 170 ++++++++++++++++++++++++++++++- pagx/src/svg/SVGParserInternal.h | 7 ++ 2 files changed, 176 insertions(+), 1 deletion(-) diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 7e9a74e555..d63f6973b9 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -83,7 +83,7 @@ std::shared_ptr SVGParserImpl::parseFile(const std::string& filePa std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, const std::string& name, const std::string& defaultValue) const { - // CSS priority: style attribute > presentation attribute (direct attribute) + // CSS priority: style attribute > presentation attribute > CSS class rules // Check style attribute first for CSS property. // Style attribute format: "property1: value1; property2: value2; ..." // CSS cascade rule: later properties override earlier ones. @@ -155,6 +155,74 @@ std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, return value; } + // Check CSS class rules (lowest priority). + // class attribute can contain multiple class names separated by whitespace. + auto [hasClass, classAttr] = node->findAttribute("class"); + if (hasClass && !classAttr.empty()) { + // Parse class names. + size_t pos = 0; + while (pos < classAttr.size()) { + // Skip whitespace. + while (pos < classAttr.size() && std::isspace(classAttr[pos])) { + pos++; + } + if (pos >= classAttr.size()) { + break; + } + // Find end of class name. + size_t endPos = pos; + while (endPos < classAttr.size() && !std::isspace(classAttr[endPos])) { + endPos++; + } + std::string className = classAttr.substr(pos, endPos - pos); + pos = endPos; + + // Look up the class in CSS rules. + auto it = _cssClassRules.find(className); + if (it != _cssClassRules.end()) { + // Parse the style string to find the property we need. + const std::string& classStyle = it->second; + size_t stylePos = 0; + while (stylePos < classStyle.size()) { + // Skip whitespace. + while (stylePos < classStyle.size() && std::isspace(classStyle[stylePos])) { + stylePos++; + } + // Find property name end (colon). + size_t colonPos = classStyle.find(':', stylePos); + if (colonPos == std::string::npos) { + break; + } + // Extract property name and trim whitespace. + std::string currentProp = classStyle.substr(stylePos, colonPos - stylePos); + size_t propStart = currentProp.find_first_not_of(" \t"); + size_t propEnd = currentProp.find_last_not_of(" \t"); + if (propStart != std::string::npos && propEnd != std::string::npos) { + currentProp = currentProp.substr(propStart, propEnd - propStart + 1); + } + // Find value end (semicolon or end of string). + size_t semicolonPos = classStyle.find(';', colonPos); + if (semicolonPos == std::string::npos) { + semicolonPos = classStyle.size(); + } + // Check if this is the property we're looking for. + if (currentProp == name) { + // Extract and trim the value. + std::string propValue = classStyle.substr(colonPos + 1, semicolonPos - colonPos - 1); + size_t valStart = propValue.find_first_not_of(" \t"); + size_t valEnd = propValue.find_last_not_of(" \t"); + if (valStart != std::string::npos && valEnd != std::string::npos) { + return propValue.substr(valStart, valEnd - valStart + 1); + } + return propValue; + } + // Move to next property. + stylePos = semicolonPos + 1; + } + } + } + } + return defaultValue; } @@ -203,6 +271,24 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr child = child->getNextSibling(); } + // Parse + + +{draft_banner}
+ +
+ {content} +
+
+ + + + + + +''' + + +def main(): + parser = argparse.ArgumentParser( + description='Publish PAGX specification as HTML', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + # Publish to default location (pagx/.spec_output/) + python publish_spec.py + + # Publish as draft + python publish_spec.py --draft + + # Publish to custom directory + python publish_spec.py --output /path/to/output + + # Publish as draft to custom directory + python publish_spec.py --draft --output /path/to/output +''' + ) + parser.add_argument( + '--output', '-o', + type=Path, + default=DEFAULT_OUTPUT_DIR, + help=f'Output directory (default: {DEFAULT_OUTPUT_DIR})' + ) + parser.add_argument( + '--draft', '-d', + action='store_true', + help='Show draft banner at the top of the page' + ) + args = parser.parse_args() + + # Check if source file exists + if not SPEC_FILE.exists(): + print(f"Error: Specification file not found: {SPEC_FILE}") + sys.exit(1) + + # Read Markdown content + print(f"Reading: {SPEC_FILE}") + with open(SPEC_FILE, 'r', encoding='utf-8') as f: + md_content = f.read() + + # Extract title from first heading + title_match = re.match(r'^#\s+(.+)$', md_content, re.MULTILINE) + title = title_match.group(1) if title_match else 'PAGX Format Specification' + + # Parse headings for TOC + headings = parse_markdown_headings(md_content) + + # Generate TOC HTML + toc_html = generate_toc_html(headings) + + # Convert Markdown to HTML + html_content = convert_markdown_to_html(md_content) + + # Generate complete HTML document + html = generate_html(html_content, title, toc_html, args.draft) + + # Create output directory + args.output.mkdir(parents=True, exist_ok=True) + + # Write output file + output_file = args.output / 'index.html' + with open(output_file, 'w', encoding='utf-8') as f: + f.write(html) + + print(f"Published to: {output_file}") + if args.draft: + print(" (with draft banner)") + + +if __name__ == '__main__': + main() From a28177c95cce4c455612f0f5cf0574ec2630968d Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 09:58:00 +0800 Subject: [PATCH 234/678] Replace Python publish_spec.py with Node.js version. --- pagx/scripts/publish_spec.js | 696 +++++++++++++++++++++++++++++++++++ pagx/scripts/publish_spec.py | 508 ------------------------- 2 files changed, 696 insertions(+), 508 deletions(-) create mode 100755 pagx/scripts/publish_spec.js delete mode 100755 pagx/scripts/publish_spec.py diff --git a/pagx/scripts/publish_spec.js b/pagx/scripts/publish_spec.js new file mode 100755 index 0000000000..141ec6e1ad --- /dev/null +++ b/pagx/scripts/publish_spec.js @@ -0,0 +1,696 @@ +#!/usr/bin/env node +/** + * PAGX Specification Publisher + * + * Converts the PAGX specification Markdown document to a standalone HTML page + * with syntax highlighting and optional draft banner. + * + * Usage: + * node publish_spec.js [--output ] [--draft] + * + * Options: + * --output, -o Output directory (default: pagx/.spec_output/) + * --draft, -d Show draft banner at the top of the page + */ + +const fs = require('fs'); +const path = require('path'); + +// Default paths +const SCRIPT_DIR = __dirname; +const PAGX_DIR = path.dirname(SCRIPT_DIR); +const SPEC_FILE = path.join(PAGX_DIR, 'docs', 'pagx_spec.md'); +const DEFAULT_OUTPUT_DIR = path.join(PAGX_DIR, '.spec_output'); + +/** + * Convert heading text to URL-friendly slug. + */ +function slugify(text) { + let slug = text.toLowerCase(); + // Remove special characters but keep Chinese characters and alphanumeric + slug = slug.replace(/[^\w\u4e00-\u9fff\s-]/g, ''); + slug = slug.replace(/[\s_]+/g, '-'); + slug = slug.replace(/-+/g, '-'); + slug = slug.replace(/^-|-$/g, ''); + return slug || 'section'; +} + +/** + * Extract headings from Markdown for TOC generation. + */ +function parseMarkdownHeadings(content) { + const headings = []; + const lines = content.split('\n'); + for (const line of lines) { + const match = line.match(/^(#{1,6})\s+(.+)$/); + if (match) { + const level = match[1].length; + const text = match[2].trim(); + const slug = slugify(text); + headings.push({ level, text, slug }); + } + } + return headings; +} + +/** + * Generate HTML for table of contents. + */ +function generateTocHtml(headings) { + if (!headings.length) { + return ''; + } + + const html = ['
    ']; + const stack = [1]; + + for (let i = 0; i < headings.length; i++) { + const heading = headings[i]; + const level = heading.level; + + // Skip h1 in TOC since it's the document title + if (level === 1) { + continue; + } + + // Adjust for starting from h2 + const adjustedLevel = level - 1; + + // Close lists if going up + while (stack.length > adjustedLevel) { + html.push('
'); + stack.pop(); + } + + // Open new lists if going down + while (stack.length < adjustedLevel) { + html.push('
    • '); + stack.push(stack.length + 1); + } + + // Add the item + html.push(`
    • ${heading.text}`); + + // Check if next heading is a child + const nextIdx = i + 1; + if (nextIdx < headings.length && headings[nextIdx].level > level) { + html.push('
        '); + stack.push(stack.length + 1); + } else { + html.push(''); + } + } + + // Close remaining lists + while (stack.length > 1) { + html.push('
    • '); + stack.pop(); + } + + html.push('
    '); + return html.join('\n'); +} + +/** + * Escape HTML special characters. + */ +function escapeHtml(text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Convert Markdown content to HTML. + */ +function convertMarkdownToHtml(content, headings) { + // Create a map for heading slugs to handle duplicates + const slugCounts = {}; + + // Process code blocks first to protect them + const codeBlocks = []; + let processedContent = content.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => { + const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`; + const langClass = lang ? ` class="language-${lang}"` : ''; + codeBlocks.push(`
    ${escapeHtml(code.trim())}
    `); + return placeholder; + }); + + // Process inline code + processedContent = processedContent.replace(/`([^`]+)`/g, '$1'); + + // Process headings with IDs + processedContent = processedContent.replace(/^(#{1,6})\s+(.+)$/gm, (match, hashes, text) => { + const level = hashes.length; + let slug = slugify(text); + + // Handle duplicate slugs + if (slugCounts[slug] !== undefined) { + slugCounts[slug]++; + slug = `${slug}-${slugCounts[slug]}`; + } else { + slugCounts[slug] = 0; + } + + return `${text}`; + }); + + // Process bold + processedContent = processedContent.replace(/\*\*([^*]+)\*\*/g, '$1'); + + // Process italic + processedContent = processedContent.replace(/\*([^*]+)\*/g, '$1'); + + // Process links + processedContent = processedContent.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // Process horizontal rules + processedContent = processedContent.replace(/^---+$/gm, '
    '); + + // Process blockquotes + processedContent = processedContent.replace(/^>\s*(.*)$/gm, '
    $1
    '); + // Merge adjacent blockquotes + processedContent = processedContent.replace(/<\/blockquote>\n
    /g, '\n'); + + // Process tables + processedContent = processedContent.replace( + /^\|(.+)\|\n\|[-:| ]+\|\n((?:\|.+\|\n?)+)/gm, + (match, headerRow, bodyRows) => { + const headers = headerRow.split('|').map((h) => h.trim()).filter(Boolean); + const headerHtml = headers.map((h) => `${h}`).join(''); + + const rows = bodyRows.trim().split('\n'); + const bodyHtml = rows + .map((row) => { + const cells = row.split('|').map((c) => c.trim()).filter(Boolean); + return `${cells.map((c) => `${c}`).join('')}`; + }) + .join('\n'); + + return `\n${headerHtml}\n\n${bodyHtml}\n\n
    `; + } + ); + + // Process unordered lists + let inList = false; + const lines = processedContent.split('\n'); + const processedLines = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const listMatch = line.match(/^(\s*)[-*]\s+(.+)$/); + + if (listMatch) { + if (!inList) { + processedLines.push('
      '); + inList = true; + } + processedLines.push(`
    • ${listMatch[2]}
    • `); + } else { + if (inList && line.trim() !== '') { + processedLines.push('
    '); + inList = false; + } + processedLines.push(line); + } + } + if (inList) { + processedLines.push(''); + } + processedContent = processedLines.join('\n'); + + // Process ordered lists + inList = false; + const lines2 = processedContent.split('\n'); + const processedLines2 = []; + + for (let i = 0; i < lines2.length; i++) { + const line = lines2[i]; + const listMatch = line.match(/^(\s*)\d+\.\s+(.+)$/); + + if (listMatch) { + if (!inList) { + processedLines2.push('
      '); + inList = true; + } + processedLines2.push(`
    1. ${listMatch[2]}
    2. `); + } else { + if (inList && line.trim() !== '') { + processedLines2.push('
    '); + inList = false; + } + processedLines2.push(line); + } + } + if (inList) { + processedLines2.push(''); + } + processedContent = processedLines2.join('\n'); + + // Process paragraphs - wrap non-HTML lines in

    tags + const finalLines = processedContent.split('\n'); + const result = []; + let paragraph = []; + + for (const line of finalLines) { + const trimmed = line.trim(); + + // Check if line starts with HTML tag or is a placeholder + const isHtml = + trimmed.startsWith('<') || + trimmed.startsWith('__CODE_BLOCK_') || + trimmed === ''; + + if (isHtml) { + if (paragraph.length > 0) { + result.push(`

    ${paragraph.join(' ')}

    `); + paragraph = []; + } + if (trimmed !== '') { + result.push(line); + } + } else { + paragraph.push(trimmed); + } + } + if (paragraph.length > 0) { + result.push(`

    ${paragraph.join(' ')}

    `); + } + + processedContent = result.join('\n'); + + // Restore code blocks + for (let i = 0; i < codeBlocks.length; i++) { + processedContent = processedContent.replace(`__CODE_BLOCK_${i}__`, codeBlocks[i]); + } + + return processedContent; +} + +/** + * Generate the complete HTML document. + */ +function generateHtml(content, title, tocHtml, showDraft) { + let draftBanner = ''; + let draftStyles = ''; + + if (showDraft) { + draftBanner = `
    + DRAFT This specification is a working draft and may change at any time. 本规范为草稿版本,内容可能随时变更。 +
    +`; + draftStyles = ` .sidebar { + top: 42px; + height: calc(100vh - 42px); + } + .content { + padding-top: 82px; + } + @media (max-width: 900px) { + .content { + padding-top: 62px; + } + }`; + } + + return ` + + + + + ${title} + + + + +${draftBanner}
    + +
    + ${content} +
    +
    + + + + + + +`; +} + +/** + * Parse command line arguments. + */ +function parseArgs() { + const args = process.argv.slice(2); + const options = { + output: DEFAULT_OUTPUT_DIR, + draft: false, + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--output' || arg === '-o') { + options.output = args[++i]; + } else if (arg === '--draft' || arg === '-d') { + options.draft = true; + } else if (arg === '--help' || arg === '-h') { + console.log(` +PAGX Specification Publisher + +Usage: + node publish_spec.js [--output ] [--draft] + +Options: + --output, -o Output directory (default: pagx/.spec_output/) + --draft, -d Show draft banner at the top of the page + --help, -h Show this help message + +Examples: + # Publish to default location (pagx/.spec_output/) + node publish_spec.js + + # Publish as draft + node publish_spec.js --draft + + # Publish to custom directory + node publish_spec.js --output /path/to/output + + # Publish as draft to custom directory + node publish_spec.js --draft --output /path/to/output +`); + process.exit(0); + } + } + + return options; +} + +/** + * Main function. + */ +function main() { + const options = parseArgs(); + + // Check if source file exists + if (!fs.existsSync(SPEC_FILE)) { + console.error(`Error: Specification file not found: ${SPEC_FILE}`); + process.exit(1); + } + + // Read Markdown content + console.log(`Reading: ${SPEC_FILE}`); + const mdContent = fs.readFileSync(SPEC_FILE, 'utf-8'); + + // Extract title from first heading + const titleMatch = mdContent.match(/^#\s+(.+)$/m); + const title = titleMatch ? titleMatch[1] : 'PAGX Format Specification'; + + // Parse headings for TOC + const headings = parseMarkdownHeadings(mdContent); + + // Generate TOC HTML + const tocHtml = generateTocHtml(headings); + + // Convert Markdown to HTML + const htmlContent = convertMarkdownToHtml(mdContent, headings); + + // Generate complete HTML document + const html = generateHtml(htmlContent, title, tocHtml, options.draft); + + // Create output directory + fs.mkdirSync(options.output, { recursive: true }); + + // Write output file + const outputFile = path.join(options.output, 'index.html'); + fs.writeFileSync(outputFile, html, 'utf-8'); + + console.log(`Published to: ${outputFile}`); + if (options.draft) { + console.log(' (with draft banner)'); + } +} + +main(); diff --git a/pagx/scripts/publish_spec.py b/pagx/scripts/publish_spec.py deleted file mode 100755 index 75ab290eb4..0000000000 --- a/pagx/scripts/publish_spec.py +++ /dev/null @@ -1,508 +0,0 @@ -#!/usr/bin/env python3 -""" -PAGX Specification Publisher - -Converts the PAGX specification Markdown document to a standalone HTML page -with syntax highlighting and optional draft banner. - -Usage: - python publish_spec.py [--output ] [--draft] - -Options: - --output, -o Output directory (default: pagx/.spec_output/) - --draft, -d Show draft banner at the top of the page -""" - -import argparse -import os -import re -import sys -from pathlib import Path - -# Default paths -SCRIPT_DIR = Path(__file__).parent -PAGX_DIR = SCRIPT_DIR.parent -SPEC_FILE = PAGX_DIR / "docs" / "pagx_spec.md" -DEFAULT_OUTPUT_DIR = PAGX_DIR / ".spec_output" - - -def slugify(text: str, separator: str = '-') -> str: - """Convert heading text to URL-friendly slug.""" - # Remove special characters but keep Chinese characters - text = text.lower() - # Handle text with both English and Chinese - text = re.sub(r'[^\w\u4e00-\u9fff\s-]', '', text) - text = re.sub(r'[\s_]+', separator, text) - text = re.sub(f'{separator}+', separator, text) - text = text.strip(separator) - return text if text else 'section' - - -def parse_markdown_headings(content: str) -> list: - """Extract headings from Markdown for TOC generation.""" - headings = [] - lines = content.split('\n') - for line in lines: - match = re.match(r'^(#{1,6})\s+(.+)$', line) - if match: - level = len(match.group(1)) - text = match.group(2).strip() - slug = slugify(text) - headings.append({'level': level, 'text': text, 'slug': slug}) - return headings - - -def generate_toc_html(headings: list) -> str: - """Generate HTML for table of contents.""" - if not headings: - return '' - - html = ['
      '] - stack = [1] # Track nesting level - - for heading in headings: - level = heading['level'] - - # Skip h1 in TOC since it's the document title - if level == 1: - continue - - # Adjust for starting from h2 - adjusted_level = level - 1 - - # Close lists if going up - while len(stack) > adjusted_level: - html.append('
  • ') - stack.pop() - - # Open new lists if going down - while len(stack) < adjusted_level: - html.append('
    • ') - stack.append(len(stack) + 1) - - # Add the item - html.append(f'
    • {heading["text"]}') - - # Check if next heading is a child - next_idx = headings.index(heading) + 1 - if next_idx < len(headings) and headings[next_idx]['level'] > level: - html.append('
        ') - stack.append(len(stack) + 1) - else: - html.append('') - - # Close remaining lists - while len(stack) > 1: - html.append('
    • ') - stack.pop() - - html.append('
    ') - return '\n'.join(html) - - -def convert_markdown_to_html(content: str) -> str: - """Convert Markdown content to HTML.""" - try: - import markdown - from markdown.extensions.codehilite import CodeHiliteExtension - from markdown.extensions.fenced_code import FencedCodeExtension - from markdown.extensions.tables import TableExtension - from markdown.extensions.toc import TocExtension - - md = markdown.Markdown(extensions=[ - 'fenced_code', - 'tables', - 'codehilite', - TocExtension(slugify=slugify, toc_depth=6), - ], extension_configs={ - 'codehilite': { - 'css_class': 'codehilite', - 'guess_lang': False, - } - }) - return md.convert(content) - except ImportError: - print("Error: 'markdown' package is required. Install with: pip install markdown") - sys.exit(1) - - -def generate_html(content: str, title: str, toc_html: str, show_draft: bool) -> str: - """Generate the complete HTML document.""" - draft_banner = '' - draft_styles = '' - - if show_draft: - draft_banner = '''
    - DRAFT This specification is a working draft and may change at any time. 本规范为草稿版本,内容可能随时变更。 -
    -''' - draft_styles = ''' .sidebar { - top: 42px; - height: calc(100vh - 42px); - } - .content { - padding-top: 82px; - } - @media (max-width: 900px) { - .content { - padding-top: 62px; - } - }''' - - return f''' - - - - - {title} - - - - -{draft_banner}
    - -
    - {content} -
    -
    - - - - - - -''' - - -def main(): - parser = argparse.ArgumentParser( - description='Publish PAGX specification as HTML', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=''' -Examples: - # Publish to default location (pagx/.spec_output/) - python publish_spec.py - - # Publish as draft - python publish_spec.py --draft - - # Publish to custom directory - python publish_spec.py --output /path/to/output - - # Publish as draft to custom directory - python publish_spec.py --draft --output /path/to/output -''' - ) - parser.add_argument( - '--output', '-o', - type=Path, - default=DEFAULT_OUTPUT_DIR, - help=f'Output directory (default: {DEFAULT_OUTPUT_DIR})' - ) - parser.add_argument( - '--draft', '-d', - action='store_true', - help='Show draft banner at the top of the page' - ) - args = parser.parse_args() - - # Check if source file exists - if not SPEC_FILE.exists(): - print(f"Error: Specification file not found: {SPEC_FILE}") - sys.exit(1) - - # Read Markdown content - print(f"Reading: {SPEC_FILE}") - with open(SPEC_FILE, 'r', encoding='utf-8') as f: - md_content = f.read() - - # Extract title from first heading - title_match = re.match(r'^#\s+(.+)$', md_content, re.MULTILINE) - title = title_match.group(1) if title_match else 'PAGX Format Specification' - - # Parse headings for TOC - headings = parse_markdown_headings(md_content) - - # Generate TOC HTML - toc_html = generate_toc_html(headings) - - # Convert Markdown to HTML - html_content = convert_markdown_to_html(md_content) - - # Generate complete HTML document - html = generate_html(html_content, title, toc_html, args.draft) - - # Create output directory - args.output.mkdir(parents=True, exist_ok=True) - - # Write output file - output_file = args.output / 'index.html' - with open(output_file, 'w', encoding='utf-8') as f: - f.write(html) - - print(f"Published to: {output_file}") - if args.draft: - print(" (with draft banner)") - - -if __name__ == '__main__': - main() From 328d591ac66377aa9f2e9b501eb1c9ebe6942af4 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 10:03:28 +0800 Subject: [PATCH 235/678] Remove basePath from PAGXDocument and resolve paths during import phase. --- pagx/include/pagx/LayerBuilder.h | 4 ---- pagx/include/pagx/PAGXDocument.h | 5 ----- pagx/src/PAGXImporter.cpp | 15 ++++++++++++- pagx/src/svg/SVGImporter.cpp | 17 ++++++++++++++- pagx/src/tgfx/LayerBuilder.cpp | 36 +++++--------------------------- test/src/PAGXTest.cpp | 18 ++++++++++++---- 6 files changed, 49 insertions(+), 46 deletions(-) diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index 8d40bca103..6b5e86b232 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -49,10 +49,6 @@ struct PAGXContent { * Build options for LayerBuilder. */ struct LayerBuildOptions { - /** - * Base path for resolving relative resource paths. - */ - std::string basePath = {}; }; /** diff --git a/pagx/include/pagx/PAGXDocument.h b/pagx/include/pagx/PAGXDocument.h index 557bbad15e..829431b3c4 100644 --- a/pagx/include/pagx/PAGXDocument.h +++ b/pagx/include/pagx/PAGXDocument.h @@ -60,11 +60,6 @@ class PAGXDocument { */ std::vector layers = {}; - /** - * Base path for resolving relative resource paths. - */ - std::string basePath = {}; - /** * Creates a node of the specified type and adds it to the document management. * If an ID is provided, the node will be indexed for lookup. diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 1c93def0c0..9c5a69e4ce 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -1724,9 +1724,22 @@ std::shared_ptr PAGXImporter::FromFile(const std::string& filePath auto doc = FromXML(content); if (doc) { + // Convert relative paths to absolute paths + std::string basePath = {}; auto lastSlash = filePath.find_last_of("/\\"); if (lastSlash != std::string::npos) { - doc->basePath = filePath.substr(0, lastSlash + 1); + basePath = filePath.substr(0, lastSlash + 1); + } + if (!basePath.empty()) { + for (auto& node : doc->nodes) { + if (node->nodeType() == NodeType::Image) { + auto* image = static_cast(node.get()); + if (!image->filePath.empty() && image->filePath[0] != '/' && + image->filePath.find("://") == std::string::npos) { + image->filePath = basePath + image->filePath; + } + } + } } } return doc; diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index 328231c980..b5bf5c0e65 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -24,6 +24,7 @@ #include "StringParser.h" #include "SVGPathParser.h" #include "pagx/PAGXDocument.h" +#include "pagx/nodes/Image.h" #include "pagx/nodes/SolidColor.h" #include "SVGParserInternal.h" #include "xml/XMLDOM.h" @@ -35,9 +36,23 @@ std::shared_ptr SVGImporter::Parse(const std::string& filePath, SVGParserImpl parser(options); auto doc = parser.parseFile(filePath); if (doc) { + // Convert relative paths to absolute paths + std::string basePath = {}; auto lastSlash = filePath.find_last_of("/\\"); if (lastSlash != std::string::npos) { - doc->basePath = filePath.substr(0, lastSlash + 1); + basePath = filePath.substr(0, lastSlash + 1); + } + if (!basePath.empty()) { + for (auto& node : doc->nodes) { + if (node->nodeType() == NodeType::Image) { + auto* image = static_cast(node.get()); + if (!image->filePath.empty() && image->filePath[0] != '/' && + image->filePath.find("://") == std::string::npos && + image->filePath.find("data:") != 0) { + image->filePath = basePath + image->filePath; + } + } + } } } return doc; diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index b091532370..c3ae8ce12a 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -661,17 +661,8 @@ class LayerBuilderImpl { } } } else if (!imageNode->filePath.empty()) { - // External file path - std::string fullPath = imageNode->filePath; - if (_document && imageNode->filePath[0] != '/' && !_document->basePath.empty()) { - // Ensure basePath ends with '/' before concatenating - if (_document->basePath.back() != '/') { - fullPath = _document->basePath + "/" + imageNode->filePath; - } else { - fullPath = _document->basePath + imageNode->filePath; - } - } - codec = tgfx::ImageCodec::MakeFrom(fullPath); + // External file path (already resolved to absolute during import) + codec = tgfx::ImageCodec::MakeFrom(imageNode->filePath); } if (codec) { @@ -803,16 +794,8 @@ class LayerBuilderImpl { } else if (imageNode->filePath.find("data:") == 0) { image = ImageFromDataURI(imageNode->filePath); } else if (!imageNode->filePath.empty()) { - std::string imagePath = imageNode->filePath; - if (!_options.basePath.empty() && imagePath[0] != '/') { - // Ensure basePath ends with '/' before concatenating - if (_options.basePath.back() != '/') { - imagePath = _options.basePath + "/" + imagePath; - } else { - imagePath = _options.basePath + imagePath; - } - } - image = tgfx::Image::MakeFromFile(imagePath); + // External file path (already resolved to absolute during import) + image = tgfx::Image::MakeFromFile(imageNode->filePath); } if (!image) { @@ -1015,16 +998,7 @@ PAGXContent LayerBuilder::FromFile(const std::string& filePath, const Options& o if (!document) { return {}; } - - auto opts = options; - if (opts.basePath.empty()) { - auto lastSlash = filePath.find_last_of("/\\"); - if (lastSlash != std::string::npos) { - opts.basePath = filePath.substr(0, lastSlash + 1); - } - } - - return Build(*document, opts); + return Build(*document, options); } PAGXContent LayerBuilder::FromData(const uint8_t* data, size_t length, const Options& options) { diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index e5039402e1..6d379710b5 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -33,6 +33,7 @@ #include "pagx/nodes/Ellipse.h" #include "pagx/nodes/Fill.h" #include "pagx/nodes/Group.h" +#include "pagx/nodes/Image.h" #include "pagx/nodes/LinearGradient.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/PathData.h" @@ -1395,15 +1396,24 @@ PAG_TEST(PAGXTest, CompleteExample) { auto doc = pagx::PAGXImporter::FromXML(pagxXml); ASSERT_TRUE(doc != nullptr); + // Manually resolve relative Image paths since we parsed from XML string + for (auto& node : doc->nodes) { + if (node->nodeType() == pagx::NodeType::Image) { + auto* image = static_cast(node.get()); + if (!image->filePath.empty() && image->filePath[0] != '/' && + image->filePath.find("://") == std::string::npos) { + image->filePath = ProjectPath::Absolute(image->filePath); + } + } + } + // Typeset text elements auto typesetter = pagx::Typesetter::Make(); typesetter->setFallbackTypefaces(GetFallbackTypefaces()); typesetter->typeset(doc.get()); - // Build layer tree with base path for image loading - pagx::LayerBuilder::Options options; - options.basePath = ProjectPath::Absolute(""); - auto content = pagx::LayerBuilder::Build(*doc, options); + // Build layer tree + auto content = pagx::LayerBuilder::Build(*doc); ASSERT_TRUE(content.root != nullptr); EXPECT_FLOAT_EQ(content.width, 800.0f); EXPECT_FLOAT_EQ(content.height, 520.0f); From 9a43deba0d773786a7b7c72f013f4d1cd1e80cab Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 14:08:39 +0800 Subject: [PATCH 236/678] Add PAGX spec and viewer publish scripts with multi-language support. --- .codebuddy/commands/cr.md | 8 +- .codebuddy/rules/Test.md | 4 +- .gitignore | 9 +- pagx/include/pagx/LayerBuilder.h | 6 + pagx/spec/package.json | 16 + pagx/spec/pagx_spec.md | 2644 +++++++++++++++++ .../pagx_spec.md => spec/pagx_spec.zh_CN.md} | 6 +- .../script/publish.js} | 574 ++-- pagx/viewer/.gitignore | 2 - pagx/viewer/package.json | 3 +- pagx/viewer/script/cmake.js | 15 - pagx/viewer/script/publish.cjs | 135 + pagx/viewer/server.js | 4 + 13 files changed, 3155 insertions(+), 271 deletions(-) create mode 100644 pagx/spec/package.json create mode 100644 pagx/spec/pagx_spec.md rename pagx/{docs/pagx_spec.md => spec/pagx_spec.zh_CN.md} (99%) rename pagx/{scripts/publish_spec.js => spec/script/publish.js} (51%) create mode 100644 pagx/viewer/script/publish.cjs diff --git a/.codebuddy/commands/cr.md b/.codebuddy/commands/cr.md index 99fddce2f1..201a946207 100644 --- a/.codebuddy/commands/cr.md +++ b/.codebuddy/commands/cr.md @@ -137,9 +137,11 @@ gh pr view {pr_number} --comments - 问题列表:按序号列出真正存在的问题,每个问题包含位置、描述、修复建议 - 若无问题则输出"无问题" -**!! IMPORTANT - 输出禁令**: -- 禁止输出任何已排除的问题,包括禁止以"排除"、"不是问题"、"二次验证后确认正确"等形式提及 -- 禁止输出分析推理过程,只输出最终结论 +**!! CRITICAL - 输出禁令**: +- 只输出最终确认存在的问题,其他一切不输出 +- 不输出分析过程、排除理由、"经确认"、"不是问题"等任何解释性文字 +- 不输出曾经考虑过但被排除的问题 +- 违反此规则视为审查失败 --- diff --git a/.codebuddy/rules/Test.md b/.codebuddy/rules/Test.md index e8f0a1db93..6011debf29 100644 --- a/.codebuddy/rules/Test.md +++ b/.codebuddy/rules/Test.md @@ -24,7 +24,9 @@ cmake --build cmake-build-debug --target PAGFullTest - 使用 `Baseline::Compare(pixels, key)` 比较截图,key 格式为 `{folder}/{name}`,例如 `PAGSurfaceTest/Mask` - 截图输出到 `test/out/{folder}/{name}.webp`,基准图为同目录下 `{name}_base.webp` -- 比较机制:对比 `test/baseline/version.json`(仓库)与 `test/baseline/.cache/version.json`(本地)中同一 key 的版本号,一致时才进行基准图对比,不一致则跳过返回成功,以此接受截图变更 +- 比较机制:对比 `test/baseline/version.json`(仓库)与 `test/baseline/.cache/version.json`(本地)中同一 key 的版本号 + - 两边 key 都存在且版本号不同:跳过比较并返回成功(用于接受截图变更) + - 其他情况:正常比较基准图,基准图不存在或不匹配则测试失败 **!! IMPORTANT - 截图基准变更限制**: - **NEVER** 自动接受截图基准变更,包括禁止自动运行 `UpdateBaseline` target、禁止修改或覆盖 `version.json` 文件 diff --git a/.gitignore b/.gitignore index 1473b44051..7705b1ca1b 100644 --- a/.gitignore +++ b/.gitignore @@ -91,5 +91,10 @@ pagx/viewer/.pagx.wasm-mt.md5 pagx/viewer/node_modules pagx/viewer/package-lock.json -# PAGX Spec output -pagx/.spec_output +# PAGX Site +pagx/public + +# PAGX Spec +pagx/spec/node_modules +pagx/spec/package-lock.json + diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index 6b5e86b232..ea620ea63a 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -20,7 +20,9 @@ #include #include +#include #include "pagx/PAGXDocument.h" +#include "tgfx/core/Typeface.h" #include "tgfx/layers/Layer.h" namespace pagx { @@ -49,6 +51,10 @@ struct PAGXContent { * Build options for LayerBuilder. */ struct LayerBuildOptions { + /** + * Fallback typefaces used when the primary font doesn't contain required glyphs. + */ + std::vector> fallbackTypefaces = {}; }; /** diff --git a/pagx/spec/package.json b/pagx/spec/package.json new file mode 100644 index 0000000000..718bbfa6c1 --- /dev/null +++ b/pagx/spec/package.json @@ -0,0 +1,16 @@ +{ + "name": "pagx-spec", + "version": "1.0", + "stableVersion": "", + "description": "PAGX specification publisher", + "private": true, + "scripts": { + "publish": "node script/publish.js" + }, + "dependencies": { + "highlight.js": "^11.9.0", + "marked": "^15.0.0", + "marked-gfm-heading-id": "^4.1.0", + "marked-highlight": "^2.2.0" + } +} diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md new file mode 100644 index 0000000000..210cc60b49 --- /dev/null +++ b/pagx/spec/pagx_spec.md @@ -0,0 +1,2644 @@ +# PAGX Format Specification + +## 1. Introduction + +**PAGX** (Portable Animated Graphics XML) is an XML-based markup language for describing animated vector graphics. It provides a unified and powerful representation of vector graphics and animations, designed to be the vector animation interchange standard across all major tools and runtimes. + +### 1.1 Design Goals + +- **Open and Readable**: Pure text XML format that is easy to read and edit, with native support for version control and diff comparison, facilitating debugging and AI comprehension and generation. + +- **Feature Complete**: Comprehensive coverage of vector graphics, images, rich text, filter effects, blend modes, masks, and more to meet the requirements of complex animation descriptions. + +- **Concise and Efficient**: Provides a simple yet powerful unified structure that balances optimized static descriptions while reserving extensibility for future interactivity and scripting. + +- **Ecosystem Compatible**: Serves as a universal interchange format for design tools such as After Effects, Figma, and Tencent Design, enabling seamless transfer of design assets. + +- **Efficient Deployment**: Design assets can be exported and deployed to development environments with one click, achieving high compression ratios and runtime performance when converted to binary PAG format. + +### 1.2 File Structure + +PAGX is a pure XML file (`.pagx`) that can reference external resource files (images, videos, audio, fonts, etc.) or embed resources via data URIs. PAGX and binary PAG formats are bidirectionally convertible: convert to PAG for optimized loading performance during publishing; use PAGX format for reading and editing during development, review, or editing. + +### 1.3 Document Organization + +This specification is organized in the following order: + +1. **Basic Data Types**: Defines the fundamental data formats used throughout the document +2. **Document Structure**: Describes the overall organization of a PAGX document +3. **Layer System**: Defines layers and their related features (styles, filters, masks) +4. **VectorElement System**: Defines vector elements within layers and their processing model + +**Appendices** (for quick reference): + +- **Appendix A**: Node hierarchy and containment relationships +- **Appendix B**: Common usage examples +- **Appendix C**: Node and attribute reference + +--- + +## 2. Basic Data Types + +This section defines the basic data types and naming conventions used in PAGX documents. + +### 2.1 Naming Conventions + +| Category | Convention | Examples | +|----------|------------|----------| +| Element names | PascalCase, no abbreviations | `Group`, `Rectangle`, `Fill` | +| Attribute names | camelCase, kept short | `antiAlias`, `blendMode`, `fontSize` | +| Default unit | Pixels (no notation required) | `width="100"` | +| Angle unit | Degrees | `rotation="45"` | + +### 2.2 Attribute Table Conventions + +This specification uses the "Default" column in attribute tables to describe whether an attribute is required: + +| Default Format | Meaning | +|----------------|---------| +| `(required)` | Attribute must be specified; no default value | +| Specific value (e.g., `0`, `true`, `normal`) | Attribute is optional; uses this default when not specified | +| `-` | Attribute is optional; has no effect when not specified | + +### 2.3 Common Attributes + +The following attributes are available on any element and are not repeated in individual node attribute tables: + +| Attribute | Type | Description | +|-----------|------|-------------| +| `id` | string | Unique identifier used for reference by other elements (e.g., masks, color sources). Must be unique within the document and cannot be empty or contain whitespace | +| `data-*` | string | Custom data attributes for storing application-specific private data. `*` can be replaced with any name (e.g., `data-name`, `data-layer-id`); ignored at runtime | + +**Custom Attribute Guidelines**: + +- Attribute names must begin with `data-` followed by at least one character +- Attribute names may only contain lowercase letters, numbers, and hyphens (`-`), and cannot end with a hyphen +- Attribute values are arbitrary strings interpreted by the creating application +- These attributes are not processed at runtime; used only for passing metadata between tools or storing debug information + +**Example**: + +```xml + + + + +``` + +### 2.4 Basic Value Types + +| Type | Description | Examples | +|------|-------------|----------| +| `float` | Floating-point number | `1.5`, `-0.5`, `100` | +| `int` | Integer | `400`, `0`, `-1` | +| `bool` | Boolean | `true`, `false` | +| `string` | String | `"Arial"`, `"myLayer"` | +| `enum` | Enumeration value | `normal`, `multiply` | +| `idref` | ID reference | `@gradientId`, `@maskLayer` | + +### 2.5 Point + +A point is represented by two comma-separated floating-point numbers: + +``` +"x,y" +``` + +**Examples**: `"100,200"`, `"0.5,0.5"`, `"-50,100"` + +### 2.6 Rect + +A rectangle is represented by four comma-separated floating-point numbers: + +``` +"x,y,width,height" +``` + +**Examples**: `"0,0,100,100"`, `"10,20,200,150"` + +### 2.7 Transform Matrix + +#### 2D Transform Matrix + +2D transforms use 6 comma-separated floating-point numbers corresponding to a standard 2D affine transformation matrix: + +``` +"a,b,c,d,tx,ty" +``` + +Matrix form: +``` +| a c tx | +| b d ty | +| 0 0 1 | +``` + +**Identity matrix**: `"1,0,0,1,0,0"` + +#### 3D Transform Matrix + +3D transforms use 16 comma-separated floating-point numbers in column-major order: + +``` +"m00,m10,m20,m30,m01,m11,m21,m31,m02,m12,m22,m32,m03,m13,m23,m33" +``` + +### 2.8 Color + +PAGX supports two color representation methods: + +#### HEX Format (Hexadecimal) + +HEX format represents colors in the sRGB color space using a `#` prefix with hexadecimal values: + +| Format | Example | Description | +|--------|---------|-------------| +| `#RGB` | `#F00` | 3-digit shorthand; each digit expands to two (equivalent to `#FF0000`) | +| `#RRGGBB` | `#FF0000` | 6-digit standard format, opaque | +| `#RRGGBBAA` | `#FF000080` | 8-digit with alpha at the end (consistent with CSS) | + +#### Floating-Point Format + +Floating-point format uses `colorspace(r, g, b)` or `colorspace(r, g, b, a)` to represent colors, supporting both sRGB and Display P3 color spaces: + +| Color Space | Format | Example | Description | +|-------------|--------|---------|-------------| +| sRGB | `srgb(r, g, b)` | `srgb(1.0, 0.5, 0.2)` | sRGB color space, components 0.0~1.0 | +| sRGB | `srgb(r, g, b, a)` | `srgb(1.0, 0.5, 0.2, 0.8)` | With alpha | +| Display P3 | `p3(r, g, b)` | `p3(1.0, 0.5, 0.2)` | Display P3 wide color gamut | +| Display P3 | `p3(r, g, b, a)` | `p3(1.0, 0.5, 0.2, 0.8)` | With alpha | + +**Notes**: +- Color space identifiers (`srgb` or `p3`) and parentheses **cannot be omitted** +- Wide color gamut (Display P3) component values may exceed the [0, 1] range to represent colors outside the sRGB gamut +- sRGB floating-point format and HEX format represent the same color space; choose as needed + +#### Color Source Reference + +Use `@resourceId` to reference color sources (gradients, patterns, etc.) defined in Resources. + +### 2.9 Path Data Syntax + +Path data uses SVG path syntax, consisting of a series of commands and coordinates. + +**Path Commands**: + +| Command | Parameters | Description | +|---------|------------|-------------| +| M/m | x y | Move to (absolute/relative) | +| L/l | x y | Line to | +| H/h | x | Horizontal line to | +| V/v | y | Vertical line to | +| C/c | x1 y1 x2 y2 x y | Cubic Bézier curve | +| S/s | x2 y2 x y | Smooth cubic Bézier | +| Q/q | x1 y1 x y | Quadratic Bézier curve | +| T/t | x y | Smooth quadratic Bézier | +| A/a | rx ry rotation large-arc sweep x y | Elliptical arc | +| Z/z | - | Close path | + +### 2.10 External Resource Reference + +External resources are referenced via relative paths or data URIs, applicable to images, videos, audio, fonts, and other files. + +```xml + + + + + + +``` + +**Path Resolution Rules**: + +- **Relative paths**: Resolved relative to the directory containing the PAGX file; supports `../` to reference parent directories +- **Data URIs**: Begin with `data:`, format is `data:;base64,`; only base64 encoding is supported +- Path separators must use `/` (forward slash); `\` (backslash) is not supported + +--- + +## 3. Document Structure + +This section defines the overall structure of a PAGX document. + +### 3.1 Coordinate System + +PAGX uses a standard 2D Cartesian coordinate system: + +- **Origin**: Located at the top-left corner of the canvas +- **X-axis**: Positive direction points right +- **Y-axis**: Positive direction points down +- **Angles**: Clockwise direction is positive (0° points toward the positive X-axis) +- **Units**: All length values default to pixels; angle values default to degrees + +### 3.2 Root Element (pagx) + +`` is the root element of a PAGX document, defining the canvas dimensions and directly containing the layer list. + +```xml + + + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `version` | string | (required) | Format version | +| `width` | float | (required) | Canvas width | +| `height` | float | (required) | Canvas height | + +**Layer Rendering Order**: Layers are rendered sequentially in document order; layers appearing earlier in the document are rendered first (appearing below), while layers appearing later are rendered last (appearing above). + +### 3.3 Resources + +`` defines reusable resources including images, path data, color sources, and compositions. Resources are identified by the `id` attribute and referenced elsewhere in the document using the `@id` syntax. + +**Element Position**: The Resources element may be placed anywhere within the root element; there are no restrictions on its position. Parsers must support forward references—elements that reference resources or layers defined later in the document. + +```xml + + + + + + + + + + + +``` + +#### 3.3.1 Image + +Image resources define bitmap data that can be referenced throughout the document. + +```xml + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `source` | string | (required) | File path or data URI | + +**Supported Formats**: PNG, JPEG, WebP, GIF + +#### 3.3.2 PathData + +PathData defines reusable path data for reference by Path elements and TextPath modifiers. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `data` | string | (required) | SVG path data | + +#### 3.3.3 Color Sources + +Color sources define colors that can be used for fills and strokes, supporting two usage patterns: + +1. **Shared Definition**: Pre-defined in `` and referenced via `@id`. Suitable for color sources **referenced in multiple places**. +2. **Inline Definition**: Nested directly within `` or `` elements. Suitable for color sources **used only once**, providing a more concise syntax. + +##### SolidColor + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `color` | color | (required) | Color value | + +##### LinearGradient + +Linear gradients interpolate along the direction from start point to end point. + +```xml + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `startPoint` | point | (required) | Start point | +| `endPoint` | point | (required) | End point | +| `matrix` | string | identity matrix | Transform matrix | + +**Calculation**: For a point P, its color is determined by the projection position of P onto the line connecting start and end points. + +##### RadialGradient + +Radial gradients radiate outward from the center. + +```xml + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `center` | point | 0,0 | Center point | +| `radius` | float | (required) | Gradient radius | +| `matrix` | string | identity matrix | Transform matrix | + +**Calculation**: For a point P, its color is determined by `distance(P, center) / radius`. + +##### ConicGradient + +Conic gradients (also known as sweep gradients) interpolate along the circumference. + +```xml + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `center` | point | 0,0 | Center point | +| `startAngle` | float | 0 | Start angle | +| `endAngle` | float | 360 | End angle | +| `matrix` | string | identity matrix | Transform matrix | + +**Calculation**: For a point P, its color is determined by the ratio of `atan2(P.y - center.y, P.x - center.x)` within the `[startAngle, endAngle]` range. + +##### DiamondGradient + +Diamond gradients radiate from the center toward the four corners. + +```xml + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `center` | point | 0,0 | Center point | +| `radius` | float | (required) | Gradient radius | +| `matrix` | string | identity matrix | Transform matrix | + +**Calculation**: For a point P, its color is determined by the Manhattan distance `(|P.x - center.x| + |P.y - center.y|) / radius`. + +##### ColorStop + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `offset` | float | (required) | Position 0.0~1.0 | +| `color` | color | (required) | Stop color | + +**Common Gradient Rules**: + +- **Stop Interpolation**: Linear interpolation between adjacent color stops +- **Stop Boundaries**: + - Stops with `offset < 0` are treated as `offset = 0` + - Stops with `offset > 1` are treated as `offset = 1` + - If there is no stop at `offset = 0`, the first stop's color is used to fill + - If there is no stop at `offset = 1`, the last stop's color is used to fill + +##### ImagePattern + +Image patterns use an image as a color source. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `image` | idref | (required) | Image reference "@id" | +| `tileModeX` | TileMode | clamp | X-direction tile mode | +| `tileModeY` | TileMode | clamp | Y-direction tile mode | +| `minFilterMode` | FilterMode | linear | Texture filter mode for minification | +| `magFilterMode` | FilterMode | linear | Texture filter mode for magnification | +| `mipmapMode` | MipmapMode | linear | Mipmap mode | +| `matrix` | string | identity matrix | Transform matrix | + +**TileMode**: `clamp`, `repeat`, `mirror`, `decal` + +**FilterMode**: `nearest`, `linear` + +**MipmapMode**: `none`, `nearest`, `linear` + +##### Color Source Coordinate System + +Except for solid colors, all color sources (gradients, image patterns) have the concept of a coordinate system, which is **relative to the origin of the geometry element's local coordinate system**. The `matrix` attribute can be used to apply transforms to the color source coordinate system. + +**Transform Behavior**: + +1. **External transforms affect both geometry and color source**: Group transforms, layer matrices, and other external transforms apply holistically to both the geometry element and its color source—they scale, rotate, and translate together. + +2. **Modifying geometry properties does not affect the color source**: Directly modifying geometry element properties (such as Rectangle's width/height or Path's path data) only changes the geometry content itself without affecting the color source coordinate system. + +**Example**: Drawing a linear gradient from left to right within a 100×100 region: + +```xml + + + + + + + + + + + +``` + +- Applying `scale(2, 2)` transform to this layer: The rectangle becomes 200×200, and the gradient scales accordingly, maintaining consistent visual appearance +- Directly changing Rectangle's size to 200,200: The rectangle becomes 200×200, but the gradient coordinates remain unchanged, covering only the left half of the rectangle + +#### 3.3.4 Composition + +Compositions are used for content reuse (similar to After Effects pre-comps). + +```xml + + + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `width` | float | (required) | Composition width | +| `height` | float | (required) | Composition height | + +#### 3.3.5 Font + +Font defines embedded font resources containing subsetted glyph data (vector outlines or bitmaps). PAGX files achieve complete self-containment through embedded glyph data, ensuring cross-platform rendering consistency. + +```xml + + + + + + + + + + + +``` + +**Consistency Constraint**: All Glyphs within the same Font must use the same type (all `path` or all `image`); mixing is not allowed. + +**GlyphID Rules**: +- **GlyphID starts from 1**: Glyph list index + 1 = GlyphID +- **GlyphID 0 is reserved**: Represents a missing glyph; not rendered + +##### Glyph + +Glyph defines rendering data for a single glyph. Either `path` or `image` must be specified (but not both). + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `path` | string | - | SVG path data (vector outline) | +| `image` | string | - | Image data (base64 data URI) or external file path | +| `offset` | point | 0,0 | Bitmap offset (only used with `image`) | + +**Glyph Types**: +- **Vector glyph**: Specifies the `path` attribute using SVG path syntax to describe the outline +- **Bitmap glyph**: Specifies the `image` attribute for colored glyphs like emoji; position can be adjusted with `offset` + +**Path Coordinate System**: Glyph paths use final rendering coordinates with font size scaling already applied. The same character at different font sizes should be stored as separate Glyphs, as fonts may have different glyph designs at different sizes. + +### 3.4 Document Hierarchy + +PAGX documents organize content in a hierarchical structure: + +``` + ← Root element (defines canvas dimensions) +├── ← Layer (multiple allowed) +│ ├── Geometry elements ← Rectangle, Ellipse, Path, Text, etc. +│ ├── Modifiers ← TrimPath, RoundCorner, TextModifier, etc. +│ ├── Painters ← Fill, Stroke +│ ├── ← VectorElement container (nestable) +│ ├── LayerStyle ← DropShadowStyle, InnerShadowStyle, etc. +│ ├── LayerFilter ← BlurFilter, ColorMatrixFilter, etc. +│ └── ← Child layers (recursive structure) +│ └── ... +│ +└── ← Resources section (optional, defines reusable resources) + ├── ← Image resource + ├── ← Path data resource + ├── ← Solid color definition + ├── ← Gradient definition + ├── ← Image pattern definition + ├── ← Font resource (embedded font) + │ └── ← Glyph definition + └── ← Composition definition + └── ← Layers within composition +``` + +--- + +## 4. Layer System + +Layers are the fundamental organizational units for PAGX content, providing rich visual effect control capabilities. + +### 4.1 Core Concepts + +This section introduces the core concepts of the layer system. These concepts form the foundation for understanding layer styles, filters, and masks. + +#### Layer Rendering Pipeline + +Painters (Fill, Stroke, etc.) bound to a layer are divided into background content and foreground content via the `placement` attribute, defaulting to background content. A single layer renders in the following order: + +1. **Layer Styles (below)**: Render layer styles positioned below content (e.g., drop shadows) +2. **Background Content**: Render Fill and Stroke with `placement="background"` +3. **Child Layers**: Recursively render all child layers in document order +4. **Layer Styles (above)**: Render layer styles positioned above content (e.g., inner shadows) +5. **Foreground Content**: Render Fill and Stroke with `placement="foreground"` +6. **Layer Filters**: Use the combined output of previous steps as input to the filter chain, applying all filters sequentially + +#### Layer Content + +**Layer content** refers to the complete rendering result of the layer's background content, child layers, and foreground content. Layer styles compute their effects based on layer content. For example, when fill is background and stroke is foreground, the stroke renders above child layers, but drop shadows are still calculated based on the complete layer content including fill, child layers, and stroke. + +#### Layer Contour + +**Layer contour** is used for masking and certain layer styles. Compared to normal layer content, layer contour has these differences: + +1. **Includes geometry drawn with alpha=0**: Geometric shapes filled with completely transparent fills are included in the contour +2. **Solid fills and gradient fills**: Original fills are ignored and replaced with opaque white drawing +3. **Image fills**: Original pixels are preserved, but semi-transparent pixels are converted to fully opaque (fully transparent pixels are preserved) + +Note: Geometry elements must have painters to participate in the contour; standalone geometry elements (Rectangle, Ellipse, etc.) without corresponding Fill or Stroke do not participate in contour calculation. + +Layer contour is primarily used for: + +- **Layer Styles**: Some layer styles require contour as one of their input sources +- **Masking**: `maskType="contour"` uses the mask layer's contour for clipping + +#### Layer Background + +**Layer background** refers to the composited result of all rendered content below the current layer, including: +- Rendering results of all sibling layers below the current layer and their subtrees +- Layer styles already drawn below the current layer (excluding BackgroundBlurStyle itself) + +Layer background is primarily used for: + +- **Layer Styles**: Some layer styles require background as one of their input sources +- **Blend Modes**: Some blend modes require background information for correct rendering + +**Background Pass-through**: The `passThroughBackground` attribute controls whether layer background passes through to child layers. When set to `false`, child layers' background-dependent styles cannot access the correct layer background. The following conditions automatically disable background pass-through: +- Layer uses a non-`normal` blend mode +- Layer has filters applied +- Layer uses 3D transforms or projection transforms + +### 4.2 Layer + +`` is the basic container for content and child layers. + +```xml + + + + + + + + + + + +``` + +#### Child Elements + +Layer child elements are automatically categorized into four collections by type: + +| Child Element Type | Category | Description | +|-------------------|----------|-------------| +| VectorElement | contents | Geometry elements, modifiers, painters (participate in accumulation processing) | +| LayerStyle | styles | DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle | +| LayerFilter | filters | BlurFilter, DropShadowFilter, and other filters | +| Layer | children | Nested child layers | + +**Recommended Order**: Although child element order does not affect parsing results, it is recommended to write them in the order: VectorElement → LayerStyle → LayerFilter → child Layer, for improved readability. + +#### Layer Attributes + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | string | "" | Display name | +| `visible` | bool | true | Whether visible | +| `alpha` | float | 1 | Opacity 0~1 | +| `blendMode` | BlendMode | normal | Blend mode | +| `x` | float | 0 | X position | +| `y` | float | 0 | Y position | +| `matrix` | string | identity matrix | 2D transform "a,b,c,d,tx,ty" | +| `matrix3D` | string | - | 3D transform (16 values, column-major) | +| `preserve3D` | bool | false | Preserve 3D transform | +| `antiAlias` | bool | true | Edge anti-aliasing | +| `groupOpacity` | bool | false | Group opacity | +| `passThroughBackground` | bool | true | Whether to pass background through to child layers | +| `excludeChildEffectsInLayerStyle` | bool | false | Whether layer styles exclude child layer effects | +| `scrollRect` | string | - | Scroll clipping region "x,y,w,h" | +| `mask` | idref | - | Mask layer reference "@id" | +| `maskType` | MaskType | alpha | Mask type | +| `composition` | idref | - | Composition reference "@id" | + +**Transform Attribute Priority**: `x`/`y`, `matrix`, and `matrix3D` have an override relationship: +- Only `x`/`y` set: Uses `x`/`y` for translation +- `matrix` set: `matrix` overrides `x`/`y` values +- `matrix3D` set: `matrix3D` overrides both `matrix` and `x`/`y` values + +**MaskType**: + +| Value | Description | +|-------|-------------| +| `alpha` | Alpha mask: Uses mask's alpha channel | +| `luminance` | Luminance mask: Uses mask's luminance values | +| `contour` | Contour mask: Uses mask's contour for clipping | + +**BlendMode**: + +Blend modes define how source color (S) combines with destination color (D). + +| Value | Formula | Description | +|-------|---------|-------------| +| `normal` | S | Normal (overwrite) | +| `multiply` | S × D | Multiply | +| `screen` | 1 - (1-S)(1-D) | Screen | +| `overlay` | multiply/screen combination | Overlay | +| `darken` | min(S, D) | Darken | +| `lighten` | max(S, D) | Lighten | +| `colorDodge` | D / (1-S) | Color Dodge | +| `colorBurn` | 1 - (1-D)/S | Color Burn | +| `hardLight` | multiply/screen reversed combination | Hard Light | +| `softLight` | Soft version of overlay | Soft Light | +| `difference` | \|S - D\| | Difference | +| `exclusion` | S + D - 2SD | Exclusion | +| `hue` | D's saturation and luminosity + S's hue | Hue | +| `saturation` | D's hue and luminosity + S's saturation | Saturation | +| `color` | D's luminosity + S's hue and saturation | Color | +| `luminosity` | S's luminosity + D's hue and saturation | Luminosity | +| `plusLighter` | S + D | Plus Lighter (toward white) | +| `plusDarker` | S + D - 1 | Plus Darker (toward black) | + +### 4.3 Layer Styles + +Layer styles add visual effects above or below layer content without replacing the original content. + +**Input Sources for Layer Styles**: + +All layer styles compute effects based on **layer content**. In layer styles, layer content is automatically converted to **Opaque mode**: geometric shapes are rendered with normal fills, then all semi-transparent pixels are converted to fully opaque (fully transparent pixels are preserved). This means shadows produced by semi-transparent fills appear the same as those from fully opaque fills. + +Some layer styles additionally use **layer contour** or **layer background** as input (see individual style descriptions). Definitions of layer contour and layer background are in Section 4.1. + +```xml + + + + + + + +``` + +**Common LayerStyle Attributes**: + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `blendMode` | BlendMode | normal | Blend mode (see Section 4.1) | + +#### 4.3.1 DropShadowStyle + +Draws a drop shadow **below** the layer. Computes shadow shape based on opaque layer content. When `showBehindLayer="false"`, additionally uses **layer contour** as an erase mask to cut out the portion occluded by the layer. + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `offsetX` | float | 0 | X offset | +| `offsetY` | float | 0 | Y offset | +| `blurX` | float | 0 | X blur radius | +| `blurY` | float | 0 | Y blur radius | +| `color` | color | #000000 | Shadow color | +| `showBehindLayer` | bool | true | Whether shadow shows behind layer | + +**Rendering Steps**: +1. Get opaque layer content and offset by `(offsetX, offsetY)` +2. Apply Gaussian blur `(blurX, blurY)` to the offset content +3. Fill the shadow region with `color` +4. If `showBehindLayer="false"`, use layer contour as erase mask to cut out occluded portion + +**showBehindLayer**: +- `true`: Shadow displays completely, including portion occluded by layer content +- `false`: Portion of shadow occluded by layer content is cut out (using layer contour as erase mask) + +#### 4.3.2 BackgroundBlurStyle + +Applies blur effect to layer background **below** the layer. Computes effect based on **layer background**, using opaque layer content as mask for clipping. + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `blurX` | float | 0 | X blur radius | +| `blurY` | float | 0 | Y blur radius | +| `tileMode` | TileMode | mirror | Tile mode | + +**Rendering Steps**: +1. Get layer background below layer bounds +2. Apply Gaussian blur `(blurX, blurY)` to layer background +3. Clip blurred result using opaque layer content as mask + +#### 4.3.3 InnerShadowStyle + +Draws an inner shadow **above** the layer, appearing inside the layer content. Computes shadow range based on opaque layer content. + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `offsetX` | float | 0 | X offset | +| `offsetY` | float | 0 | Y offset | +| `blurX` | float | 0 | X blur radius | +| `blurY` | float | 0 | Y blur radius | +| `color` | color | #000000 | Shadow color | + +**Rendering Steps**: +1. Get opaque layer content and offset by `(offsetX, offsetY)` +2. Apply Gaussian blur `(blurX, blurY)` to the inverse of the offset content (exterior region) +3. Fill the shadow region with `color` +4. Intersect with opaque layer content, keeping only shadow inside content + +### 4.4 Layer Filters + +Layer filters are the final stage of layer rendering. All previously rendered results (including layer styles) accumulated in order serve as filter input. Filters are applied in chain fashion according to document order, with each filter's output becoming the next filter's input. + +Key difference from layer styles (Section 4.3): Layer styles **independently render** visual effects above or below layer content, while filters **modify** the layer's overall rendering output. Layer styles are applied before filters. + +```xml + + + + + + + +``` + +#### 4.4.1 BlurFilter + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `blurX` | float | (required) | X blur radius | +| `blurY` | float | (required) | Y blur radius | +| `tileMode` | TileMode | decal | Tile mode | + +#### 4.4.2 DropShadowFilter + +Generates shadow effect based on filter input. Core difference from DropShadowStyle: The filter projects based on original rendering content and supports semi-transparency; the style projects based on opaque layer content. Additionally, the two support different attribute features. + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `offsetX` | float | 0 | X offset | +| `offsetY` | float | 0 | Y offset | +| `blurX` | float | 0 | X blur radius | +| `blurY` | float | 0 | Y blur radius | +| `color` | color | #000000 | Shadow color | +| `shadowOnly` | bool | false | Show shadow only | + +**Rendering Steps**: +1. Offset filter input by `(offsetX, offsetY)` +2. Extract alpha channel and apply Gaussian blur `(blurX, blurY)` +3. Fill shadow region with `color` +4. Composite shadow with filter input (`shadowOnly=false`) or output shadow only (`shadowOnly=true`) + +#### 4.4.3 InnerShadowFilter + +Draws shadow inside the filter input. + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `offsetX` | float | 0 | X offset | +| `offsetY` | float | 0 | Y offset | +| `blurX` | float | 0 | X blur radius | +| `blurY` | float | 0 | Y blur radius | +| `color` | color | #000000 | Shadow color | +| `shadowOnly` | bool | false | Show shadow only | + +**Rendering Steps**: +1. Create inverse mask of filter input's alpha channel +2. Offset and apply Gaussian blur +3. Intersect with filter input's alpha channel +4. Composite result with filter input (`shadowOnly=false`) or output shadow only (`shadowOnly=true`) + +#### 4.4.4 BlendFilter + +Overlays a specified color onto the layer using a specified blend mode. + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `color` | color | (required) | Blend color | +| `blendMode` | BlendMode | normal | Blend mode (see Section 4.1) | + +#### 4.4.5 ColorMatrixFilter + +Transforms colors using a 4×5 color matrix. + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `matrix` | string | (required) | 4x5 color matrix (20 comma-separated floats) | + +**Matrix Format** (20 values, row-major): +``` +| R' | | m[0] m[1] m[2] m[3] m[4] | | R | +| G' | | m[5] m[6] m[7] m[8] m[9] | | G | +| B' | = | m[10] m[11] m[12] m[13] m[14] | × | B | +| A' | | m[15] m[16] m[17] m[18] m[19] | | A | + | 1 | +``` + +### 4.5 Clipping and Masking + +#### 4.5.1 scrollRect + +The `scrollRect` attribute defines the layer's visible region; content outside this region is clipped. + +```xml + + + + +``` + +#### 4.5.2 Masking + +Reference another layer as a mask using the `mask` attribute. + +```xml + + + + + + + + +``` + +**Masking Rules**: +- The mask layer itself is not rendered (the `visible` attribute is ignored) +- The mask layer's transforms do not affect the masked layer + +--- + +## 5. VectorElement System + +The VectorElement system defines how vector content within Layers is processed and rendered. + +### 5.1 Processing Model + +The VectorElement system employs an **accumulate-render** processing model: geometry elements accumulate in a rendering context, modifiers transform the accumulated geometry, and painters trigger final rendering. + +#### 5.1.1 Terminology + +| Term | Elements | Description | +|------|----------|-------------| +| **Geometry Elements** | Rectangle, Ellipse, Polystar, Path, Text | Elements providing geometric shapes; accumulate as a geometry list in the context | +| **Modifiers** | TrimPath, RoundCorner, MergePath, TextModifier, TextPath, TextLayout, Repeater | Transform accumulated geometry | +| **Painters** | Fill, Stroke | Perform fill or stroke rendering on accumulated geometry | +| **Containers** | Group | Create isolated scopes and apply matrix transforms; merge upon completion | + +#### 5.1.2 Internal Structure of Geometry Elements + +Geometry elements have different internal structures when accumulating in the context: + +| Element Type | Internal Structure | Description | +|--------------|-------------------|-------------| +| Shape elements (Rectangle, Ellipse, Polystar, Path) | Single Path | Each shape element produces one path | +| Text element (Text) | Glyph list | A Text produces multiple glyphs after shaping | + +#### 5.1.3 Processing and Rendering Order + +VectorElements are processed sequentially in **document order**; elements appearing earlier are processed first. By default, painters processed earlier are rendered first (appearing below). + +Since Fill and Stroke can specify rendering to layer background or foreground via the `placement` attribute, the final rendering order may not exactly match document order. However, in the default case (all content as background), rendering order matches document order. + +#### 5.1.4 Unified Processing Flow + +``` +Geometry Elements Modifiers Painters +┌──────────┐ ┌──────────┐ ┌──────────┐ +│Rectangle │ │ TrimPath │ │ Fill │ +│ Ellipse │ │RoundCorn │ │ Stroke │ +│ Polystar │ │MergePath │ └────┬─────┘ +│ Path │ │TextModif │ │ +│ Text │ │ TextPath │ │ +└────┬─────┘ │TextLayout│ │ + │ │ Repeater │ │ + │ └────┬─────┘ │ + │ │ │ + │ Accumulate │ Transform │ Render + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────┐ +│ Geometry List [Path1, Path2, GlyphList1, GlyphList2...] │ +└─────────────────────────────────────────────────────────┘ +``` + +**Rendering context** accumulates a geometry list where: +- Each shape element contributes one Path +- Each Text contributes a glyph list (containing multiple glyphs) + +#### 5.1.5 Modifier Scope + +Different modifiers have different scopes over elements in the geometry list: + +| Modifier Type | Target | Description | +|---------------|--------|-------------| +| Shape modifiers (TrimPath, RoundCorner, MergePath) | Paths only | Trigger forced conversion for text | +| Text modifiers (TextModifier, TextPath, TextLayout) | Glyph lists only | No effect on Paths | +| Repeater | Paths + glyph lists | Affects all geometry simultaneously | + +### 5.2 Geometry Elements + +Geometry elements provide renderable shapes. + +#### 5.2.1 Rectangle + +Rectangles are defined from center point with uniform corner rounding support. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `center` | point | 0,0 | Center point | +| `size` | size | 100,100 | Dimensions "width,height" | +| `roundness` | float | 0 | Corner radius | +| `reversed` | bool | false | Reverse path direction | + +**Calculation Rules**: +``` +rect.left = center.x - size.width / 2 +rect.top = center.y - size.height / 2 +rect.right = center.x + size.width / 2 +rect.bottom = center.y + size.height / 2 +``` + +**Corner Rounding**: +- `roundness` value is automatically limited to `min(roundness, size.width/2, size.height/2)` +- When `roundness >= min(size.width, size.height) / 2`, the shorter dimension becomes semicircular + +**Path Start Point**: Rectangle path starts from the **top-right corner**, drawn clockwise (`reversed="false"`). + +#### 5.2.2 Ellipse + +Ellipses are defined from center point. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `center` | point | 0,0 | Center point | +| `size` | size | 100,100 | Dimensions "width,height" | +| `reversed` | bool | false | Reverse path direction | + +**Calculation Rules**: +``` +boundingRect.left = center.x - size.width / 2 +boundingRect.top = center.y - size.height / 2 +boundingRect.right = center.x + size.width / 2 +boundingRect.bottom = center.y + size.height / 2 +``` + +**Path Start Point**: Ellipse path starts from the **right midpoint** (3 o'clock position). + +#### 5.2.3 Polystar + +Supports both regular polygon and star modes. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `center` | point | 0,0 | Center point | +| `type` | PolystarType | star | Type (see below) | +| `pointCount` | float | 5 | Number of points (supports decimals) | +| `outerRadius` | float | 100 | Outer radius | +| `innerRadius` | float | 50 | Inner radius (star only) | +| `rotation` | float | 0 | Rotation angle | +| `outerRoundness` | float | 0 | Outer corner roundness | +| `innerRoundness` | float | 0 | Inner corner roundness | +| `reversed` | bool | false | Reverse path direction | + +**PolystarType**: + +| Value | Description | +|-------|-------------| +| `polygon` | Regular polygon: Uses outer radius only | +| `star` | Star: Alternates between outer and inner radii | + +**Polygon Mode** (`type="polygon"`): +- Uses only `outerRadius` and `outerRoundness` +- `innerRadius` and `innerRoundness` are ignored + +**Star Mode** (`type="star"`): +- Outer vertices at `outerRadius` +- Inner vertices at `innerRadius` +- Vertices alternate to form star shape + +**Vertex Calculation** (i-th outer vertex): +``` +angle = rotation + (i / pointCount) * 360° +x = center.x + outerRadius * cos(angle) +y = center.y + outerRadius * sin(angle) +``` + +**Fractional Point Count**: +- `pointCount` supports decimal values (e.g., `5.5`) +- The fractional part represents the "completion degree" of the last vertex, producing an incomplete final corner +- `pointCount <= 0` generates no path + +**Roundness**: +- `outerRoundness` and `innerRoundness` range from 0~1 +- 0 means sharp corners; 1 means fully rounded +- Roundness is achieved by adding Bézier control points at vertices + +#### 5.2.4 Path + +Defines arbitrary shapes using SVG path syntax, supporting inline data or references to PathData defined in Resources. + +```xml + + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `data` | string/idref | (required) | SVG path data or PathData resource reference "@id" | +| `reversed` | bool | false | Reverse path direction | + +#### 5.2.5 Text + +Text elements provide geometric shapes for text content. Unlike shape elements that produce a single Path, Text produces a **glyph list** (multiple glyphs) after shaping, which accumulates in the rendering context's geometry list for subsequent modifier transformation or painter rendering. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `text` | string | "" | Text content | +| `position` | point | 0,0 | Text start position, y is baseline (may be overridden by TextLayout) | +| `fontFamily` | string | system default | Font family | +| `fontStyle` | string | "Regular" | Font variant (Regular, Bold, Italic, Bold Italic, etc.) | +| `fontSize` | float | 12 | Font size | +| `letterSpacing` | float | 0 | Letter spacing | +| `baselineShift` | float | 0 | Baseline shift (positive shifts up, negative shifts down) | + +Child elements: `CDATA` text, `GlyphRun`* + +**Text Content**: Typically use the `text` attribute to specify text content. When text contains XML special characters (`<`, `>`, `&`, etc.) or needs to preserve multi-line formatting, use a CDATA child node instead of the `text` attribute. Text does not allow direct plain text child nodes; CDATA wrapping is required. + +```xml + + + + + D]]> + + + + + +``` + +**Rendering Modes**: Text supports **pre-layout** and **runtime layout** modes. Pre-layout provides pre-computed glyphs and positions via GlyphRun child nodes, rendering with embedded fonts for cross-platform consistency. Runtime layout performs shaping and layout at runtime; due to platform differences in fonts and layout features, minor inconsistencies may occur. For exact reproduction of design tool layouts, pre-layout is recommended. + +**Runtime Layout Rendering Flow**: +1. Find system font based on `fontFamily` and `fontStyle`; if unavailable, select fallback font according to runtime-configured fallback list +2. Shape using `text` attribute (or CDATA child node); newlines trigger line breaks (default 1.2× font size line height, customizable via TextLayout) +3. Apply typography parameters: `fontSize`, `letterSpacing`, `baselineShift` +4. Construct glyph list and accumulate to rendering context + +##### GlyphRun (Pre-layout Data) + +GlyphRun defines pre-layout data for a group of glyphs, each GlyphRun independently referencing one font resource. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `font` | idref | Font resource reference `@id` | +| `glyphs` | string | GlyphID sequence, comma-separated (0 means missing glyph) | +| `y` | float | Shared y coordinate (Horizontal mode only), default 0 | +| `xPositions` | string | x coordinate sequence, comma-separated (Horizontal mode) | +| `positions` | string | (x,y) coordinate sequence, semicolon-separated (Point mode) | +| `xforms` | string | RSXform sequence (scos,ssin,tx,ty), semicolon-separated (RSXform mode) | +| `matrices` | string | Matrix sequence (a,b,c,d,tx,ty), semicolon-separated (Matrix mode) | + +**Positioning Mode Selection** (priority from high to low): +1. Has `matrices` → Matrix mode: Each glyph has full 2D affine transform +2. Has `xforms` → RSXform mode: Each glyph has rotation+scale+translation (path text) +3. Has `positions` → Point mode: Each glyph has independent (x,y) position (multi-line/complex layout) +4. Has `xPositions` → Horizontal mode: Each glyph has x coordinate, sharing `y` value (single-line horizontal text) +5. Only `glyphs` → Not supported; position data must be provided + +**RSXform**: +RSXform is a compressed rotation+scale matrix with four components (scos, ssin, tx, ty): +``` +| scos -ssin tx | +| ssin scos ty | +| 0 0 1 | +``` +Where scos = scale × cos(angle), ssin = scale × sin(angle). + +**Matrix**: +Matrix is a full 2D affine transformation matrix with six components (a, b, c, d, tx, ty): +``` +| a c tx | +| b d ty | +| 0 0 1 | +``` + +**Pre-layout Examples**: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 5.3 Painters + +Painters (Fill, Stroke) render all geometry (Paths and glyph lists) accumulated at the **current moment**. + +#### 5.3.1 Fill + +Fill draws the interior region of geometry using a specified color source. + +```xml + + + + + + + + + + + + + + + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `color` | color/idref | #000000 | Color value or color source reference, default black | +| `alpha` | float | 1 | Opacity 0~1 | +| `blendMode` | BlendMode | normal | Blend mode (see Section 4.1) | +| `fillRule` | FillRule | winding | Fill rule (see below) | +| `placement` | LayerPlacement | background | Rendering position (see Section 5.3.3) | + +Child elements: May embed one color source (SolidColor, LinearGradient, RadialGradient, ConicGradient, DiamondGradient, ImagePattern) + +**FillRule**: + +| Value | Description | +|-------|-------------| +| `winding` | Non-zero winding rule: Counts based on path direction; fills if non-zero | +| `evenOdd` | Even-odd rule: Counts based on crossing count; fills if odd | + +**Text Fill**: +- Text is filled per glyph +- Supports color override for individual glyphs via TextModifier +- Color override uses alpha blending: `finalColor = lerp(originalColor, overrideColor, overrideAlpha)` + +#### 5.3.2 Stroke + +Stroke draws lines along geometry boundaries. + +```xml + + + + + + + + + + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `color` | color/idref | #000000 | Color value or color source reference, default black | +| `width` | float | 1 | Stroke width | +| `alpha` | float | 1 | Opacity 0~1 | +| `blendMode` | BlendMode | normal | Blend mode (see Section 4.1) | +| `cap` | LineCap | butt | Line cap style (see below) | +| `join` | LineJoin | miter | Line join style (see below) | +| `miterLimit` | float | 4 | Miter limit | +| `dashes` | string | - | Dash pattern "d1,d2,..." | +| `dashOffset` | float | 0 | Dash offset | +| `align` | StrokeAlign | center | Stroke alignment (see below) | +| `placement` | LayerPlacement | background | Rendering position (see Section 5.3.3) | + +**LineCap**: + +| Value | Description | +|-------|-------------| +| `butt` | Butt cap: Line does not extend beyond endpoints | +| `round` | Round cap: Semicircular extension at endpoints | +| `square` | Square cap: Rectangular extension at endpoints | + +**LineJoin**: + +| Value | Description | +|-------|-------------| +| `miter` | Miter join: Extends outer edges to form sharp corner | +| `round` | Round join: Connected with circular arc | +| `bevel` | Bevel join: Fills connection with triangle | + +**StrokeAlign**: + +| Value | Description | +|-------|-------------| +| `center` | Stroke centered on path (default) | +| `inside` | Stroke inside closed path | +| `outside` | Stroke outside closed path | + +Inside/outside stroke is achieved by: +1. Stroking at double width +2. Boolean operation with original shape (intersection for inside, difference for outside) + +**Dash Pattern**: +- `dashes`: Defines dash segment length sequence, e.g., `"5,3"` means 5px solid + 3px gap +- `dashOffset`: Dash start offset + +#### 5.3.3 LayerPlacement + +The `placement` attribute of Fill and Stroke controls rendering order relative to child layers: + +| Value | Description | +|-------|-------------| +| `background` | Render **below** child layers (default) | +| `foreground` | Render **above** child layers | + +### 5.4 Shape Modifiers + +Shape modifiers perform **in-place transformation** on accumulated Paths; for glyph lists, they trigger forced conversion to Paths. + +#### 5.4.1 TrimPath + +Trims paths to a specified start/end range. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `start` | float | 0 | Start position 0~1 | +| `end` | float | 1 | End position 0~1 | +| `offset` | float | 0 | Offset (degrees); 360 degrees represents one cycle of the full path length | +| `type` | TrimType | separate | Trim type (see below) | + +**TrimType**: + +| Value | Description | +|-------|-------------| +| `separate` | Separate mode: Each shape trimmed independently with same start/end parameters | +| `continuous` | Continuous mode: All shapes treated as one continuous path, trimmed by total length ratio | + +**Edge Cases**: +- `start > end`: Reverse trim; path direction reversed +- Supports wrapping: When trim range exceeds [0,1], automatically wraps to other end of path +- When total path length is 0, no operation is performed + +**Continuous Mode Example**: +```xml + + + +``` + +#### 5.4.2 RoundCorner + +Converts sharp corners of paths to rounded corners. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `radius` | float | 10 | Corner radius | + +**Processing Rules**: +- Only affects sharp corners (non-smooth connected vertices) +- Corner radius is automatically limited to not exceed half the length of adjacent edges +- `radius <= 0` performs no operation + +#### 5.4.3 MergePath + +Merges all shapes into a single shape. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `mode` | MergePathOp | append | Merge operation (see below) | + +**MergePathOp**: + +| Value | Description | +|-------|-------------| +| `append` | Append: Simply merge all paths without boolean operations (default) | +| `union` | Union: Merge all shape coverage areas | +| `intersect` | Intersect: Keep only overlapping areas of all shapes | +| `xor` | XOR: Keep non-overlapping areas | +| `difference` | Difference: Subtract subsequent shapes from first shape | + +**Important Behavior**: +- MergePath **clears all previously rendered styles** +- Current transformation matrices of shapes are applied during merge +- Merged shape's transformation matrix resets to identity matrix + +### 5.5 Text Modifiers + +Text modifiers transform individual glyphs within text. + +#### 5.5.1 Text Modifier Processing + +When a text modifier is encountered, **all glyph lists** accumulated in the context are combined into a unified glyph list for the operation. + +#### 5.5.2 Text to Shape Conversion + +When text encounters a shape modifier, it is forcibly converted to shape paths. + +**Conversion Rules**: + +1. **Trigger Condition**: Conversion triggered when text encounters TrimPath, RoundCorner, or MergePath +2. **Merge into Single Path**: All glyphs of one Text merge into **one** Path, not one independent Path per glyph +3. **Emoji Loss**: Emoji cannot be converted to path outlines; discarded during conversion +4. **Irreversible Conversion**: After conversion becomes pure Path; subsequent text modifiers have no effect on it + +#### 5.5.3 TextModifier + +Applies transforms and style overrides to glyphs within selected ranges. TextModifier may contain multiple RangeSelector child elements. + +```xml + + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `anchorPoint` | point | 0,0 | Anchor point offset | +| `position` | point | 0,0 | Position offset | +| `rotation` | float | 0 | Rotation | +| `scale` | point | 1,1 | Scale | +| `skew` | float | 0 | Skew | +| `skewAxis` | float | 0 | Skew axis | +| `alpha` | float | 1 | Opacity | +| `fillColor` | color | - | Fill color override | +| `strokeColor` | color | - | Stroke color override | +| `strokeWidth` | float | - | Stroke width override | + +**Selector Calculation**: +1. Calculate selection range based on RangeSelector's `start`, `end`, `offset` (supports any decimal value; automatically wraps when exceeding [0,1] range) +2. Calculate influence factor (0~1) for each glyph based on `shape` +3. Combine multiple selectors according to `mode` + +**Transform Application**: + +The `factor` calculated by the selector ranges from [-1, 1] and controls the degree to which transform properties are applied: + +``` +factor = clamp(selectorFactor × weight, -1, 1) + +// Position and rotation: apply factor linearly +transform = translate(-anchorPoint × factor) + × scale(1 + (scale - 1) × factor) // Scale interpolates from 1 to target value + × skew(skew × factor, skewAxis) + × rotate(rotation × factor) + × translate(anchorPoint × factor) + × translate(position × factor) + +// Opacity: use absolute value of factor +alphaFactor = 1 + (alpha - 1) × |factor| +finalAlpha = originalAlpha × max(0, alphaFactor) +``` + +**Color Override**: + +Color override uses the absolute value of `factor` for alpha blending: + +``` +blendFactor = overrideColor.alpha × |factor| +finalColor = blend(originalColor, overrideColor, blendFactor) +``` + +#### 5.5.4 RangeSelector + +Range selectors define the glyph range and influence degree for TextModifier. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `start` | float | 0 | Selection start | +| `end` | float | 1 | Selection end | +| `offset` | float | 0 | Selection offset | +| `unit` | SelectorUnit | percentage | Unit | +| `shape` | SelectorShape | square | Shape | +| `easeIn` | float | 0 | Ease in amount | +| `easeOut` | float | 0 | Ease out amount | +| `mode` | SelectorMode | add | Combine mode | +| `weight` | float | 1 | Selector weight | +| `randomOrder` | bool | false | Random order | +| `randomSeed` | int | 0 | Random seed | + +**SelectorUnit**: `index`, `percentage` + +**SelectorShape**: `square`, `rampUp`, `rampDown`, `triangle`, `round`, `smooth` + +**SelectorMode**: `add`, `subtract`, `intersect`, `min`, `max`, `difference` + +#### 5.5.5 TextPath + +Arranges text along a specified path. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `path` | string/idref | (required) | SVG path data or PathData resource reference "@id" | +| `textAlign` | TextAlign | start | Alignment mode | +| `firstMargin` | float | 0 | Start margin | +| `lastMargin` | float | 0 | End margin | +| `perpendicular` | bool | true | Perpendicular to path | +| `reversed` | bool | false | Reverse direction | + +**TextAlign in TextPath Context**: + +| Value | Description | +|-------|-------------| +| `start` | Arrange from path start | +| `center` | Center text on path | +| `end` | Text ends at path end | +| `justify` | Force fill path, automatically adjusting letter spacing to fill available path length (minus margins) | + +**Margins**: +- `firstMargin`: Start margin (offset inward from path start) +- `lastMargin`: End margin (offset inward from path end) + +**Glyph Positioning**: +1. Calculate glyph center position on path +2. Get path tangent direction at that position +3. If `perpendicular="true"`, rotate glyph perpendicular to path + +**Closed Paths**: For closed paths, glyphs exceeding the range wrap to the other end of the path. + +#### 5.5.6 TextLayout + +TextLayout is a text layout modifier that applies typography to accumulated Text elements. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `position` | point | 0,0 | Layout origin | +| `width` | float | auto | Layout width (auto-wraps when specified) | +| `height` | float | auto | Layout height (enables vertical alignment when specified) | +| `textAlign` | TextAlign | start | Horizontal alignment | +| `verticalAlign` | VerticalAlign | top | Vertical alignment | +| `writingMode` | WritingMode | horizontal | Layout direction | +| `lineHeight` | float | 1.2 | Line height multiplier | + +**TextAlign**: `start`, `center`, `end`, `justify` + +**VerticalAlign**: `top`, `center`, `bottom` + +**WritingMode**: `horizontal`, `vertical` + +#### 5.5.7 Rich Text + +Rich text is achieved through multiple Text elements within a Group, each Text having independent Fill/Stroke styles. TextLayout provides unified typography. + +### 5.6 Repeater + +Repeater duplicates accumulated content and rendered styles, applying progressive transforms to each copy. + +```xml + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `copies` | float | 3 | Number of copies | +| `offset` | float | 0 | Start offset | +| `order` | RepeaterOrder | belowOriginal | Stacking order | +| `anchorPoint` | point | 0,0 | Anchor point | +| `position` | point | 100,100 | Position offset per copy | +| `rotation` | float | 0 | Rotation per copy | +| `scale` | point | 1,1 | Scale per copy | +| `startAlpha` | float | 1 | First copy opacity | +| `endAlpha` | float | 1 | Last copy opacity | + +**Transform Calculation** (i-th copy, i starts from 0): +``` +progress = i + offset +matrix = translate(-anchorPoint) + × scale(scale^progress) // Exponential scaling + × rotate(rotation × progress) // Linear rotation + × translate(position × progress) // Linear translation + × translate(anchorPoint) +``` + +**Opacity Interpolation**: +``` +t = progress / copies +alpha = lerp(startAlpha, endAlpha, t) +``` + +**RepeaterOrder**: + +| Value | Description | +|-------|-------------| +| `belowOriginal` | Copies below original. Index 0 on top | +| `aboveOriginal` | Copies above original. Index N-1 on top | + +**Fractional Copy Count**: + +When `copies` is a decimal (e.g., `3.5`), partial copies are achieved through **overlay with semi-transparency**: + +1. **Geometry copying**: Shapes and text are copied by `ceil(copies)` (i.e., 4), geometry itself is not scaled or clipped +2. **Opacity adjustment**: The last copy's opacity is multiplied by the fractional part (e.g., 0.5), producing semi-transparent effect +3. **Visual effect**: Simulates "partially existing" copies through opacity gradation + +**Example**: When `copies="2.3"`: +- Copy 3 complete geometry copies +- Copies 1, 2 render normally +- Copy 3's opacity × 0.3, rendering semi-transparent + +**Edge Cases**: +- `copies < 0`: No operation performed +- `copies = 0`: Clear all accumulated content and rendered styles + +**Repeater Characteristics**: +- **Simultaneous effect**: Copies all accumulated Paths and glyph lists +- **Preserve text attributes**: Glyph lists retain glyph information after copying; subsequent text modifiers can still affect them +- **Copy rendered styles**: Also copies already rendered fills and strokes + +### 5.7 Group + +Group is a VectorElement container with transform properties. + +```xml + + + +``` + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `anchorPoint` | point | 0,0 | Anchor point "x,y" | +| `position` | point | 0,0 | Position "x,y" | +| `rotation` | float | 0 | Rotation angle | +| `scale` | point | 1,1 | Scale "sx,sy" | +| `skew` | float | 0 | Skew amount | +| `skewAxis` | float | 0 | Skew axis angle | +| `alpha` | float | 1 | Opacity 0~1 | + +#### Transform Order + +Transforms are applied in the following order: + +1. Translate to negative anchor point (`translate(-anchorPoint)`) +2. Scale (`scale`) +3. Skew (`skew` along `skewAxis` direction) +4. Rotate (`rotation`) +5. Translate to position (`translate(position)`) + +**Transform Matrix**: +``` +M = translate(position) × rotate(rotation) × skew(skew, skewAxis) × scale(scale) × translate(-anchorPoint) +``` + +**Skew Transform**: +``` +skewMatrix = rotate(skewAxis) × shearX(tan(skew)) × rotate(-skewAxis) +``` + +#### Scope Isolation + +Groups create isolated scopes for geometry accumulation and rendering: + +- Geometry elements within the group accumulate only within the group +- Painters within the group only render geometry accumulated within the group +- Modifiers within the group only affect geometry accumulated within the group +- The group's transform matrix applies to all content within the group +- The group's `alpha` property applies to all rendered content within the group + +**Geometry Accumulation Rules**: + +- **Painters do not clear geometry**: After Fill and Stroke render, the geometry list remains unchanged; subsequent painters can still render the same geometry +- **Child Group geometry propagates upward**: After a child Group completes, its geometry accumulates to the parent scope; painters at the end of the parent can render all child Group geometry +- **Sibling Groups do not affect each other**: Each Group creates an independent accumulation starting point; it cannot see geometry from subsequent sibling Groups +- **Isolate rendering scope**: Painters within a Group can only render geometry accumulated up to the current position, including this group and completed child Groups +- **Layer is accumulation boundary**: Geometry propagates upward until reaching a Layer boundary; it does not pass across Layers + +**Example 1 - Basic Isolation**: +```xml + + + + + + +``` + +**Example 2 - Child Group Geometry Propagates Upward**: +```xml + + + + + + + + + +``` + +**Example 3 - Multiple Painters Reuse Geometry**: +```xml + + + +``` + +#### Multiple Fills and Strokes + +Since painters do not clear the geometry list, the same geometry can have multiple Fills and Strokes applied consecutively. + +**Example 4 - Multiple Fills**: +```xml + + + + + +``` + +**Example 5 - Multiple Strokes**: +```xml + + + + +``` + +**Example 6 - Mixed Overlay**: +```xml + + + + + + + + + + + +``` + +**Rendering Order**: Multiple painters render in document order; those appearing earlier are below. + +--- + +## Appendix A. Node Hierarchy + +This appendix describes node categorization and nesting rules. + +### A.1 Node Categories + +| Category | Nodes | +|----------|-------| +| **Document Root** | `pagx` | +| **Resources** | `Resources`, `Image`, `PathData`, `Font`, `Glyph`, `SolidColor`, `LinearGradient`, `RadialGradient`, `ConicGradient`, `DiamondGradient`, `ColorStop`, `ImagePattern`, `Composition` | +| **Layer** | `Layer` | +| **Layer Styles** | `DropShadowStyle`, `InnerShadowStyle`, `BackgroundBlurStyle` | +| **Filters** | `BlurFilter`, `DropShadowFilter`, `InnerShadowFilter`, `BlendFilter`, `ColorMatrixFilter` | +| **Geometry Elements** | `Rectangle`, `Ellipse`, `Polystar`, `Path`, `Text` | +| **Pre-layout Data** | `GlyphRun` (Text child element) | +| **Painters** | `Fill`, `Stroke` | +| **Shape Modifiers** | `TrimPath`, `RoundCorner`, `MergePath` | +| **Text Modifiers** | `TextModifier`, `TextPath`, `TextLayout` | +| **Text Selectors** | `RangeSelector` (TextModifier child element) | +| **Other** | `Repeater`, `Group` | + +### A.2 Document Containment + +``` +pagx +├── Resources +│ ├── Image +│ ├── PathData +│ ├── SolidColor +│ ├── LinearGradient → ColorStop* +│ ├── RadialGradient → ColorStop* +│ ├── ConicGradient → ColorStop* +│ ├── DiamondGradient → ColorStop* +│ ├── ImagePattern +│ ├── Font → Glyph* +│ └── Composition → Layer* +│ +└── Layer* + ├── VectorElement* (see A.3) + ├── DropShadowStyle* + ├── InnerShadowStyle* + ├── BackgroundBlurStyle* + ├── BlurFilter* + ├── DropShadowFilter* + ├── InnerShadowFilter* + ├── BlendFilter* + ├── ColorMatrixFilter* + └── Layer* (child layers) +``` + +### A.3 VectorElement Containment + +`Layer` and `Group` may contain the following VectorElements: + +``` +Layer / Group +├── Rectangle +├── Ellipse +├── Polystar +├── Path +├── Text → GlyphRun* (pre-layout mode) +├── Fill (may embed color source) +│ └── SolidColor / LinearGradient / RadialGradient / ConicGradient / DiamondGradient / ImagePattern +├── Stroke (may embed color source) +│ └── SolidColor / LinearGradient / RadialGradient / ConicGradient / DiamondGradient / ImagePattern +├── TrimPath +├── RoundCorner +├── MergePath +├── TextModifier → RangeSelector* +├── TextPath +├── TextLayout +├── Repeater +└── Group* (recursive) +``` + +--- + +## Appendix B. Common Usage Examples + +### B.1 Complete Example + +The following example covers all major node types in PAGX, demonstrating complete document structure. + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**Example Description**: + +This example demonstrates the complete feature set of PAGX with a modern dark theme design: + +| Category | Nodes Covered | +|----------|--------------| +| **Resources** | Image, PathData, Font/Glyph, SolidColor, LinearGradient, RadialGradient, ConicGradient, Composition | +| **Geometry Elements** | Rectangle, Ellipse, Polystar (star/polygon), Path, Text | +| **Painters** | Fill (solid/gradient/image), Stroke | +| **Layer Styles** | DropShadowStyle, InnerShadowStyle | +| **Filters** | BlurFilter, DropShadowFilter, BlendFilter, ColorMatrixFilter | +| **Shape Modifiers** | TrimPath, RoundCorner, MergePath | +| **Text Modifiers** | TextModifier/RangeSelector, TextPath, TextLayout, GlyphRun | +| **Other** | Repeater, Group, Masking (mask/maskType), Composition reference | + +--- + +## Appendix C. Node and Attribute Reference + +This appendix lists all node attribute definitions. The `id` and `data-*` attributes are common attributes available on any element (see Section 2.3) and are not repeated in each table. + +### C.1 Document Structure Nodes + +#### pagx + +| Attribute | Type | Default | +|-----------|------|---------| +| `version` | string | (required) | +| `width` | float | (required) | +| `height` | float | (required) | + +#### Composition + +| Attribute | Type | Default | +|-----------|------|---------| +| `width` | float | (required) | +| `height` | float | (required) | + +### C.2 Resource Nodes + +#### Image + +| Attribute | Type | Default | +|-----------|------|---------| +| `source` | string | (required) | + +#### PathData + +| Attribute | Type | Default | +|-----------|------|---------| +| `data` | string | (required) | + +#### Font + +Child elements: `Glyph`* + +#### Glyph + +| Attribute | Type | Default | +|-----------|------|---------| +| `path` | string | - | +| `image` | string | - | +| `offset` | point | 0,0 | + +#### SolidColor + +| Attribute | Type | Default | +|-----------|------|---------| +| `color` | color | (required) | + +#### LinearGradient + +| Attribute | Type | Default | +|-----------|------|---------| +| `startPoint` | point | (required) | +| `endPoint` | point | (required) | +| `matrix` | string | identity matrix | + +Child elements: `ColorStop`+ + +#### RadialGradient + +| Attribute | Type | Default | +|-----------|------|---------| +| `center` | point | 0,0 | +| `radius` | float | (required) | +| `matrix` | string | identity matrix | + +Child elements: `ColorStop`+ + +#### ConicGradient + +| Attribute | Type | Default | +|-----------|------|---------| +| `center` | point | 0,0 | +| `startAngle` | float | 0 | +| `endAngle` | float | 360 | +| `matrix` | string | identity matrix | + +Child elements: `ColorStop`+ + +#### DiamondGradient + +| Attribute | Type | Default | +|-----------|------|---------| +| `center` | point | 0,0 | +| `radius` | float | (required) | +| `matrix` | string | identity matrix | + +Child elements: `ColorStop`+ + +#### ColorStop + +| Attribute | Type | Default | +|-----------|------|---------| +| `offset` | float | (required) | +| `color` | color | (required) | + +#### ImagePattern + +| Attribute | Type | Default | +|-----------|------|---------| +| `image` | idref | (required) | +| `tileModeX` | TileMode | clamp | +| `tileModeY` | TileMode | clamp | +| `minFilterMode` | FilterMode | linear | +| `magFilterMode` | FilterMode | linear | +| `mipmapMode` | MipmapMode | linear | +| `matrix` | string | identity matrix | + +### C.3 Layer Node + +#### Layer + +| Attribute | Type | Default | +|-----------|------|---------| +| `name` | string | "" | +| `visible` | bool | true | +| `alpha` | float | 1 | +| `blendMode` | BlendMode | normal | +| `x` | float | 0 | +| `y` | float | 0 | +| `matrix` | string | identity matrix | +| `matrix3D` | string | - | +| `preserve3D` | bool | false | +| `antiAlias` | bool | true | +| `groupOpacity` | bool | false | +| `passThroughBackground` | bool | true | +| `excludeChildEffectsInLayerStyle` | bool | false | +| `scrollRect` | string | - | +| `mask` | idref | - | +| `maskType` | MaskType | alpha | +| `composition` | idref | - | + +Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* + +### C.4 Layer Style Nodes + +#### DropShadowStyle + +| Attribute | Type | Default | +|-----------|------|---------| +| `offsetX` | float | 0 | +| `offsetY` | float | 0 | +| `blurX` | float | 0 | +| `blurY` | float | 0 | +| `color` | color | #000000 | +| `showBehindLayer` | bool | true | +| `blendMode` | BlendMode | normal | + +#### InnerShadowStyle + +| Attribute | Type | Default | +|-----------|------|---------| +| `offsetX` | float | 0 | +| `offsetY` | float | 0 | +| `blurX` | float | 0 | +| `blurY` | float | 0 | +| `color` | color | #000000 | +| `blendMode` | BlendMode | normal | + +#### BackgroundBlurStyle + +| Attribute | Type | Default | +|-----------|------|---------| +| `blurX` | float | 0 | +| `blurY` | float | 0 | +| `tileMode` | TileMode | mirror | +| `blendMode` | BlendMode | normal | + +### C.5 Filter Nodes + +#### BlurFilter + +| Attribute | Type | Default | +|-----------|------|---------| +| `blurX` | float | (required) | +| `blurY` | float | (required) | +| `tileMode` | TileMode | decal | + +#### DropShadowFilter + +| Attribute | Type | Default | +|-----------|------|---------| +| `offsetX` | float | 0 | +| `offsetY` | float | 0 | +| `blurX` | float | 0 | +| `blurY` | float | 0 | +| `color` | color | #000000 | +| `shadowOnly` | bool | false | + +#### InnerShadowFilter + +| Attribute | Type | Default | +|-----------|------|---------| +| `offsetX` | float | 0 | +| `offsetY` | float | 0 | +| `blurX` | float | 0 | +| `blurY` | float | 0 | +| `color` | color | #000000 | +| `shadowOnly` | bool | false | + +#### BlendFilter + +| Attribute | Type | Default | +|-----------|------|---------| +| `color` | color | (required) | +| `blendMode` | BlendMode | normal | + +#### ColorMatrixFilter + +| Attribute | Type | Default | +|-----------|------|---------| +| `matrix` | string | (required) | + +### C.6 Geometry Element Nodes + +#### Rectangle + +| Attribute | Type | Default | +|-----------|------|---------| +| `center` | point | 0,0 | +| `size` | size | 100,100 | +| `roundness` | float | 0 | +| `reversed` | bool | false | + +#### Ellipse + +| Attribute | Type | Default | +|-----------|------|---------| +| `center` | point | 0,0 | +| `size` | size | 100,100 | +| `reversed` | bool | false | + +#### Polystar + +| Attribute | Type | Default | +|-----------|------|---------| +| `center` | point | 0,0 | +| `type` | PolystarType | star | +| `pointCount` | float | 5 | +| `outerRadius` | float | 100 | +| `innerRadius` | float | 50 | +| `rotation` | float | 0 | +| `outerRoundness` | float | 0 | +| `innerRoundness` | float | 0 | +| `reversed` | bool | false | + +#### Path + +| Attribute | Type | Default | +|-----------|------|---------| +| `data` | string/idref | (required) | +| `reversed` | bool | false | + +#### Text + +| Attribute | Type | Default | +|-----------|------|---------| +| `text` | string | "" | +| `position` | point | 0,0 | +| `fontFamily` | string | system default | +| `fontStyle` | string | "Regular" | +| `fontSize` | float | 12 | +| `letterSpacing` | float | 0 | +| `baselineShift` | float | 0 | + +Child elements: `CDATA` text, `GlyphRun`* + +#### GlyphRun + +| Attribute | Type | Default | +|-----------|------|---------| +| `font` | idref | (required) | +| `glyphs` | string | (required) | +| `y` | float | 0 | +| `xPositions` | string | - | +| `positions` | string | - | +| `xforms` | string | - | +| `matrices` | string | - | + +### C.7 Painter Nodes + +#### Fill + +| Attribute | Type | Default | +|-----------|------|---------| +| `color` | color/idref | #000000 | +| `alpha` | float | 1 | +| `blendMode` | BlendMode | normal | +| `fillRule` | FillRule | winding | +| `placement` | LayerPlacement | background | + +#### Stroke + +| Attribute | Type | Default | +|-----------|------|---------| +| `color` | color/idref | #000000 | +| `width` | float | 1 | +| `alpha` | float | 1 | +| `blendMode` | BlendMode | normal | +| `cap` | LineCap | butt | +| `join` | LineJoin | miter | +| `miterLimit` | float | 4 | +| `dashes` | string | - | +| `dashOffset` | float | 0 | +| `align` | StrokeAlign | center | +| `placement` | LayerPlacement | background | + +### C.8 Shape Modifier Nodes + +#### TrimPath + +| Attribute | Type | Default | +|-----------|------|---------| +| `start` | float | 0 | +| `end` | float | 1 | +| `offset` | float | 0 | +| `type` | TrimType | separate | + +#### RoundCorner + +| Attribute | Type | Default | +|-----------|------|---------| +| `radius` | float | 10 | + +#### MergePath + +| Attribute | Type | Default | +|-----------|------|---------| +| `mode` | MergePathOp | append | + +### C.9 Text Modifier Nodes + +#### TextModifier + +| Attribute | Type | Default | +|-----------|------|---------| +| `anchorPoint` | point | 0,0 | +| `position` | point | 0,0 | +| `rotation` | float | 0 | +| `scale` | point | 1,1 | +| `skew` | float | 0 | +| `skewAxis` | float | 0 | +| `alpha` | float | 1 | +| `fillColor` | color | - | +| `strokeColor` | color | - | +| `strokeWidth` | float | - | + +Child elements: `RangeSelector`* + +#### RangeSelector + +| Attribute | Type | Default | +|-----------|------|---------| +| `start` | float | 0 | +| `end` | float | 1 | +| `offset` | float | 0 | +| `unit` | SelectorUnit | percentage | +| `shape` | SelectorShape | square | +| `easeIn` | float | 0 | +| `easeOut` | float | 0 | +| `mode` | SelectorMode | add | +| `weight` | float | 1 | +| `randomOrder` | bool | false | +| `randomSeed` | int | 0 | + +#### TextPath + +| Attribute | Type | Default | +|-----------|------|---------| +| `path` | string/idref | (required) | +| `textAlign` | TextAlign | start | +| `firstMargin` | float | 0 | +| `lastMargin` | float | 0 | +| `perpendicular` | bool | true | +| `reversed` | bool | false | + +#### TextLayout + +| Attribute | Type | Default | +|-----------|------|---------| +| `position` | point | 0,0 | +| `width` | float | auto | +| `height` | float | auto | +| `textAlign` | TextAlign | start | +| `verticalAlign` | VerticalAlign | top | +| `writingMode` | WritingMode | horizontal | +| `lineHeight` | float | 1.2 | + +### C.10 Other Nodes + +#### Repeater + +| Attribute | Type | Default | +|-----------|------|---------| +| `copies` | float | 3 | +| `offset` | float | 0 | +| `order` | RepeaterOrder | belowOriginal | +| `anchorPoint` | point | 0,0 | +| `position` | point | 100,100 | +| `rotation` | float | 0 | +| `scale` | point | 1,1 | +| `startAlpha` | float | 1 | +| `endAlpha` | float | 1 | + +#### Group + +| Attribute | Type | Default | +|-----------|------|---------| +| `anchorPoint` | point | 0,0 | +| `position` | point | 0,0 | +| `rotation` | float | 0 | +| `scale` | point | 1,1 | +| `skew` | float | 0 | +| `skewAxis` | float | 0 | +| `alpha` | float | 1 | + +Child elements: `VectorElement`* (recursive including Group) + +### C.11 Enumeration Types + +#### Layer Related + +| Enum | Values | +|------|--------| +| **BlendMode** | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`, `softLight`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plusLighter`, `plusDarker` | +| **MaskType** | `alpha`, `luminance`, `contour` | +| **TileMode** | `clamp`, `repeat`, `mirror`, `decal` | +| **FilterMode** | `nearest`, `linear` | +| **MipmapMode** | `none`, `nearest`, `linear` | + +#### Color Related + +| Enum | Values | +|------|--------| +| **ColorSpace** | `sRGB`, `displayP3` | + +#### Painter Related + +| Enum | Values | +|------|--------| +| **FillRule** | `winding`, `evenOdd` | +| **LineCap** | `butt`, `round`, `square` | +| **LineJoin** | `miter`, `round`, `bevel` | +| **StrokeAlign** | `center`, `inside`, `outside` | +| **LayerPlacement** | `background`, `foreground` | + +#### Geometry Element Related + +| Enum | Values | +|------|--------| +| **PolystarType** | `polygon`, `star` | + +#### Modifier Related + +| Enum | Values | +|------|--------| +| **TrimType** | `separate`, `continuous` | +| **MergePathOp** | `append`, `union`, `intersect`, `xor`, `difference` | +| **SelectorUnit** | `index`, `percentage` | +| **SelectorShape** | `square`, `rampUp`, `rampDown`, `triangle`, `round`, `smooth` | +| **SelectorMode** | `add`, `subtract`, `intersect`, `min`, `max`, `difference` | +| **TextAlign** | `start`, `center`, `end`, `justify` | +| **VerticalAlign** | `top`, `center`, `bottom` | +| **WritingMode** | `horizontal`, `vertical` | +| **RepeaterOrder** | `belowOriginal`, `aboveOriginal` | diff --git a/pagx/docs/pagx_spec.md b/pagx/spec/pagx_spec.zh_CN.md similarity index 99% rename from pagx/docs/pagx_spec.md rename to pagx/spec/pagx_spec.zh_CN.md index c8b40600da..9d6e92e82f 100644 --- a/pagx/docs/pagx_spec.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -1,18 +1,16 @@ -# PAGX 格式规范 v1.0 +# PAGX 格式规范 ## 1. 介绍(Introduction) **PAGX**(Portable Animated Graphics XML)是一种基于 XML 的矢量动画标记语言。它提供了统一且强大的矢量图形与动画描述能力,旨在成为跨所有主要工具与运行时的矢量动画交换标准。 -**PAGX** (Portable Animated Graphics XML) is an XML-based markup language for describing animated vector graphics. It provides a unified and powerful representation of vector graphics and animations, designed to be the vector animation interchange standard across all major tools and runtimes. - ### 1.1 设计目标 - **开放可读**:纯文本 XML 格式,易于阅读和编辑,天然支持版本控制与差异对比,便于调试及 AI 理解与生成。 - **特性完备**:完整覆盖矢量图形、图片、富文本、滤镜效果、混合模式、遮罩等能力,满足复杂动效的描述需求。 -- **精简高效**:提供简洁且强大的统一结构,兼顾静态矢量与动画的优化描述,同时预留未来交互和脚本的扩展能力。 +- **精简高效**:提供简洁且强大的统一结构,兼顾静态的优化描述,同时预留未来交互和脚本的扩展能力。 - **生态兼容**:可作为 After Effects、Figma、腾讯设计等设计工具的通用交换格式,实现设计资产无缝流转。 diff --git a/pagx/scripts/publish_spec.js b/pagx/spec/script/publish.js similarity index 51% rename from pagx/scripts/publish_spec.js rename to pagx/spec/script/publish.js index 141ec6e1ad..a8c8ccd048 100755 --- a/pagx/scripts/publish_spec.js +++ b/pagx/spec/script/publish.js @@ -2,32 +2,75 @@ /** * PAGX Specification Publisher * - * Converts the PAGX specification Markdown document to a standalone HTML page + * Converts the PAGX specification Markdown documents to standalone HTML pages * with syntax highlighting and optional draft banner. * - * Usage: - * node publish_spec.js [--output ] [--draft] + * Source files: + * pagx_spec.md - English version + * pagx_spec.zh_CN.md - Chinese version + * + * Output structure: + * ../public//index.html - English (default) + * ../public//cn/index.html - Chinese * - * Options: - * --output, -o Output directory (default: pagx/.spec_output/) - * --draft, -d Show draft banner at the top of the page + * Usage: + * cd pagx/spec && npm run publish */ const fs = require('fs'); const path = require('path'); +const { Marked } = require('marked'); +const { markedHighlight } = require('marked-highlight'); +const { gfmHeadingId } = require('marked-gfm-heading-id'); +const hljs = require('highlight.js'); // Default paths const SCRIPT_DIR = __dirname; -const PAGX_DIR = path.dirname(SCRIPT_DIR); -const SPEC_FILE = path.join(PAGX_DIR, 'docs', 'pagx_spec.md'); -const DEFAULT_OUTPUT_DIR = path.join(PAGX_DIR, '.spec_output'); +const SPEC_DIR = path.dirname(SCRIPT_DIR); +const PAGX_DIR = path.dirname(SPEC_DIR); +const SPEC_FILE_EN = path.join(SPEC_DIR, 'pagx_spec.md'); +const SPEC_FILE_ZH = path.join(SPEC_DIR, 'pagx_spec.zh_CN.md'); +const PACKAGE_FILE = path.join(SPEC_DIR, 'package.json'); +const SITE_DIR = path.join(PAGX_DIR, 'public'); + +/** + * Read version from package.json. + */ +function getVersion() { + const pkg = JSON.parse(fs.readFileSync(PACKAGE_FILE, 'utf-8')); + return pkg.version; +} + +/** + * Read stable version from package.json. + */ +function getStableVersion() { + const pkg = JSON.parse(fs.readFileSync(PACKAGE_FILE, 'utf-8')); + return pkg.stableVersion || ''; +} + +/** + * Format date based on language. + */ +function formatDate(date, lang) { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + if (lang === 'en') { + const months = ['January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December']; + return `${day} ${months[month - 1]} ${year}`; + } else { + return `${year} 年 ${month} 月 ${day} 日`; + } +} /** * Convert heading text to URL-friendly slug. */ function slugify(text) { let slug = text.toLowerCase(); - // Remove special characters but keep Chinese characters and alphanumeric slug = slug.replace(/[^\w\u4e00-\u9fff\s-]/g, ''); slug = slug.replace(/[\s_]+/g, '-'); slug = slug.replace(/-+/g, '-'); @@ -40,13 +83,24 @@ function slugify(text) { */ function parseMarkdownHeadings(content) { const headings = []; + const slugCounts = {}; const lines = content.split('\n'); + for (const line of lines) { const match = line.match(/^(#{1,6})\s+(.+)$/); if (match) { const level = match[1].length; const text = match[2].trim(); - const slug = slugify(text); + let slug = slugify(text); + + // Handle duplicate slugs + if (slugCounts[slug] !== undefined) { + slugCounts[slug]++; + slug = `${slug}-${slugCounts[slug]}`; + } else { + slugCounts[slug] = 0; + } + headings.push({ level, text, slug }); } } @@ -112,194 +166,62 @@ function generateTocHtml(headings) { } /** - * Escape HTML special characters. + * Create configured Marked instance with syntax highlighting. */ -function escapeHtml(text) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -/** - * Convert Markdown content to HTML. - */ -function convertMarkdownToHtml(content, headings) { - // Create a map for heading slugs to handle duplicates +function createMarkedInstance() { const slugCounts = {}; - // Process code blocks first to protect them - const codeBlocks = []; - let processedContent = content.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => { - const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`; - const langClass = lang ? ` class="language-${lang}"` : ''; - codeBlocks.push(`
    ${escapeHtml(code.trim())}
    `); - return placeholder; - }); - - // Process inline code - processedContent = processedContent.replace(/`([^`]+)`/g, '$1'); - - // Process headings with IDs - processedContent = processedContent.replace(/^(#{1,6})\s+(.+)$/gm, (match, hashes, text) => { - const level = hashes.length; - let slug = slugify(text); - - // Handle duplicate slugs - if (slugCounts[slug] !== undefined) { - slugCounts[slug]++; - slug = `${slug}-${slugCounts[slug]}`; - } else { - slugCounts[slug] = 0; - } - - return `${text}`; - }); - - // Process bold - processedContent = processedContent.replace(/\*\*([^*]+)\*\*/g, '$1'); - - // Process italic - processedContent = processedContent.replace(/\*([^*]+)\*/g, '$1'); - - // Process links - processedContent = processedContent.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); - - // Process horizontal rules - processedContent = processedContent.replace(/^---+$/gm, '
    '); - - // Process blockquotes - processedContent = processedContent.replace(/^>\s*(.*)$/gm, '
    $1
    '); - // Merge adjacent blockquotes - processedContent = processedContent.replace(/<\/blockquote>\n
    /g, '\n'); - - // Process tables - processedContent = processedContent.replace( - /^\|(.+)\|\n\|[-:| ]+\|\n((?:\|.+\|\n?)+)/gm, - (match, headerRow, bodyRows) => { - const headers = headerRow.split('|').map((h) => h.trim()).filter(Boolean); - const headerHtml = headers.map((h) => `${h}`).join(''); - - const rows = bodyRows.trim().split('\n'); - const bodyHtml = rows - .map((row) => { - const cells = row.split('|').map((c) => c.trim()).filter(Boolean); - return `${cells.map((c) => `${c}`).join('')}`; - }) - .join('\n'); - - return `\n${headerHtml}\n\n${bodyHtml}\n\n
    `; - } + const marked = new Marked( + markedHighlight({ + langPrefix: 'hljs language-', + highlight(code, lang) { + if (lang && hljs.getLanguage(lang)) { + return hljs.highlight(code, { language: lang }).value; + } + return hljs.highlightAuto(code).value; + }, + }), + gfmHeadingId({ + prefix: '', + // Custom slug function to handle Chinese characters + slug: (text) => { + let slug = slugify(text); + if (slugCounts[slug] !== undefined) { + slugCounts[slug]++; + slug = `${slug}-${slugCounts[slug]}`; + } else { + slugCounts[slug] = 0; + } + return slug; + }, + }) ); - // Process unordered lists - let inList = false; - const lines = processedContent.split('\n'); - const processedLines = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const listMatch = line.match(/^(\s*)[-*]\s+(.+)$/); - - if (listMatch) { - if (!inList) { - processedLines.push('
      '); - inList = true; - } - processedLines.push(`
    • ${listMatch[2]}
    • `); - } else { - if (inList && line.trim() !== '') { - processedLines.push('
    '); - inList = false; - } - processedLines.push(line); - } - } - if (inList) { - processedLines.push(''); - } - processedContent = processedLines.join('\n'); - - // Process ordered lists - inList = false; - const lines2 = processedContent.split('\n'); - const processedLines2 = []; - - for (let i = 0; i < lines2.length; i++) { - const line = lines2[i]; - const listMatch = line.match(/^(\s*)\d+\.\s+(.+)$/); - - if (listMatch) { - if (!inList) { - processedLines2.push('
      '); - inList = true; - } - processedLines2.push(`
    1. ${listMatch[2]}
    2. `); - } else { - if (inList && line.trim() !== '') { - processedLines2.push('
    '); - inList = false; - } - processedLines2.push(line); - } - } - if (inList) { - processedLines2.push(''); - } - processedContent = processedLines2.join('\n'); - - // Process paragraphs - wrap non-HTML lines in

    tags - const finalLines = processedContent.split('\n'); - const result = []; - let paragraph = []; - - for (const line of finalLines) { - const trimmed = line.trim(); - - // Check if line starts with HTML tag or is a placeholder - const isHtml = - trimmed.startsWith('<') || - trimmed.startsWith('__CODE_BLOCK_') || - trimmed === ''; - - if (isHtml) { - if (paragraph.length > 0) { - result.push(`

    ${paragraph.join(' ')}

    `); - paragraph = []; - } - if (trimmed !== '') { - result.push(line); - } - } else { - paragraph.push(trimmed); - } - } - if (paragraph.length > 0) { - result.push(`

    ${paragraph.join(' ')}

    `); - } - - processedContent = result.join('\n'); - - // Restore code blocks - for (let i = 0; i < codeBlocks.length; i++) { - processedContent = processedContent.replace(`__CODE_BLOCK_${i}__`, codeBlocks[i]); - } + marked.setOptions({ + gfm: true, + breaks: false, + }); - return processedContent; + return marked; } /** * Generate the complete HTML document. */ -function generateHtml(content, title, tocHtml, showDraft) { +function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl) { + const isEnglish = lang === 'en'; + const htmlLang = isEnglish ? 'en' : 'zh-CN'; + const tocTitle = isEnglish ? 'Table of Contents' : '目录'; + let draftBanner = ''; let draftStyles = ''; if (showDraft) { + const draftText = isEnglish + ? 'DRAFT This specification is a working draft and may change at any time.' + : '草案 本规范为草稿版本,内容可能随时变更。'; draftBanner = `
    - DRAFT This specification is a working draft and may change at any time. 本规范为草稿版本,内容可能随时变更。 + ${draftText}
    `; draftStyles = ` .sidebar { @@ -309,6 +231,9 @@ function generateHtml(content, title, tocHtml, showDraft) { .content { padding-top: 82px; } + .lang-switch { + top: 54px; + } @media (max-width: 900px) { .content { padding-top: 62px; @@ -317,7 +242,7 @@ function generateHtml(content, title, tocHtml, showDraft) { } return ` - + @@ -414,6 +339,13 @@ function generateHtml(content, title, tocHtml, showDraft) { font-size: 2em; padding-bottom: 0.3em; border-bottom: 1px solid var(--border-color); + margin-bottom: 16px; + } + .last-updated { + margin-top: 0; + margin-bottom: 24px; + color: #6a737d; + font-size: 14px; } h2 { font-size: 1.5em; @@ -563,13 +495,90 @@ function generateHtml(content, title, tocHtml, showDraft) { .draft-banner strong { color: #5a4a06; } + /* Language switcher */ + .lang-switch { + position: fixed; + top: 12px; + right: 20px; + z-index: 1001; + } + .lang-switch-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: #f8f9fa; + border: 1px solid var(--border-color); + border-radius: 6px; + color: #555; + font-size: 13px; + cursor: pointer; + transition: all 0.2s; + } + .lang-switch-btn:hover { + background: #e9ecef; + } + .lang-switch-btn svg { + width: 12px; + height: 12px; + transition: transform 0.2s; + } + .lang-switch.open .lang-switch-btn svg { + transform: rotate(180deg); + } + .lang-switch-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + background: white; + border: 1px solid var(--border-color); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + overflow: hidden; + opacity: 0; + visibility: hidden; + transform: translateY(-8px); + transition: all 0.2s; + } + .lang-switch.open .lang-switch-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); + } + .lang-switch-menu a { + display: block; + padding: 8px 16px; + color: #555; + font-size: 13px; + text-decoration: none; + white-space: nowrap; + } + .lang-switch-menu a:hover { + background: #f8f9fa; + color: var(--primary-color); + } + .lang-switch-menu a.active { + background: #e3f2fd; + color: #1565c0; + } ${draftStyles} -${draftBanner}
    +${draftBanner}
    + + +
    +
    - - `; } +/** + * Publish a single spec file. + */ +function publishSpec(specFile, outputDir, lang, version, showDraft, langSwitchUrl) { + if (!fs.existsSync(specFile)) { + console.log(` Skipped (file not found): ${specFile}`); + return; + } + + console.log(` Reading: ${specFile}`); + let mdContent = fs.readFileSync(specFile, 'utf-8'); + + // Get file last modified time + const stats = fs.statSync(specFile); + const lastModified = stats.mtime; + + // Format date based on language + const dateStr = formatDate(lastModified, lang); + + // Generate version info HTML + const isEnglish = lang === 'en'; + const lastUpdatedLabel = isEnglish ? `Last updated: ${dateStr}` : `最后更新:${dateStr}`; + + // Inject version into title and add last updated below + mdContent = mdContent.replace(/^(#\s+.+)$/m, `$1 ${version}\n\n

    ${lastUpdatedLabel}

    `); + + // Extract title + const titleMatch = mdContent.match(/^#\s+(.+)$/m); + const title = titleMatch ? titleMatch[1] : 'PAGX Format Specification'; + + // Parse headings for TOC + const headings = parseMarkdownHeadings(mdContent); + + // Generate TOC HTML + const tocHtml = generateTocHtml(headings); + + // Convert Markdown to HTML using marked + const marked = createMarkedInstance(); + const htmlContent = marked.parse(mdContent); + + // Generate complete HTML document + const html = generateHtml(htmlContent, title, tocHtml, lang, showDraft, langSwitchUrl); + + // Create output directory + fs.mkdirSync(outputDir, { recursive: true }); + + // Write output file + const outputFile = path.join(outputDir, 'index.html'); + fs.writeFileSync(outputFile, html, 'utf-8'); + + console.log(` Published: ${outputFile}`); +} + /** * Parse command line arguments. */ function parseArgs() { const args = process.argv.slice(2); - const options = { - output: DEFAULT_OUTPUT_DIR, - draft: false, - }; + const options = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; - if (arg === '--output' || arg === '-o') { - options.output = args[++i]; - } else if (arg === '--draft' || arg === '-d') { - options.draft = true; - } else if (arg === '--help' || arg === '-h') { + if (arg === '--help' || arg === '-h') { console.log(` PAGX Specification Publisher Usage: - node publish_spec.js [--output ] [--draft] + cd pagx/spec && npm run publish -Options: - --output, -o Output directory (default: pagx/.spec_output/) - --draft, -d Show draft banner at the top of the page - --help, -h Show this help message +Configuration (package.json): + version - Current version to publish + stableVersion - Latest stable version (empty if none) + If version != stableVersion, draft banner is shown -Examples: - # Publish to default location (pagx/.spec_output/) - node publish_spec.js +Source files: + spec/pagx_spec.md - English version + spec/pagx_spec.zh_CN.md - Chinese version - # Publish as draft - node publish_spec.js --draft +Output structure: + public/index.html - Redirect page + public//index.html - English (default) + public//cn/index.html - Chinese - # Publish to custom directory - node publish_spec.js --output /path/to/output - - # Publish as draft to custom directory - node publish_spec.js --draft --output /path/to/output +Examples: + npm run publish:spec `); process.exit(0); } @@ -654,43 +710,75 @@ Examples: function main() { const options = parseArgs(); - // Check if source file exists - if (!fs.existsSync(SPEC_FILE)) { - console.error(`Error: Specification file not found: ${SPEC_FILE}`); - process.exit(1); + // Read versions from package.json + const version = getVersion(); + const stableVersion = getStableVersion(); + const isDraft = version !== stableVersion; + + console.log(`Version: ${version}`); + console.log(`Stable: ${stableVersion || '(none)'}`); + if (isDraft) { + console.log('Mode: Draft'); } - // Read Markdown content - console.log(`Reading: ${SPEC_FILE}`); - const mdContent = fs.readFileSync(SPEC_FILE, 'utf-8'); + const baseOutputDir = path.join(SITE_DIR, version); - // Extract title from first heading - const titleMatch = mdContent.match(/^#\s+(.+)$/m); - const title = titleMatch ? titleMatch[1] : 'PAGX Format Specification'; - - // Parse headings for TOC - const headings = parseMarkdownHeadings(mdContent); + // Publish English version (default, at root) + console.log('\nPublishing English version...'); + publishSpec(SPEC_FILE_EN, baseOutputDir, 'en', version, isDraft, 'cn/'); - // Generate TOC HTML - const tocHtml = generateTocHtml(headings); + // Publish Chinese version (under /cn/) + console.log('\nPublishing Chinese version...'); + publishSpec(SPEC_FILE_ZH, path.join(baseOutputDir, 'cn'), 'zh', version, isDraft, '../'); - // Convert Markdown to HTML - const htmlContent = convertMarkdownToHtml(mdContent, headings); + // Generate redirect index page (point to stableVersion if exists, otherwise current version) + const redirectVersion = stableVersion || version; + console.log('\nGenerating redirect page...'); + console.log(` Redirect to: ${redirectVersion}`); + generateRedirectPage(redirectVersion); - // Generate complete HTML document - const html = generateHtml(htmlContent, title, tocHtml, options.draft); + console.log('\nDone!'); +} - // Create output directory - fs.mkdirSync(options.output, { recursive: true }); +/** + * Generate the redirect index page at site/index.html. + */ +function generateRedirectPage(version) { + const html = ` + + + + + PAGX Specification + + + +

    Redirecting to the latest specification...

    + + +`; - // Write output file - const outputFile = path.join(options.output, 'index.html'); + fs.mkdirSync(SITE_DIR, { recursive: true }); + const outputFile = path.join(SITE_DIR, 'index.html'); fs.writeFileSync(outputFile, html, 'utf-8'); - - console.log(`Published to: ${outputFile}`); - if (options.draft) { - console.log(' (with draft banner)'); - } + console.log(` Generated: ${outputFile}`); } main(); diff --git a/pagx/viewer/.gitignore b/pagx/viewer/.gitignore index 9c2ab5fcc0..58cb8738c6 100644 --- a/pagx/viewer/.gitignore +++ b/pagx/viewer/.gitignore @@ -1,7 +1,5 @@ # Build outputs wasm-mt/ -dist/ -fonts/ # Build cache script/build-pagx-viewer/ diff --git a/pagx/viewer/package.json b/pagx/viewer/package.json index a081d1c251..26e2d7e6d6 100644 --- a/pagx/viewer/package.json +++ b/pagx/viewer/package.json @@ -4,10 +4,11 @@ "description": "PAGX File Viewer", "type": "module", "scripts": { - "clean": "rimraf wasm-mt build-pagx .pagx.wasm-mt.md5", + "clean": "rimraf wasm-mt", "build": "node script/cmake.js -a wasm-mt && rollup -c ./script/rollup.js", "build:debug": "node script/cmake.js -a wasm-mt --debug && rollup -c ./script/rollup.js", "build:release": "node script/cmake.js -a wasm-mt && BUILD_MODE=release rollup -c ./script/rollup.js", + "publish": "node script/publish.cjs", "server": "node server.js" }, "devDependencies": { diff --git a/pagx/viewer/script/cmake.js b/pagx/viewer/script/cmake.js index f7fac18971..2e6f8d5cf3 100644 --- a/pagx/viewer/script/cmake.js +++ b/pagx/viewer/script/cmake.js @@ -47,18 +47,3 @@ if (!existsSync(destDir)) { } copyFileSync(path.join(srcDir, 'pagx-viewer.js'), path.join(destDir, 'pagx-viewer.js')); copyFileSync(path.join(srcDir, 'pagx-viewer.wasm'), path.join(destDir, 'pagx-viewer.wasm')); - -// Copy font files to viewer/fonts/ directory -const fontSrcDir = path.resolve(__dirname, '../../../third_party/tgfx/resources/font'); -const fontDestDir = path.resolve(__dirname, '../fonts'); -if (!existsSync(fontDestDir)) { - mkdirSync(fontDestDir, { recursive: true }); -} -const fontFiles = ['NotoSansSC-Regular.otf', 'NotoColorEmoji.ttf']; -for (const fontFile of fontFiles) { - const srcPath = path.join(fontSrcDir, fontFile); - if (existsSync(srcPath)) { - copyFileSync(srcPath, path.join(fontDestDir, fontFile)); - console.log(`Copied font: ${fontFile}`); - } -} diff --git a/pagx/viewer/script/publish.cjs b/pagx/viewer/script/publish.cjs new file mode 100644 index 0000000000..508ce3970f --- /dev/null +++ b/pagx/viewer/script/publish.cjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node +/** + * PAGX Viewer Publisher + * + * Copies the PAGX Viewer build output to the public directory. + * Requires release build (npm run build:release). + * + * Source files: + * index.html + * index.css + * wasm-mt/ + * ../../resources/font/ (from libpag root) + * + * Output structure: + * ../public/viewer/index.html + * ../public/viewer/index.css + * ../public/viewer/fonts/ + * ../public/viewer/wasm-mt/ + * + * Usage: + * npm run publish + */ + +const fs = require('fs'); +const path = require('path'); + +// Default paths +const SCRIPT_DIR = __dirname; +const VIEWER_DIR = path.dirname(SCRIPT_DIR); +const PAGX_DIR = path.dirname(VIEWER_DIR); +const LIBPAG_DIR = path.dirname(PAGX_DIR); +const RESOURCES_FONT_DIR = path.join(LIBPAG_DIR, 'resources', 'font'); +const PUBLIC_DIR = path.join(PAGX_DIR, 'public'); +const OUTPUT_DIR = path.join(PUBLIC_DIR, 'viewer'); + +/** + * Copy a file from source to destination. + */ +function copyFile(src, dest) { + const destDir = path.dirname(dest); + fs.mkdirSync(destDir, { recursive: true }); + fs.copyFileSync(src, dest); + console.log(` Copied: ${path.relative(VIEWER_DIR, dest)}`); +} + +/** + * Check if the build is a release build (minified, no sourcemaps). + */ +function checkReleaseBuild(wasmDir) { + const indexJs = path.join(wasmDir, 'index.js'); + + // Check if index.js exists + if (!fs.existsSync(indexJs)) { + return { ok: false, reason: 'wasm-mt/index.js not found. Please run "npm run build:release" first.' }; + } + + // Check if sourcemap exists (should not exist in release) + const mapFile = path.join(wasmDir, 'index.js.map'); + if (fs.existsSync(mapFile)) { + return { ok: false, reason: 'Sourcemap file found. Please run "npm run build:release" first.' }; + } + + // Check if JS is minified (first line should be very long) + const content = fs.readFileSync(indexJs, 'utf-8'); + const firstLine = content.split('\n')[0]; + if (firstLine.length < 1000) { + return { ok: false, reason: 'JS file does not appear to be minified. Please run "npm run build:release" first.' }; + } + + return { ok: true }; +} + +/** + * Main function. + */ +function main() { + console.log('Publishing PAGX Viewer...\n'); + + // Check if viewer build exists + const wasmDir = path.join(VIEWER_DIR, 'wasm-mt'); + if (!fs.existsSync(wasmDir)) { + console.error('Error: Viewer build not found. Run "npm run build:release" first.'); + process.exit(1); + } + + // Check if it's a release build + const releaseCheck = checkReleaseBuild(wasmDir); + if (!releaseCheck.ok) { + console.error(`Error: ${releaseCheck.reason}`); + process.exit(1); + } + + // Copy index.html + copyFile( + path.join(VIEWER_DIR, 'index.html'), + path.join(OUTPUT_DIR, 'index.html') + ); + + // Copy index.css + copyFile( + path.join(VIEWER_DIR, 'index.css'), + path.join(OUTPUT_DIR, 'index.css') + ); + + // Copy fonts from resources/font + console.log('\n Copying fonts...'); + copyFile( + path.join(RESOURCES_FONT_DIR, 'NotoSansSC-Regular.otf'), + path.join(OUTPUT_DIR, 'fonts', 'NotoSansSC-Regular.otf') + ); + copyFile( + path.join(RESOURCES_FONT_DIR, 'NotoColorEmoji.ttf'), + path.join(OUTPUT_DIR, 'fonts', 'NotoColorEmoji.ttf') + ); + + // Copy wasm-mt directory + console.log('\n Copying wasm-mt...'); + const wasmOutputDir = path.join(OUTPUT_DIR, 'wasm-mt'); + copyFile( + path.join(wasmDir, 'index.js'), + path.join(wasmOutputDir, 'index.js') + ); + copyFile( + path.join(wasmDir, 'pagx-viewer.js'), + path.join(wasmOutputDir, 'pagx-viewer.js') + ); + copyFile( + path.join(wasmDir, 'pagx-viewer.wasm'), + path.join(wasmOutputDir, 'pagx-viewer.wasm') + ); + + console.log('\nDone!'); +} + +main(); diff --git a/pagx/viewer/server.js b/pagx/viewer/server.js index 519528c4e5..b79b3d010b 100644 --- a/pagx/viewer/server.js +++ b/pagx/viewer/server.js @@ -23,6 +23,7 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const libpagDir = path.resolve(__dirname, '../..'); const app = express(); @@ -33,6 +34,9 @@ app.use((req, res, next) => { next(); }); +// Map /fonts to resources/font directory +app.use('/fonts', express.static(path.join(libpagDir, 'resources', 'font'))); + app.use('', express.static(__dirname, { setHeaders: (res, filePath) => { if (filePath.endsWith('.wasm')) { From 33052e42b5431d699ecb876be05497e1c2d6a327 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 14:21:18 +0800 Subject: [PATCH 237/678] Fix Typesetter overriding Text position unconditionally. --- pagx/src/tgfx/Typesetter.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index f20f821f7e..86dc806299 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -245,9 +245,13 @@ class TypesetterImpl : public Typesetter { float xOffset = 0; if (textLayout != nullptr) { xOffset = calculateLayoutOffset(textLayout, shapedInfo.totalWidth); - // Apply TextLayout position to Text position - text->position.x = textLayout->position.x; - text->position.y = textLayout->position.y; + // Apply TextLayout position to Text position only if TextLayout has a non-zero position. + // This allows SVG-imported text (where position is on Text) to work correctly while + // also supporting PAGX XML format (where position is on TextLayout). + if (textLayout->position.x != 0 || textLayout->position.y != 0) { + text->position.x = textLayout->position.x; + text->position.y = textLayout->position.y; + } } // Create GlyphRun with layout offset baked in From d79f49110beaaa845fc4cf25a31ee9bc263db1ff Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 14:31:26 +0800 Subject: [PATCH 238/678] Fix Typesetter text layout offset calculation and use incremental font IDs. --- pagx/src/tgfx/Typesetter.cpp | 81 ++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index 86dc806299..d1f0833e8a 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -241,21 +241,29 @@ class TypesetterImpl : public Typesetter { continue; } - // Calculate layout offset based on TextLayout - float xOffset = 0; + // Calculate alignment offset from TextLayout + float alignOffset = 0; + float positionOffsetX = 0; + float positionOffsetY = 0; + if (textLayout != nullptr) { - xOffset = calculateLayoutOffset(textLayout, shapedInfo.totalWidth); - // Apply TextLayout position to Text position only if TextLayout has a non-zero position. - // This allows SVG-imported text (where position is on Text) to work correctly while - // also supporting PAGX XML format (where position is on TextLayout). + alignOffset = calculateLayoutOffset(textLayout, shapedInfo.totalWidth); + + // If TextLayout has a non-zero position, it overrides Text.position. + // Calculate the offset needed to move from Text.position to TextLayout.position. if (textLayout->position.x != 0 || textLayout->position.y != 0) { - text->position.x = textLayout->position.x; - text->position.y = textLayout->position.y; + positionOffsetX = textLayout->position.x - text->position.x; + positionOffsetY = textLayout->position.y - text->position.y; } } - // Create GlyphRun with layout offset baked in - createGlyphRun(text, shapedInfo, xOffset); + // GlyphRun positions are relative to Text.position (applied by LayerBuilder). + // We only need to include alignment offset and any position override from TextLayout. + float xOffset = alignOffset + positionOffsetX; + float yOffset = positionOffsetY; + + // Create GlyphRun with calculated offsets + createGlyphRun(text, shapedInfo, xOffset, yOffset); _textTypeset = true; } } @@ -356,7 +364,7 @@ class TypesetterImpl : public Typesetter { return info; } - void createGlyphRun(Text* text, const ShapedTextInfo& info, float xOffset) { + void createGlyphRun(Text* text, const ShapedTextInfo& info, float xOffset, float yOffset) { if (info.originalGlyphIDs.empty()) { return; } @@ -384,8 +392,8 @@ class TypesetterImpl : public Typesetter { glyphRun->xPositions.push_back(x + xOffset); } - // For horizontal text, y should be 0 (relative to baseline) - glyphRun->y = 0; + // Apply y offset (for TextLayout position override) + glyphRun->y = yOffset; text->glyphRuns.push_back(glyphRun); } @@ -435,22 +443,39 @@ class TypesetterImpl : public Typesetter { std::string getOrCreateFontResource(const std::shared_ptr& typeface, const tgfx::Font& font, const std::vector& glyphIDs) { - // Create unique font ID based on typeface identity - std::string fontId = "font_" + std::to_string(reinterpret_cast(typeface.get())); - - // Check if font resource already exists - auto it = _fontResources.find(fontId); - if (it == _fontResources.end()) { - // Create new Font resource with the ID so it gets registered in nodeMap - auto fontNode = _document->makeNode(fontId); - _fontResources[fontId] = fontNode; - _glyphMapping[fontId] = {}; + // Create a key combining typeface pointer and font size for lookup. + // Different font sizes produce different glyph paths, so they need separate Font resources. + std::string lookupKey = std::to_string(reinterpret_cast(typeface.get())) + "_" + + std::to_string(static_cast(font.getSize())); + + // Check if we already have a font ID for this key + auto keyIt = _fontKeyToId.find(lookupKey); + if (keyIt != _fontKeyToId.end()) { + // Font resource already exists, just add any new glyphs + std::string fontId = keyIt->second; + addGlyphsToFont(fontId, font, glyphIDs); + return fontId; } + // Create new font ID using incremental counter + std::string fontId = "font_" + std::to_string(_nextFontId++); + _fontKeyToId[lookupKey] = fontId; + + // Create new Font resource with the ID so it gets registered in nodeMap + auto fontNode = _document->makeNode(fontId); + _fontResources[fontId] = fontNode; + _glyphMapping[fontId] = {}; + + // Add glyphs to the new font + addGlyphsToFont(fontId, font, glyphIDs); + return fontId; + } + + void addGlyphsToFont(const std::string& fontId, const tgfx::Font& font, + const std::vector& glyphIDs) { Font* fontNode = _fontResources[fontId]; auto& glyphMap = _glyphMapping[fontId]; - // Add any new glyphs for (tgfx::GlyphID glyphID : glyphIDs) { if (glyphMap.find(glyphID) != glyphMap.end()) { continue; // Already added @@ -478,8 +503,6 @@ class TypesetterImpl : public Typesetter { fontNode->glyphs.push_back(glyph); } - - return fontId; } // Registered typefaces by family + style @@ -500,6 +523,12 @@ class TypesetterImpl : public Typesetter { // Font ID -> (original GlyphID -> new GlyphID) std::unordered_map> _glyphMapping = {}; + + // Lookup key (typeface_ptr + font_size) -> Font ID + std::unordered_map _fontKeyToId = {}; + + // Counter for generating incremental font IDs + int _nextFontId = 0; }; std::shared_ptr Typesetter::Make() { From 8102568c387d01f5ce001f47d7a53e6878b99d7f Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 14:33:15 +0800 Subject: [PATCH 239/678] Fix inconsistencies in Chinese spec document. --- pagx/spec/pagx_spec.zh_CN.md | 7 ++++--- pagx/src/tgfx/Typesetter.cpp | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index 9d6e92e82f..c698b1f764 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -10,7 +10,7 @@ - **特性完备**:完整覆盖矢量图形、图片、富文本、滤镜效果、混合模式、遮罩等能力,满足复杂动效的描述需求。 -- **精简高效**:提供简洁且强大的统一结构,兼顾静态的优化描述,同时预留未来交互和脚本的扩展能力。 +- **精简高效**:提供简洁且强大的统一结构,兼顾静态矢量与动画的优化描述,同时预留未来交互和脚本的扩展能力。 - **生态兼容**:可作为 After Effects、Figma、腾讯设计等设计工具的通用交换格式,实现设计资产无缝流转。 @@ -426,7 +426,8 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | `image` | idref | (必填) | 图片引用 "@id" | | `tileModeX` | TileMode | clamp | X 方向平铺模式 | | `tileModeY` | TileMode | clamp | Y 方向平铺模式 | -| `filterMode` | FilterMode | linear | 纹理滤镜模式 | +| `minFilterMode` | FilterMode | linear | 纹理缩小时的滤镜模式 | +| `magFilterMode` | FilterMode | linear | 纹理放大时的滤镜模式 | | `mipmapMode` | MipmapMode | linear | 多级渐远纹理模式 | | `matrix` | string | 单位矩阵 | 变换矩阵 | @@ -2799,5 +2800,5 @@ Layer / Group | **SelectorMode** | `add`, `subtract`, `intersect`, `min`, `max`, `difference` | | **TextAlign** | `start`, `center`, `end`, `justify` | | **VerticalAlign** | `top`, `center`, `bottom` | -| **TextDirection** | `horizontal`, `vertical` | +| **WritingMode** | `horizontal`, `vertical` | | **RepeaterOrder** | `belowOriginal`, `aboveOriginal` | diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index d1f0833e8a..cfb21e5835 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -348,10 +348,13 @@ class TypesetterImpl : public Typesetter { // Get glyph advance first (needed for spacing even if no outline) float advance = info.font.getAdvance(glyphID); - // Check if glyph has an outline (skip space-like characters without outlines) + // Check if glyph has renderable content (outline or image) + // Skip space-like characters that have no visual representation tgfx::Path testPath; bool hasOutline = info.font.getPath(glyphID, &testPath) && !testPath.isEmpty(); - if (hasOutline) { + bool hasImage = info.font.getImage(glyphID, nullptr, nullptr) != nullptr; + + if (hasOutline || hasImage) { info.xPositions.push_back(currentX); info.originalGlyphIDs.push_back(glyphID); } From a15e5e7c9da6fa64ff1320d849a80fb6525c35da Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 14:44:43 +0800 Subject: [PATCH 240/678] Add bitmap glyph support for emoji and reset font state between typeset calls. --- pagx/src/tgfx/Typesetter.cpp | 65 ++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index cfb21e5835..da4eb1e8ff 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -23,9 +23,12 @@ #include "pagx/nodes/Composition.h" #include "pagx/nodes/Font.h" #include "pagx/nodes/Group.h" +#include "pagx/nodes/Image.h" #include "pagx/nodes/Text.h" #include "pagx/nodes/TextLayout.h" +#include "tgfx/core/Bitmap.h" #include "tgfx/core/Font.h" +#include "tgfx/core/ImageCodec.h" #include "tgfx/core/Path.h" #include "tgfx/core/PathTypes.h" @@ -120,6 +123,8 @@ class TypesetterImpl : public Typesetter { _textTypeset = false; _fontResources.clear(); _glyphMapping.clear(); + _fontKeyToId.clear(); + _nextFontId = 0; // Process all layers for (auto& layer : _document->layers) { @@ -484,27 +489,61 @@ class TypesetterImpl : public Typesetter { continue; // Already added } - // Get glyph path + // Try to get glyph path first (vector outline) tgfx::Path glyphPath; - if (!font.getPath(glyphID, &glyphPath)) { - continue; // No outline (e.g., color emoji) - } + bool hasPath = font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); - // Convert path to SVG string - std::string pathStr = PathToSVGString(glyphPath); - if (pathStr.empty()) { - continue; + // Try to get glyph image (bitmap, e.g., color emoji) + tgfx::Matrix imageMatrix; + auto imageCodec = font.getImage(glyphID, nullptr, &imageMatrix); + + if (!hasPath && !imageCodec) { + continue; // No renderable content } // Create Glyph node auto glyph = _document->makeNode(); - glyph->path = _document->makeNode(); - *glyph->path = PathDataFromSVGString(pathStr); - // Map original glyph ID to new index (1-based, since PathTypefaceBuilder uses 1-based IDs) - glyphMap[glyphID] = static_cast(fontNode->glyphs.size() + 1); + if (hasPath) { + // Vector glyph + std::string pathStr = PathToSVGString(glyphPath); + if (!pathStr.empty()) { + glyph->path = _document->makeNode(); + *glyph->path = PathDataFromSVGString(pathStr); + } + } else if (imageCodec) { + // Bitmap glyph (e.g., color emoji) + // Read pixels from the codec and re-encode as PNG + int w = imageCodec->width(); + int h = imageCodec->height(); + if (w > 0 && h > 0) { + tgfx::Bitmap bitmap(w, h, false, false); + if (!bitmap.isEmpty()) { + auto* pixels = bitmap.lockPixels(); + if (pixels && imageCodec->readPixels(bitmap.info(), pixels)) { + bitmap.unlockPixels(); + auto pngData = bitmap.encode(tgfx::EncodedFormat::PNG, 100); + if (pngData) { + auto image = _document->makeNode(); + image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); + glyph->image = image; + // Store the offset from the image matrix (translation component) + glyph->offset.x = imageMatrix.getTranslateX(); + glyph->offset.y = imageMatrix.getTranslateY(); + } + } else { + bitmap.unlockPixels(); + } + } + } + } - fontNode->glyphs.push_back(glyph); + // Only add glyph if it has content + if (glyph->path || glyph->image) { + // Map original glyph ID to new index (1-based, since PathTypefaceBuilder uses 1-based IDs) + glyphMap[glyphID] = static_cast(fontNode->glyphs.size() + 1); + fontNode->glyphs.push_back(glyph); + } } } From 0f95ebcf1ac35d0f36db643badc9d10746731986 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 14:50:48 +0800 Subject: [PATCH 241/678] Add character-level font fallback support for emoji rendering. --- pagx/src/tgfx/Typesetter.cpp | 138 +++++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index da4eb1e8ff..5713b05d9b 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -84,14 +84,19 @@ struct FontKeyHash { } }; +// Shaped glyph run for a specific font +struct ShapedGlyphRun { + std::shared_ptr typeface = nullptr; + tgfx::Font font = {}; + std::vector glyphIDs = {}; + std::vector xPositions = {}; +}; + // Intermediate shaping result for a single Text element struct ShapedTextInfo { Text* text = nullptr; - std::vector originalGlyphIDs = {}; - std::vector xPositions = {}; + std::vector runs = {}; float totalWidth = 0; - std::shared_ptr typeface = nullptr; - tgfx::Font font = {}; }; // Private implementation class @@ -242,7 +247,7 @@ class TypesetterImpl : public Typesetter { auto* text = textElements[i]; auto& shapedInfo = shapedInfos[i]; - if (shapedInfo.originalGlyphIDs.empty()) { + if (shapedInfo.runs.empty()) { continue; } @@ -267,8 +272,8 @@ class TypesetterImpl : public Typesetter { float xOffset = alignOffset + positionOffsetX; float yOffset = positionOffsetY; - // Create GlyphRun with calculated offsets - createGlyphRun(text, shapedInfo, xOffset, yOffset); + // Create GlyphRuns for each shaped run (different fonts) + createGlyphRuns(text, shapedInfo, xOffset, yOffset); _textTypeset = true; } } @@ -296,18 +301,20 @@ class TypesetterImpl : public Typesetter { ShapedTextInfo info = {}; info.text = text; - // Find typeface for this text - auto typeface = findTypeface(text->fontFamily, text->fontStyle); - if (!typeface) { + // Find primary typeface for this text + auto primaryTypeface = findTypeface(text->fontFamily, text->fontStyle); + if (!primaryTypeface) { return info; } - info.typeface = typeface; - info.font = tgfx::Font(typeface, text->fontSize); - + tgfx::Font primaryFont(primaryTypeface, text->fontSize); float currentX = 0; const std::string& content = text->text; + // Current run being built + ShapedGlyphRun* currentRun = nullptr; + std::shared_ptr currentTypeface = nullptr; + // Simple text shaping: iterate through characters size_t i = 0; while (i < content.size()) { @@ -344,24 +351,57 @@ class TypesetterImpl : public Typesetter { continue; } - // Get glyph ID - tgfx::GlyphID glyphID = info.font.getGlyphID(unichar); - if (glyphID == 0) { + // Try to find a typeface that can render this character + std::shared_ptr glyphTypeface = nullptr; + tgfx::Font glyphFont; + tgfx::GlyphID glyphID = 0; + + // First try primary font + glyphID = primaryFont.getGlyphID(unichar); + if (glyphID != 0) { + glyphTypeface = primaryTypeface; + glyphFont = primaryFont; + } else { + // Try fallback typefaces + for (const auto& fallback : _fallbackTypefaces) { + if (!fallback || fallback == primaryTypeface) { + continue; + } + tgfx::Font fallbackFont(fallback, text->fontSize); + glyphID = fallbackFont.getGlyphID(unichar); + if (glyphID != 0) { + glyphTypeface = fallback; + glyphFont = fallbackFont; + break; + } + } + } + + if (glyphID == 0 || !glyphTypeface) { + // No font can render this character, skip it continue; } - // Get glyph advance first (needed for spacing even if no outline) - float advance = info.font.getAdvance(glyphID); + // Get glyph advance + float advance = glyphFont.getAdvance(glyphID); // Check if glyph has renderable content (outline or image) - // Skip space-like characters that have no visual representation tgfx::Path testPath; - bool hasOutline = info.font.getPath(glyphID, &testPath) && !testPath.isEmpty(); - bool hasImage = info.font.getImage(glyphID, nullptr, nullptr) != nullptr; + bool hasOutline = glyphFont.getPath(glyphID, &testPath) && !testPath.isEmpty(); + bool hasImage = glyphFont.getImage(glyphID, nullptr, nullptr) != nullptr; if (hasOutline || hasImage) { - info.xPositions.push_back(currentX); - info.originalGlyphIDs.push_back(glyphID); + // Check if we need to start a new run (different typeface) + if (currentTypeface != glyphTypeface) { + info.runs.emplace_back(); + currentRun = &info.runs.back(); + currentRun->typeface = glyphTypeface; + currentRun->font = glyphFont; + currentTypeface = glyphTypeface; + } + + currentRun->xPositions.push_back(currentX); + currentRun->glyphIDs.push_back(glyphID); } // Always advance position @@ -372,38 +412,40 @@ class TypesetterImpl : public Typesetter { return info; } - void createGlyphRun(Text* text, const ShapedTextInfo& info, float xOffset, float yOffset) { - if (info.originalGlyphIDs.empty()) { - return; - } + void createGlyphRuns(Text* text, const ShapedTextInfo& info, float xOffset, float yOffset) { + for (const auto& run : info.runs) { + if (run.glyphIDs.empty()) { + continue; + } - // Get or create Font resource for this typeface - std::string fontId = getOrCreateFontResource(info.typeface, info.font, info.originalGlyphIDs); + // Get or create Font resource for this typeface + std::string fontId = getOrCreateFontResource(run.typeface, run.font, run.glyphIDs); - // Create GlyphRun with remapped glyph IDs - auto glyphRun = _document->makeNode(); - glyphRun->font = _fontResources[fontId]; + // Create GlyphRun with remapped glyph IDs + auto glyphRun = _document->makeNode(); + glyphRun->font = _fontResources[fontId]; - // Remap glyph IDs to font-specific indices - for (tgfx::GlyphID glyphID : info.originalGlyphIDs) { - auto it = _glyphMapping[fontId].find(glyphID); - if (it != _glyphMapping[fontId].end()) { - glyphRun->glyphs.push_back(it->second); - } else { - glyphRun->glyphs.push_back(0); // Missing glyph + // Remap glyph IDs to font-specific indices + for (tgfx::GlyphID glyphID : run.glyphIDs) { + auto it = _glyphMapping[fontId].find(glyphID); + if (it != _glyphMapping[fontId].end()) { + glyphRun->glyphs.push_back(it->second); + } else { + glyphRun->glyphs.push_back(0); // Missing glyph + } } - } - // Apply layout offset to x positions - glyphRun->xPositions.reserve(info.xPositions.size()); - for (float x : info.xPositions) { - glyphRun->xPositions.push_back(x + xOffset); - } + // Apply layout offset to x positions + glyphRun->xPositions.reserve(run.xPositions.size()); + for (float x : run.xPositions) { + glyphRun->xPositions.push_back(x + xOffset); + } - // Apply y offset (for TextLayout position override) - glyphRun->y = yOffset; + // Apply y offset (for TextLayout position override) + glyphRun->y = yOffset; - text->glyphRuns.push_back(glyphRun); + text->glyphRuns.push_back(glyphRun); + } } std::shared_ptr findTypeface(const std::string& fontFamily, From 3fb73661161317e56e4be258d41f8a7a06b701d9 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 14:52:15 +0800 Subject: [PATCH 242/678] Sync English spec with Chinese examples and detailed tables. --- pagx/spec/pagx_spec.md | 158 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 10 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 210cc60b49..f0b92244ab 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -1427,17 +1427,60 @@ Merges all shapes into a single shape. - Current transformation matrices of shapes are applied during merge - Merged shape's transformation matrix resets to identity matrix +**Example**: + +```xml + + + + +``` + ### 5.5 Text Modifiers Text modifiers transform individual glyphs within text. #### 5.5.1 Text Modifier Processing -When a text modifier is encountered, **all glyph lists** accumulated in the context are combined into a unified glyph list for the operation. +When a text modifier is encountered, **all glyph lists** accumulated in the context are combined into a unified glyph list for the operation: + +```xml + + + + + + +``` #### 5.5.2 Text to Shape Conversion -When text encounters a shape modifier, it is forcibly converted to shape paths. +When text encounters a shape modifier, it is forcibly converted to shape paths: + +``` +Text Element Shape Modifier Subsequent Modifiers +┌──────────┐ ┌──────────┐ +│ Text │ │ TrimPath │ +└────┬─────┘ │RoundCorn │ + │ │MergePath │ + │ Accumulated └────┬─────┘ + │ Glyph List │ + ▼ │ Triggers Conversion +┌──────────────┐ │ +│ Glyph List │───────────┼──────────────────────┐ +│ [H,e,l,l,o] │ │ │ +└──────────────┘ ▼ ▼ + ┌──────────────┐ ┌──────────────────┐ + │ Merged into │ │ Emoji Discarded │ + │ Single Path │ │ (Cannot convert) │ + └──────────────┘ └──────────────────┘ + │ + │ Subsequent text modifiers no longer effective + ▼ + ┌──────────────┐ + │ TextModifier │ → Skipped (Already Path) + └──────────────┘ +``` **Conversion Rules**: @@ -1446,6 +1489,17 @@ When text encounters a shape modifier, it is forcibly converted to shape paths. 3. **Emoji Loss**: Emoji cannot be converted to path outlines; discarded during conversion 4. **Irreversible Conversion**: After conversion becomes pure Path; subsequent text modifiers have no effect on it +**Example**: + +```xml + + + + + + +``` + #### 5.5.3 TextModifier Applies transforms and style overrides to glyphs within selected ranges. TextModifier may contain multiple RangeSelector child elements. @@ -1526,11 +1580,34 @@ Range selectors define the glyph range and influence degree for TextModifier. | `randomOrder` | bool | false | Random order | | `randomSeed` | int | 0 | Random seed | -**SelectorUnit**: `index`, `percentage` +**SelectorUnit**: + +| Value | Description | +|-------|-------------| +| `index` | Index: Calculate range by glyph index | +| `percentage` | Percentage: Calculate range by percentage of total glyphs | + +**SelectorShape**: + +| Value | Description | +|-------|-------------| +| `square` | Square: 1 within range, 0 outside | +| `rampUp` | Ramp Up: Linear increase from 0 to 1 | +| `rampDown` | Ramp Down: Linear decrease from 1 to 0 | +| `triangle` | Triangle: 1 at center, 0 at edges | +| `round` | Round: Sinusoidal transition | +| `smooth` | Smooth: Smoother transition curve | -**SelectorShape**: `square`, `rampUp`, `rampDown`, `triangle`, `round`, `smooth` +**SelectorMode**: -**SelectorMode**: `add`, `subtract`, `intersect`, `min`, `max`, `difference` +| Value | Description | +|-------|-------------| +| `add` | Add: Accumulate selector weights | +| `subtract` | Subtract: Subtract selector weights | +| `intersect` | Intersect: Take minimum weight | +| `min` | Min: Take minimum value | +| `max` | Max: Take maximum value | +| `difference` | Difference: Take absolute difference | #### 5.5.5 TextPath @@ -1571,10 +1648,27 @@ Arranges text along a specified path. #### 5.5.6 TextLayout -TextLayout is a text layout modifier that applies typography to accumulated Text elements. +TextLayout is a text layout modifier that applies typography to accumulated Text elements. It overrides the original positions of Text elements (similar to how TextPath overrides positions). Two modes are supported: + +- **Point Text Mode** (no width): Text does not auto-wrap; textAlign controls text alignment relative to the (x, y) anchor point +- **Paragraph Text Mode** (with width): Text auto-wraps within the specified width + +During rendering, an attached text typesetting module performs pre-layout, recalculating each glyph's position. TextLayout is expanded during pre-layout, with glyph positions written directly into Text. ```xml - + + + + + + + + + + + + + ``` | Attribute | Type | Default | Description | @@ -1587,16 +1681,60 @@ TextLayout is a text layout modifier that applies typography to accumulated Text | `writingMode` | WritingMode | horizontal | Layout direction | | `lineHeight` | float | 1.2 | Line height multiplier | -**TextAlign**: `start`, `center`, `end`, `justify` +**TextAlign (Horizontal Alignment)**: + +| Value | Description | +|-------|-------------| +| `start` | Start alignment (left-aligned; right-aligned for RTL text) | +| `center` | Center alignment | +| `end` | End alignment (right-aligned; left-aligned for RTL text) | +| `justify` | Justified (last line start-aligned) | -**VerticalAlign**: `top`, `center`, `bottom` +**VerticalAlign (Vertical Alignment)**: -**WritingMode**: `horizontal`, `vertical` +| Value | Description | +|-------|-------------| +| `top` | Top alignment | +| `center` | Vertical center | +| `bottom` | Bottom alignment | + +**WritingMode (Layout Direction)**: + +| Value | Description | +|-------|-------------| +| `horizontal` | Horizontal text | +| `vertical` | Vertical text (columns arranged right-to-left, traditional CJK vertical layout) | #### 5.5.7 Rich Text Rich text is achieved through multiple Text elements within a Group, each Text having independent Fill/Stroke styles. TextLayout provides unified typography. +```xml + + + + + + + + + + + + + + + + + + + + + + + +``` + ### 5.6 Repeater Repeater duplicates accumulated content and rendered styles, applying progressive transforms to each copy. From 27f6103ec5233d5f67c68fefedc7d3d20d88e434 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 14:56:47 +0800 Subject: [PATCH 243/678] Fix English spec to align with Chinese version. --- pagx/spec/pagx_spec.md | 52 +++++++++++++++++++++++++----------- pagx/spec/pagx_spec.zh_CN.md | 6 ++--- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index f0b92244ab..796f2c6983 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -426,8 +426,7 @@ Image patterns use an image as a color source. | `image` | idref | (required) | Image reference "@id" | | `tileModeX` | TileMode | clamp | X-direction tile mode | | `tileModeY` | TileMode | clamp | Y-direction tile mode | -| `minFilterMode` | FilterMode | linear | Texture filter mode for minification | -| `magFilterMode` | FilterMode | linear | Texture filter mode for magnification | +| `filterMode` | FilterMode | linear | Texture filter mode | | `mipmapMode` | MipmapMode | linear | Mipmap mode | | `matrix` | string | identity matrix | Transform matrix | @@ -1611,10 +1610,14 @@ Range selectors define the glyph range and influence degree for TextModifier. #### 5.5.5 TextPath -Arranges text along a specified path. +Arranges text along a specified path. The path can reference a PathData defined in Resources, or use inline path data. ```xml + + + + ``` | Attribute | Type | Default | Description | @@ -1716,28 +1719,37 @@ Rich text is achieved through multiple Text elements within a Group, each Text h - + - - + + - + - + + - - + + + - - + + + + + + + ``` +**Note**: Each Group's Text + Fill/Stroke defines a text segment with independent styling. TextLayout treats all segments as a whole for typography, enabling auto-wrapping and alignment. + ### 5.6 Repeater -Repeater duplicates accumulated content and rendered styles, applying progressive transforms to each copy. +Repeater duplicates accumulated content and rendered styles, applying progressive transforms to each copy. Repeater affects both Paths and glyph lists simultaneously, and does not trigger text-to-shape conversion. ```xml @@ -1800,6 +1812,15 @@ When `copies` is a decimal (e.g., `3.5`), partial copies are achieved through ** - **Preserve text attributes**: Glyph lists retain glyph information after copying; subsequent text modifiers can still affect them - **Copy rendered styles**: Also copies already rendered fills and strokes +```xml + + + + + + +``` + ### 5.7 Group Group is a VectorElement container with transform properties. @@ -2418,8 +2439,7 @@ Child elements: `ColorStop`+ | `image` | idref | (required) | | `tileModeX` | TileMode | clamp | | `tileModeY` | TileMode | clamp | -| `minFilterMode` | FilterMode | linear | -| `magFilterMode` | FilterMode | linear | +| `filterMode` | FilterMode | linear | | `mipmapMode` | MipmapMode | linear | | `matrix` | string | identity matrix | @@ -2447,7 +2467,7 @@ Child elements: `ColorStop`+ | `maskType` | MaskType | alpha | | `composition` | idref | - | -Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* +Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* (automatically categorized by type) ### C.4 Layer Style Nodes diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index c698b1f764..d41fc7d703 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -426,8 +426,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | `image` | idref | (必填) | 图片引用 "@id" | | `tileModeX` | TileMode | clamp | X 方向平铺模式 | | `tileModeY` | TileMode | clamp | Y 方向平铺模式 | -| `minFilterMode` | FilterMode | linear | 纹理缩小时的滤镜模式 | -| `magFilterMode` | FilterMode | linear | 纹理放大时的滤镜模式 | +| `filterMode` | FilterMode | linear | 纹理滤镜模式 | | `mipmapMode` | MipmapMode | linear | 多级渐远纹理模式 | | `matrix` | string | 单位矩阵 | 变换矩阵 | @@ -2440,8 +2439,7 @@ Layer / Group | `image` | idref | (必填) | | `tileModeX` | TileMode | clamp | | `tileModeY` | TileMode | clamp | -| `minFilterMode` | FilterMode | linear | -| `magFilterMode` | FilterMode | linear | +| `filterMode` | FilterMode | linear | | `mipmapMode` | MipmapMode | linear | | `matrix` | string | 单位矩阵 | From b41c62256baaf5b7b8076c26b62c97158727212f Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 15:00:48 +0800 Subject: [PATCH 244/678] Scale emoji glyph image to target font size during typesetting. --- pagx/src/tgfx/Typesetter.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index 5713b05d9b..fd7a4903ca 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -555,13 +555,19 @@ class TypesetterImpl : public Typesetter { } } else if (imageCodec) { // Bitmap glyph (e.g., color emoji) - // Read pixels from the codec and re-encode as PNG - int w = imageCodec->width(); - int h = imageCodec->height(); - if (w > 0 && h > 0) { - tgfx::Bitmap bitmap(w, h, false, false); + // The imageMatrix contains scale and translation to transform the glyph image. + // We scale the image to the target size during encoding, so only offset is needed at render. + int srcW = imageCodec->width(); + int srcH = imageCodec->height(); + float scaleX = imageMatrix.getScaleX(); + float scaleY = imageMatrix.getScaleY(); + int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); + int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); + if (dstW > 0 && dstH > 0) { + tgfx::Bitmap bitmap(dstW, dstH, false, false); if (!bitmap.isEmpty()) { auto* pixels = bitmap.lockPixels(); + // readPixels supports downscaling when dstInfo size differs from codec size if (pixels && imageCodec->readPixels(bitmap.info(), pixels)) { bitmap.unlockPixels(); auto pngData = bitmap.encode(tgfx::EncodedFormat::PNG, 100); @@ -569,7 +575,7 @@ class TypesetterImpl : public Typesetter { auto image = _document->makeNode(); image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); glyph->image = image; - // Store the offset from the image matrix (translation component) + // Store offset; scale is already applied to the image glyph->offset.x = imageMatrix.getTranslateX(); glyph->offset.y = imageMatrix.getTranslateY(); } From c8f3ff127c84e3a139b9748d4c8082617ac9eced Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 15:06:20 +0800 Subject: [PATCH 245/678] Use bilinear interpolation for smooth emoji image downscaling. --- pagx/src/tgfx/Typesetter.cpp | 96 ++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index fd7a4903ca..cd54776789 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -17,6 +17,8 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "pagx/Typesetter.h" +#include +#include #include #include #include "SVGPathParser.h" @@ -34,6 +36,55 @@ namespace pagx { +// Scales RGBA8888 pixels using bilinear interpolation for smooth downscaling. +static void ScalePixelsBilinear(const uint8_t* srcPixels, int srcW, int srcH, size_t srcRowBytes, + uint8_t* dstPixels, int dstW, int dstH, size_t dstRowBytes) { + float scaleX = static_cast(srcW) / static_cast(dstW); + float scaleY = static_cast(srcH) / static_cast(dstH); + + for (int y = 0; y < dstH; y++) { + float srcY = (static_cast(y) + 0.5f) * scaleY - 0.5f; + int y0 = static_cast(std::floor(srcY)); + int y1 = y0 + 1; + float fy = srcY - static_cast(y0); + + y0 = std::max(0, std::min(y0, srcH - 1)); + y1 = std::max(0, std::min(y1, srcH - 1)); + + auto* dstRow = dstPixels + y * dstRowBytes; + + for (int x = 0; x < dstW; x++) { + float srcX = (static_cast(x) + 0.5f) * scaleX - 0.5f; + int x0 = static_cast(std::floor(srcX)); + int x1 = x0 + 1; + float fx = srcX - static_cast(x0); + + x0 = std::max(0, std::min(x0, srcW - 1)); + x1 = std::max(0, std::min(x1, srcW - 1)); + + // Sample 4 neighboring pixels + const auto* p00 = srcPixels + y0 * srcRowBytes + x0 * 4; + const auto* p10 = srcPixels + y0 * srcRowBytes + x1 * 4; + const auto* p01 = srcPixels + y1 * srcRowBytes + x0 * 4; + const auto* p11 = srcPixels + y1 * srcRowBytes + x1 * 4; + + // Bilinear interpolation for each channel (RGBA) + for (int c = 0; c < 4; c++) { + float v00 = static_cast(p00[c]); + float v10 = static_cast(p10[c]); + float v01 = static_cast(p01[c]); + float v11 = static_cast(p11[c]); + + float v0 = v00 + (v10 - v00) * fx; + float v1 = v01 + (v11 - v01) * fx; + float v = v0 + (v1 - v0) * fy; + + dstRow[x * 4 + c] = static_cast(std::round(std::max(0.0f, std::min(255.0f, v)))); + } + } + } +} + // Converts a tgfx::Path to SVG path string static std::string PathToSVGString(const tgfx::Path& path) { std::string result = {}; @@ -564,23 +615,38 @@ class TypesetterImpl : public Typesetter { int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); if (dstW > 0 && dstH > 0) { - tgfx::Bitmap bitmap(dstW, dstH, false, false); - if (!bitmap.isEmpty()) { - auto* pixels = bitmap.lockPixels(); - // readPixels supports downscaling when dstInfo size differs from codec size - if (pixels && imageCodec->readPixels(bitmap.info(), pixels)) { - bitmap.unlockPixels(); - auto pngData = bitmap.encode(tgfx::EncodedFormat::PNG, 100); - if (pngData) { - auto image = _document->makeNode(); - image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); - glyph->image = image; - // Store offset; scale is already applied to the image - glyph->offset.x = imageMatrix.getTranslateX(); - glyph->offset.y = imageMatrix.getTranslateY(); + // Read original pixels first + tgfx::Bitmap srcBitmap(srcW, srcH, false, false); + if (!srcBitmap.isEmpty()) { + auto* srcPixels = srcBitmap.lockPixels(); + if (srcPixels && imageCodec->readPixels(srcBitmap.info(), srcPixels)) { + srcBitmap.unlockPixels(); + // Scale using bilinear interpolation for smooth downscaling + tgfx::Bitmap dstBitmap(dstW, dstH, false, false); + if (!dstBitmap.isEmpty()) { + auto* dstPixels = dstBitmap.lockPixels(); + if (dstPixels) { + auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); + ScalePixelsBilinear(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), + static_cast(dstPixels), dstW, dstH, + dstBitmap.info().rowBytes()); + srcBitmap.unlockPixels(); + dstBitmap.unlockPixels(); + auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); + if (pngData) { + auto image = _document->makeNode(); + image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); + glyph->image = image; + // Store offset; scale is already applied to the image + glyph->offset.x = imageMatrix.getTranslateX(); + glyph->offset.y = imageMatrix.getTranslateY(); + } + } else { + dstBitmap.unlockPixels(); + } } } else { - bitmap.unlockPixels(); + srcBitmap.unlockPixels(); } } } From 731627abbdac6284cca48dd5b70c2a3120b97f16 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Wed, 28 Jan 2026 17:22:00 +0800 Subject: [PATCH 246/678] Add performance monitoring panel to WeChat demo and optimize tile count. --- pagx/wechat/server.js | 48 ----------- pagx/wechat/src/PAGXView.cpp | 2 +- pagx/wechat/wx_demo/pages/viewer/viewer.js | 86 ++++++++++++++++++-- pagx/wechat/wx_demo/pages/viewer/viewer.wxml | 29 +++++++ pagx/wechat/wx_demo/pages/viewer/viewer.wxss | 82 +++++++++++++++++++ 5 files changed, 193 insertions(+), 54 deletions(-) delete mode 100644 pagx/wechat/server.js diff --git a/pagx/wechat/server.js b/pagx/wechat/server.js deleted file mode 100644 index 13bfd700af..0000000000 --- a/pagx/wechat/server.js +++ /dev/null @@ -1,48 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making tgfx available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at -// -// https://opensource.org/licenses/BSD-3-Clause -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -import express from 'express'; -import path from 'path'; -import { exec } from 'child_process'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const app = express(); - -// Enable SharedArrayBuffer -app.use((req, res, next) => { - res.set('Cross-Origin-Opener-Policy', 'same-origin'); - res.set('Cross-Origin-Embedder-Policy', 'require-corp'); - next(); -}); - -app.use('', express.static(__dirname)); - -app.get('/', (req, res) => { - res.redirect('/index.html'); -}); - -const port = 8082; -app.listen(port, () => { - const url = `http://localhost:${port}/`; - const start = (process.platform === 'darwin' ? 'open' : 'start'); - exec(start + ' ' + url); - console.log(`PAGX Viewer running at ${url}`); -}); diff --git a/pagx/wechat/src/PAGXView.cpp b/pagx/wechat/src/PAGXView.cpp index 811de13516..377e949181 100644 --- a/pagx/wechat/src/PAGXView.cpp +++ b/pagx/wechat/src/PAGXView.cpp @@ -64,7 +64,7 @@ PAGXView::PAGXView(std::shared_ptr device, int width, int height) : device(device), _width(width), _height(height) { displayList.setRenderMode(tgfx::RenderMode::Tiled); displayList.setAllowZoomBlur(true); - displayList.setMaxTileCount(512); + displayList.setMaxTileCount(256); } static std::vector> fallbackTypefaces; diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.js b/pagx/wechat/wx_demo/pages/viewer/viewer.js index 62eade60a3..6cf0600183 100644 --- a/pagx/wechat/wx_demo/pages/viewer/viewer.js +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.js @@ -7,21 +7,26 @@ import { PAGXInit, } from '../../utils/pagx-viewer'; import { WXGestureManager } from '../../utils/gesture-manager'; +import { PerformanceMonitor } from '../../utils/performance-monitor'; // PAGX sample files configuration const SAMPLE_FILES = [ { name: 'ColorPicker', - url: 'https://pag.io/pagx/testFiles/ColorPicker.libpag.pagx' + url: 'https://pag.io/pagx/testFiles/ColorPicker.pagx' }, { - name: 'complex7', - url: 'https://pag.io/pagx/testFiles/complex7.pagx' + name: 'Baseline', + url: 'https://pag.io/pagx/testFiles/Baseline.pagx' + }, + { + name: 'Guidelines', + url: 'https://pag.io/pagx/testFiles/Guidelines.pagx' }, { name: 'complex6', url: 'https://pag.io/pagx/testFiles/complex6.pagx' - } + }, ]; Page({ @@ -30,7 +35,18 @@ Page({ samples: SAMPLE_FILES, sampleNames: SAMPLE_FILES.map(item => item.name), currentIndex: 0, - loadingFile: false + loadingFile: false, + + // Performance monitoring + showPerf: false, + perfStats: { + fps: 0, + avgFPS: 0, + frameTime: 0, + dropRate: 0, + smoothness: 0, + quality: 'N/A' + } }, // State @@ -39,7 +55,9 @@ Page({ canvas: null, animationFrameId: 0, gestureManager: null, + perfMonitor: null, dpr: 2, + gestureJustStarted: false, async onLoad(options) { try { @@ -49,6 +67,12 @@ Page({ // Create gesture manager this.gestureManager = new WXGestureManager(); + // Create performance monitor + this.perfMonitor = new PerformanceMonitor(); + this.perfMonitor.onStatsUpdate = (stats) => { + this.updatePerfStats(stats); + }; + // Support custom file index from query params if (options && options.index) { const index = parseInt(options.index); @@ -269,8 +293,20 @@ Page({ startRendering() { const render = () => { if (!this.View) return; + this.View.draw(); + // Record frame for performance monitoring + if (this.perfMonitor && this.perfMonitor.enabled) { + this.perfMonitor.recordFrame(); + + // Mark first frame after gesture start + if (this.gestureJustStarted) { + this.perfMonitor.onGestureFirstFrame(); + this.gestureJustStarted = false; + } + } + // Use Canvas.requestAnimationFrame in WeChat Miniprogram if (this.canvas && this.canvas.requestAnimationFrame) { this.animationFrameId = this.canvas.requestAnimationFrame(render); @@ -296,6 +332,13 @@ Page({ // Touch Events onTouchStart(e) { if (!this.gestureManager) return; + + // Notify performance monitor + if (this.perfMonitor && this.perfMonitor.enabled) { + this.perfMonitor.onGestureStart(); + this.gestureJustStarted = true; + } + // Pass dpr to convert touch coordinates (logical pixels) to physical pixels const state = this.gestureManager.onTouchStart(e.touches, this.dpr); this.applyGestureState(state); @@ -313,6 +356,12 @@ Page({ // IMPORTANT: Pass e.touches (remaining touches), not e.changedTouches (ended touches) const state = this.gestureManager.onTouchEnd(e.touches); this.applyGestureState(state); + + // Notify performance monitor + if (this.perfMonitor && this.perfMonitor.enabled && e.touches.length === 0) { + this.perfMonitor.onGestureEnd(); + this.gestureJustStarted = false; + } }, // Reset Button @@ -337,4 +386,31 @@ Page({ this.switchFile(index); } }, + + // Performance Monitoring + togglePerfMonitor() { + const newShowPerf = !this.data.showPerf; + this.setData({ showPerf: newShowPerf }); + + if (newShowPerf && this.perfMonitor) { + this.perfMonitor.reset(); + this.perfMonitor.start(); + } else if (this.perfMonitor) { + this.perfMonitor.stop(); + this.perfMonitor.logStats(); + } + }, + + updatePerfStats(stats) { + this.setData({ + perfStats: { + fps: stats.currentFPS, + avgFPS: stats.averageFPS, + frameTime: stats.averageFrameTime, + dropRate: stats.droppedFrameRate, + smoothness: stats.smoothness, + quality: this.perfMonitor.getSummary().quality + } + }); + }, }); diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.wxml b/pagx/wechat/wx_demo/pages/viewer/viewer.wxml index faac3d4db2..1e26ec4287 100644 --- a/pagx/wechat/wx_demo/pages/viewer/viewer.wxml +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.wxml @@ -20,6 +20,10 @@ class="reset-btn" bindtap="onReset" >Reset + @@ -42,5 +46,30 @@ Switching... + + + + Performance Monitor + + Quality: + {{perfStats.quality}} + + + Smoothness: + {{perfStats.smoothness}}/100 + + + FPS: + {{perfStats.fps}} (avg: {{perfStats.avgFPS}}) + + + Frame Time: + {{perfStats.frameTime}}ms + + + Frame Drops: + {{perfStats.dropRate}}% + + diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.wxss b/pagx/wechat/wx_demo/pages/viewer/viewer.wxss index 035f6fb69c..75e5953014 100644 --- a/pagx/wechat/wx_demo/pages/viewer/viewer.wxss +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.wxss @@ -84,6 +84,28 @@ border: none; } +.perf-btn { + padding: 20rpx 35rpx; + font-size: 28rpx; + background: rgba(255, 255, 255, 0.1); + color: #999; + border: 1rpx solid rgba(255, 255, 255, 0.2); + border-radius: 8rpx; + flex-shrink: 0; + line-height: 1; + transition: all 0.3s; +} + +.perf-btn.active { + background: #1989fa; + color: #fff; + border-color: #1989fa; +} + +.perf-btn::after { + border: none; +} + .canvas-container { flex: 1; position: relative; @@ -129,3 +151,63 @@ border-radius: 4rpx; font-size: 24rpx; } + +.perf-panel { + position: absolute; + top: 20rpx; + left: 20rpx; + background: rgba(0, 0, 0, 0.85); + padding: 25rpx 30rpx; + border-radius: 12rpx; + border: 1rpx solid rgba(255, 255, 255, 0.2); + min-width: 350rpx; + backdrop-filter: blur(10rpx); +} + +.perf-title { + color: #fff; + font-size: 28rpx; + font-weight: bold; + margin-bottom: 15rpx; + border-bottom: 1rpx solid rgba(255, 255, 255, 0.2); + padding-bottom: 10rpx; +} + +.perf-row { + display: flex; + justify-content: space-between; + align-items: center; + margin: 12rpx 0; +} + +.perf-label { + color: #999; + font-size: 24rpx; +} + +.perf-value { + color: #fff; + font-size: 24rpx; + font-weight: 500; + font-family: 'Courier New', monospace; +} + +.quality-Excellent { + color: #52c41a; +} + +.quality-Good { + color: #73d13d; +} + +.quality-Fair { + color: #faad14; +} + +.quality-Poor { + color: #ff7a45; +} + +.quality-Very { + color: #f5222d; +} From 0d19f48901b63d95751b21c58f4e0b04ac04807a Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Wed, 28 Jan 2026 19:52:14 +0800 Subject: [PATCH 247/678] Remove zoom scale limits to allow unlimited zoom in and out. --- pagx/wechat/ts/gesture-manager.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pagx/wechat/ts/gesture-manager.ts b/pagx/wechat/ts/gesture-manager.ts index a5ae8ba8a8..45cf4990d0 100644 --- a/pagx/wechat/ts/gesture-manager.ts +++ b/pagx/wechat/ts/gesture-manager.ts @@ -16,8 +16,6 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -const MIN_ZOOM = 0.1; -const MAX_ZOOM = 10.0; const TAP_TIMEOUT = 300; const TAP_DISTANCE_THRESHOLD = 50; @@ -221,10 +219,7 @@ export class WXGestureManager { const currentDistance = Math.hypot(dx, dy); const scaleChange = currentDistance / this.initialDistance; - const newZoom = Math.max( - MIN_ZOOM, - Math.min(MAX_ZOOM, this.initialZoom * scaleChange) - ); + const newZoom = this.initialZoom * scaleChange; // Calculate current pinch center (may have moved during zoom) const currentPinchCenterX = (touches[0].x + touches[1].x) * 0.5 * dpr; From 96e55782220cbe6be3d49d331a8ee6c869118d38 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 22:31:07 +0800 Subject: [PATCH 248/678] Improve English expressions in PAGX specification for native readability. --- pagx/spec/pagx_spec.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 796f2c6983..294efac872 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -6,7 +6,7 @@ ### 1.1 Design Goals -- **Open and Readable**: Pure text XML format that is easy to read and edit, with native support for version control and diff comparison, facilitating debugging and AI comprehension and generation. +- **Open and Readable**: Plain-text XML format that is easy to read and edit, with native support for version control and diff comparison, facilitating debugging, AI understanding, and content generation. - **Feature Complete**: Comprehensive coverage of vector graphics, images, rich text, filter effects, blend modes, masks, and more to meet the requirements of complex animation descriptions. @@ -250,7 +250,7 @@ PAGX uses a standard 2D Cartesian coordinate system: | `width` | float | (required) | Canvas width | | `height` | float | (required) | Canvas height | -**Layer Rendering Order**: Layers are rendered sequentially in document order; layers appearing earlier in the document are rendered first (appearing below), while layers appearing later are rendered last (appearing above). +**Layer Rendering Order**: Layers are rendered sequentially in document order; layers earlier in the document render first (below); later layers render last (above). ### 3.3 Resources @@ -438,7 +438,7 @@ Image patterns use an image as a color source. ##### Color Source Coordinate System -Except for solid colors, all color sources (gradients, image patterns) have the concept of a coordinate system, which is **relative to the origin of the geometry element's local coordinate system**. The `matrix` attribute can be used to apply transforms to the color source coordinate system. +Except for solid colors, all color sources (gradients, image patterns) operate within a coordinate system **relative to the origin of the geometry element's local coordinate system**. The `matrix` attribute can be used to apply transforms to the color source coordinate system. **Transform Behavior**: @@ -501,7 +501,7 @@ Font defines embedded font resources containing subsetted glyph data (vector out ``` -**Consistency Constraint**: All Glyphs within the same Font must use the same type (all `path` or all `image`); mixing is not allowed. +**Consistency Constraint**: All Glyphs within the same Font must be of the same type—either all `path` or all `image`. Mixing is not allowed. **GlyphID Rules**: - **GlyphID starts from 1**: Glyph list index + 1 = GlyphID @@ -806,7 +806,7 @@ Key difference from layer styles (Section 4.3): Layer styles **independently ren #### 4.4.2 DropShadowFilter -Generates shadow effect based on filter input. Core difference from DropShadowStyle: The filter projects based on original rendering content and supports semi-transparency; the style projects based on opaque layer content. Additionally, the two support different attribute features. +Generates shadow effect based on filter input. Key difference from DropShadowStyle: the filter projects from original rendering content and preserves semi-transparency, while the style projects from opaque layer content. Additionally, the two support different attribute features. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| @@ -1360,7 +1360,7 @@ Trims paths to a specified start/end range. |-----------|------|---------|-------------| | `start` | float | 0 | Start position 0~1 | | `end` | float | 1 | End position 0~1 | -| `offset` | float | 0 | Offset (degrees); 360 degrees represents one cycle of the full path length | +| `offset` | float | 0 | Offset in degrees; 360° equals one full cycle of the path length | | `type` | TrimType | separate | Trim type (see below) | **TrimType**: @@ -1792,7 +1792,7 @@ alpha = lerp(startAlpha, endAlpha, t) **Fractional Copy Count**: -When `copies` is a decimal (e.g., `3.5`), partial copies are achieved through **overlay with semi-transparency**: +When `copies` is a decimal (e.g., `3.5`), partial copies are achieved through **semi-transparent blending**: 1. **Geometry copying**: Shapes and text are copied by `ceil(copies)` (i.e., 4), geometry itself is not scaled or clipped 2. **Opacity adjustment**: The last copy's opacity is multiplied by the fractional part (e.g., 0.5), producing semi-transparent effect From f08a82c126c46be903bcd0ef721b5acf115730a1 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 22:34:57 +0800 Subject: [PATCH 249/678] Refine design goals section in PAGX specification. --- pagx/spec/pagx_spec.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 294efac872..21d4138817 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -6,15 +6,15 @@ ### 1.1 Design Goals -- **Open and Readable**: Plain-text XML format that is easy to read and edit, with native support for version control and diff comparison, facilitating debugging, AI understanding, and content generation. +- **Open and Readable**: Uses a plain-text XML format that is easy to read and edit, with native support for version control and diffing, facilitating debugging as well as AI understanding and generation. -- **Feature Complete**: Comprehensive coverage of vector graphics, images, rich text, filter effects, blend modes, masks, and more to meet the requirements of complex animation descriptions. +- **Feature-Complete**: Fully covers vector graphics, raster images, rich text, filter effects, blending modes, masking, and related capabilities, meeting the requirements for complex animated graphics. -- **Concise and Efficient**: Provides a simple yet powerful unified structure that balances optimized static descriptions while reserving extensibility for future interactivity and scripting. +- **Concise and Efficient**: Defines a compact yet expressive unified structure that optimizes the description of both static vector content and animations, while reserving extensibility for future interaction and scripting. -- **Ecosystem Compatible**: Serves as a universal interchange format for design tools such as After Effects, Figma, and Tencent Design, enabling seamless transfer of design assets. +- **Ecosystem Compatible**: Can serve as a common interchange format for design tools such as After Effects, Figma, and Tencent Design, enabling seamless asset exchange across platforms. -- **Efficient Deployment**: Design assets can be exported and deployed to development environments with one click, achieving high compression ratios and runtime performance when converted to binary PAG format. +- **Efficient Deployment**: Design assets can be exported and deployed to production environments with a single action, achieving high compression ratios and excellent runtime performance after conversion to the binary PAG format. ### 1.2 File Structure From 357859a88ec30273a86b48b3ce0f76ea5335e33d Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 22:37:09 +0800 Subject: [PATCH 250/678] Refactor text typesetting to decouple font embedding from text shaping. --- pagx/include/pagx/FontEmbedder.h | 52 +++ pagx/include/pagx/LayerBuilder.h | 57 +-- pagx/include/pagx/TextGlyphs.h | 74 ++++ pagx/include/pagx/Typesetter.h | 77 +++-- pagx/src/TextGlyphs.cpp | 56 +++ pagx/src/tgfx/FontEmbedder.cpp | 415 ++++++++++++++++++++++ pagx/src/tgfx/LayerBuilder.cpp | 53 ++- pagx/src/tgfx/Typesetter.cpp | 574 ++++++------------------------- test/src/PAGXTest.cpp | 171 ++++----- 9 files changed, 869 insertions(+), 660 deletions(-) create mode 100644 pagx/include/pagx/FontEmbedder.h create mode 100644 pagx/include/pagx/TextGlyphs.h create mode 100644 pagx/src/TextGlyphs.cpp create mode 100644 pagx/src/tgfx/FontEmbedder.cpp diff --git a/pagx/include/pagx/FontEmbedder.h b/pagx/include/pagx/FontEmbedder.h new file mode 100644 index 0000000000..e09333799d --- /dev/null +++ b/pagx/include/pagx/FontEmbedder.h @@ -0,0 +1,52 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/PAGXDocument.h" +#include "pagx/TextGlyphs.h" + +namespace pagx { + +/** + * FontEmbedder extracts glyph data from TextGlyphs and embeds it into the PAGXDocument. + * It creates Font nodes with glyph paths/images and updates Text nodes with GlyphRun data. + * + * Font merging strategy: + * - All vector glyphs (with path) are merged into one Font node + * - All bitmap glyphs (with image) are merged into another Font node + * - Maximum 2 Font nodes per document + */ +class FontEmbedder { + public: + /** + * Embeds font data from TextGlyphs into the document. + * + * This method: + * 1. Iterates all TextBlobs in textGlyphs, extracts glyph data (paths or images) + * 2. Merges glyphs into Font nodes (one for vector, one for bitmap) + * 3. Creates GlyphRun nodes for each Text, referencing the embedded fonts + * + * @param document The document to embed fonts into (modified in place). + * @param textGlyphs The typesetting results containing Text -> TextBlob mappings. + * @return true if embedding succeeded, false otherwise. + */ + static bool Embed(PAGXDocument* document, const TextGlyphs& textGlyphs); +}; + +} // namespace pagx diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index ea620ea63a..906bc940ba 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -19,70 +19,27 @@ #pragma once #include -#include -#include #include "pagx/PAGXDocument.h" -#include "tgfx/core/Typeface.h" +#include "pagx/TextGlyphs.h" #include "tgfx/layers/Layer.h" namespace pagx { -/** - * Result of building a layer tree from a PAGXDocument. - */ -struct PAGXContent { - /** - * The root layer of the built layer tree. - */ - std::shared_ptr root = nullptr; - - /** - * The width of the content. - */ - float width = 0; - - /** - * The height of the content. - */ - float height = 0; -}; - -/** - * Build options for LayerBuilder. - */ -struct LayerBuildOptions { - /** - * Fallback typefaces used when the primary font doesn't contain required glyphs. - */ - std::vector> fallbackTypefaces = {}; -}; - /** * LayerBuilder converts PAGXDocument to tgfx::Layer tree for rendering. * This is the bridge between the independent pagx module and tgfx rendering. - * - * Note: LayerBuilder expects the document to be pre-typeset. Text elements without GlyphRun data - * will trigger a DEBUG_ASSERT in debug builds and be skipped in release builds. Use Typesetter - * to typeset the document before calling LayerBuilder. */ class LayerBuilder { public: - using Options = LayerBuildOptions; - /** * Builds a layer tree from a PAGXDocument. + * @param document The document to build from. + * @param textGlyphs Optional typesetting results. If provided, uses original typefaces for + * rendering (best quality). If nullptr, builds from embedded GlyphRun data. + * @return The root layer of the built layer tree. */ - static PAGXContent Build(const PAGXDocument& document, const Options& options = {}); - - /** - * Builds a layer tree from a PAGX file. - */ - static PAGXContent FromFile(const std::string& filePath, const Options& options = {}); - - /** - * Builds a layer tree from PAGX XML data. - */ - static PAGXContent FromData(const uint8_t* data, size_t length, const Options& options = {}); + static std::shared_ptr Build(const PAGXDocument& document, + const TextGlyphs* textGlyphs = nullptr); }; } // namespace pagx diff --git a/pagx/include/pagx/TextGlyphs.h b/pagx/include/pagx/TextGlyphs.h new file mode 100644 index 0000000000..6d93c1d87b --- /dev/null +++ b/pagx/include/pagx/TextGlyphs.h @@ -0,0 +1,74 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "tgfx/core/TextBlob.h" + +namespace pagx { + +class Text; + +/** + * TextGlyphs holds the text typesetting results, mapping Text nodes to their shaped TextBlob + * representations. This class serves as the bridge between text typesetting (by Typesetter or + * external typesetters) and downstream consumers (LayerBuilder for rendering, FontEmbedder for + * font embedding). + */ +class TextGlyphs { + public: + TextGlyphs() = default; + + /** + * Adds a mapping from a Text node to its shaped TextBlob. + */ + void add(Text* text, std::shared_ptr textBlob); + + /** + * Returns the TextBlob for the given Text node, or nullptr if not found. + */ + std::shared_ptr get(const Text* text) const; + + /** + * Returns true if this TextGlyphs contains a mapping for the given Text node. + */ + bool contains(const Text* text) const; + + /** + * Iterates over all Text-TextBlob mappings. + */ + void forEach(std::function)> callback) const; + + /** + * Returns the number of Text-TextBlob mappings. + */ + size_t size() const; + + /** + * Returns true if there are no mappings. + */ + bool empty() const; + + private: + std::unordered_map> textBlobs = {}; +}; + +} // namespace pagx diff --git a/pagx/include/pagx/Typesetter.h b/pagx/include/pagx/Typesetter.h index d50368482b..031b90d4ef 100644 --- a/pagx/include/pagx/Typesetter.h +++ b/pagx/include/pagx/Typesetter.h @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// Copyright (C) 2026 Tencent. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -19,62 +19,63 @@ #pragma once #include +#include #include #include "pagx/PAGXDocument.h" +#include "pagx/TextGlyphs.h" #include "tgfx/core/Typeface.h" namespace pagx { /** - * Typesetter performs text typesetting on PAGXDocument, converting Text elements to pre-shaped - * format with embedded glyph data. It handles both text shaping (glyph mapping) and text layout - * (alignment, line breaking, etc.). - * - * Terminology: - * - Pre-shaped: Text has been typeset with embedded GlyphRun data - * - Runtime shaping: Text needs to be shaped at render time (no GlyphRun data) - * - * The typesetter processes text in Group granularity: - * - If any Text in a Group lacks GlyphRun data, the entire Group is typeset - * - This ensures consistent TextLayout calculations within the same Group + * Typesetter performs text typesetting on PAGXDocument, converting Text elements into positioned + * glyph data (TextBlob). It handles font matching, fallback, text shaping, and layout (alignment, + * line breaking, etc.). */ class Typesetter { public: - /** - * Creates a Typesetter instance. - */ - static std::shared_ptr Make(); - - virtual ~Typesetter() = default; + Typesetter() = default; /** - * Registers a typeface for a specific font family and style. When typesetting text, the - * registered typeface will be used if its family and style match the text's fontFamily and - * fontStyle. - * @param typeface The typeface to register. + * Registers a typeface for font matching. When typesetting, registered typefaces are matched + * first by fontFamily and fontStyle. If no registered typeface matches, the system font is used. */ - virtual void registerTypeface(std::shared_ptr typeface) = 0; + void registerTypeface(std::shared_ptr typeface); /** - * Sets the fallback typefaces used when no registered typeface matches the text's font - * properties. The first matching typeface in the list will be used. - * @param typefaces Fallback typefaces in priority order. + * Sets the fallback typefaces used when a character is not found in the primary font (either + * registered or system). Typefaces are tried in order until one containing the character is found. */ - virtual void setFallbackTypefaces(std::vector> typefaces) = 0; + void setFallbackTypefaces(std::vector> typefaces); /** - * Typesets all text elements in the document. For each Text element, generates GlyphRun data - * with positioned glyphs. TextLayout modifiers are processed to bake alignment and layout - * adjustments into the GlyphRun positions. Font resources are created for each unique typeface - * used, containing glyph path data. - * - * @param document The document to process (modified in place). - * @param force If true, forces re-typesetting of all text elements even if they already have - * GlyphRun data. If false (default), only typesets Groups where any Text lacks - * GlyphRun data. - * @return true if any text was typeset, false if no text elements found or all skipped. + * Creates TextGlyphs for all Text nodes in the document. TextLayout modifiers are processed to + * apply alignment, line breaking, and other layout properties. + * @param document The document containing Text nodes to typeset. + * @return TextGlyphs containing Text -> TextBlob mappings. */ - virtual bool typeset(PAGXDocument* document, bool force = false) = 0; + TextGlyphs createTextGlyphs(PAGXDocument* document); + + private: + friend class TypesetterContext; + + struct FontKey { + std::string family = {}; + std::string style = {}; + + bool operator==(const FontKey& other) const { + return family == other.family && style == other.style; + } + }; + + struct FontKeyHash { + size_t operator()(const FontKey& key) const { + return std::hash()(key.family) ^ (std::hash()(key.style) << 1); + } + }; + + std::unordered_map, FontKeyHash> registeredTypefaces = {}; + std::vector> fallbackTypefaces = {}; }; } // namespace pagx diff --git a/pagx/src/TextGlyphs.cpp b/pagx/src/TextGlyphs.cpp new file mode 100644 index 0000000000..ecb1155e4d --- /dev/null +++ b/pagx/src/TextGlyphs.cpp @@ -0,0 +1,56 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/TextGlyphs.h" + +namespace pagx { + +void TextGlyphs::add(Text* text, std::shared_ptr textBlob) { + if (text != nullptr && textBlob != nullptr) { + textBlobs[text] = std::move(textBlob); + } +} + +std::shared_ptr TextGlyphs::get(const Text* text) const { + auto it = textBlobs.find(const_cast(text)); + if (it != textBlobs.end()) { + return it->second; + } + return nullptr; +} + +bool TextGlyphs::contains(const Text* text) const { + return textBlobs.find(const_cast(text)) != textBlobs.end(); +} + +void TextGlyphs::forEach( + std::function)> callback) const { + for (const auto& pair : textBlobs) { + callback(pair.first, pair.second); + } +} + +size_t TextGlyphs::size() const { + return textBlobs.size(); +} + +bool TextGlyphs::empty() const { + return textBlobs.empty(); +} + +} // namespace pagx diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp new file mode 100644 index 0000000000..f8ef06937f --- /dev/null +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -0,0 +1,415 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "pagx/FontEmbedder.h" +#include +#include +#include +#include "SVGPathParser.h" +#include "pagx/nodes/Font.h" +#include "pagx/nodes/Image.h" +#include "pagx/nodes/Text.h" +#include "pagx/types/Data.h" +#include "tgfx/core/Bitmap.h" +#include "tgfx/core/GlyphRun.h" +#include "tgfx/core/ImageCodec.h" +#include "tgfx/core/Path.h" + +namespace pagx { + +// Scales RGBA8888 pixels using bilinear interpolation for smooth downscaling. +static void ScalePixelsBilinear(const uint8_t* srcPixels, int srcW, int srcH, size_t srcRowBytes, + uint8_t* dstPixels, int dstW, int dstH, size_t dstRowBytes) { + float scaleX = static_cast(srcW) / static_cast(dstW); + float scaleY = static_cast(srcH) / static_cast(dstH); + + for (int y = 0; y < dstH; y++) { + float srcY = (static_cast(y) + 0.5f) * scaleY - 0.5f; + int y0 = static_cast(std::floor(srcY)); + int y1 = y0 + 1; + float fy = srcY - static_cast(y0); + + y0 = std::max(0, std::min(y0, srcH - 1)); + y1 = std::max(0, std::min(y1, srcH - 1)); + + auto* dstRow = dstPixels + y * dstRowBytes; + + for (int x = 0; x < dstW; x++) { + float srcX = (static_cast(x) + 0.5f) * scaleX - 0.5f; + int x0 = static_cast(std::floor(srcX)); + int x1 = x0 + 1; + float fx = srcX - static_cast(x0); + + x0 = std::max(0, std::min(x0, srcW - 1)); + x1 = std::max(0, std::min(x1, srcW - 1)); + + const auto* p00 = srcPixels + y0 * srcRowBytes + x0 * 4; + const auto* p10 = srcPixels + y0 * srcRowBytes + x1 * 4; + const auto* p01 = srcPixels + y1 * srcRowBytes + x0 * 4; + const auto* p11 = srcPixels + y1 * srcRowBytes + x1 * 4; + + for (int c = 0; c < 4; c++) { + float v00 = static_cast(p00[c]); + float v10 = static_cast(p10[c]); + float v01 = static_cast(p01[c]); + float v11 = static_cast(p11[c]); + + float v0 = v00 + (v10 - v00) * fx; + float v1 = v01 + (v11 - v01) * fx; + float v = v0 + (v1 - v0) * fy; + + dstRow[x * 4 + c] = static_cast(std::round(std::max(0.0f, std::min(255.0f, v)))); + } + } + } +} + +// Converts a tgfx::Path to SVG path string. +static std::string PathToSVGString(const tgfx::Path& path) { + std::string result = {}; + result.reserve(256); + char buf[64] = {}; + + path.decompose([&](tgfx::PathVerb verb, const tgfx::Point pts[4], void*) { + switch (verb) { + case tgfx::PathVerb::Move: + snprintf(buf, sizeof(buf), "M%g %g", pts[0].x, pts[0].y); + result += buf; + break; + case tgfx::PathVerb::Line: + snprintf(buf, sizeof(buf), "L%g %g", pts[1].x, pts[1].y); + result += buf; + break; + case tgfx::PathVerb::Quad: + snprintf(buf, sizeof(buf), "Q%g %g %g %g", pts[1].x, pts[1].y, pts[2].x, pts[2].y); + result += buf; + break; + case tgfx::PathVerb::Cubic: + snprintf(buf, sizeof(buf), "C%g %g %g %g %g %g", pts[1].x, pts[1].y, pts[2].x, pts[2].y, + pts[3].x, pts[3].y); + result += buf; + break; + case tgfx::PathVerb::Close: + result += "Z"; + break; + } + }); + + return result; +} + +// Key for identifying unique glyphs: typeface pointer + glyph ID. +struct GlyphKey { + const tgfx::Typeface* typeface = nullptr; + tgfx::GlyphID glyphID = 0; + + bool operator==(const GlyphKey& other) const { + return typeface == other.typeface && glyphID == other.glyphID; + } +}; + +struct GlyphKeyHash { + size_t operator()(const GlyphKey& key) const { + return std::hash()(key.typeface) ^ (std::hash()(key.glyphID) << 1); + } +}; + +// Information about a glyph to be embedded. +struct GlyphInfo { + std::string pathString = {}; + std::shared_ptr imageCodec = nullptr; + tgfx::Matrix imageMatrix = {}; +}; + +// Internal implementation class for FontEmbedder. +class FontEmbedderImpl { + public: + explicit FontEmbedderImpl(PAGXDocument* document) : document(document) { + } + + bool embed(const TextGlyphs& textGlyphs) { + if (document == nullptr) { + return false; + } + + // First pass: collect all glyphs and create Font nodes + textGlyphs.forEach([this](Text* text, std::shared_ptr textBlob) { + if (textBlob == nullptr) { + return; + } + collectGlyphs(textBlob); + }); + + // Create Font nodes if we have glyphs + createFontNodes(); + + // Second pass: create GlyphRuns for each Text + textGlyphs.forEach([this](Text* text, std::shared_ptr textBlob) { + if (textBlob == nullptr) { + return; + } + createGlyphRuns(text, textBlob); + }); + + return true; + } + + private: + void collectGlyphs(const std::shared_ptr& textBlob) { + for (const auto& run : *textBlob) { + auto* typeface = run.font.getTypeface().get(); + for (size_t i = 0; i < run.glyphCount; ++i) { + tgfx::GlyphID glyphID = run.glyphs[i]; + GlyphKey key = {typeface, glyphID}; + + if (vectorGlyphMapping.find(key) != vectorGlyphMapping.end() || + bitmapGlyphMapping.find(key) != bitmapGlyphMapping.end()) { + continue; // Already processed + } + + // Try to get glyph path first (vector outline) + tgfx::Path glyphPath = {}; + bool hasPath = run.font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); + + // Try to get glyph image (bitmap, e.g., color emoji) + tgfx::Matrix imageMatrix = {}; + auto imageCodec = run.font.getImage(glyphID, nullptr, &imageMatrix); + + if (hasPath) { + // Vector glyph + GlyphInfo info = {}; + info.pathString = PathToSVGString(glyphPath); + if (!info.pathString.empty()) { + pendingVectorGlyphs.push_back({key, info}); + } + } else if (imageCodec) { + // Bitmap glyph + GlyphInfo info = {}; + info.imageCodec = imageCodec; + info.imageMatrix = imageMatrix; + pendingBitmapGlyphs.push_back({key, info}); + } + } + } + } + + void createFontNodes() { + // Create vector font if needed + if (!pendingVectorGlyphs.empty()) { + vectorFont = document->makeNode("vector_font"); + tgfx::GlyphID nextID = 1; + for (auto& [key, info] : pendingVectorGlyphs) { + auto glyph = document->makeNode(); + glyph->path = document->makeNode(); + *glyph->path = PathDataFromSVGString(info.pathString); + vectorFont->glyphs.push_back(glyph); + vectorGlyphMapping[key] = nextID++; + } + } + + // Create bitmap font if needed + if (!pendingBitmapGlyphs.empty()) { + bitmapFont = document->makeNode("bitmap_font"); + tgfx::GlyphID nextID = 1; + for (auto& [key, info] : pendingBitmapGlyphs) { + auto glyph = createBitmapGlyph(info); + if (glyph != nullptr) { + bitmapFont->glyphs.push_back(glyph); + bitmapGlyphMapping[key] = nextID++; + } + } + } + } + + Glyph* createBitmapGlyph(const GlyphInfo& info) { + auto imageCodec = info.imageCodec; + auto imageMatrix = info.imageMatrix; + + int srcW = imageCodec->width(); + int srcH = imageCodec->height(); + float scaleX = imageMatrix.getScaleX(); + float scaleY = imageMatrix.getScaleY(); + int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); + int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); + + if (dstW <= 0 || dstH <= 0) { + return nullptr; + } + + tgfx::Bitmap srcBitmap(srcW, srcH, false, false); + if (srcBitmap.isEmpty()) { + return nullptr; + } + + auto* srcPixels = srcBitmap.lockPixels(); + if (srcPixels == nullptr || !imageCodec->readPixels(srcBitmap.info(), srcPixels)) { + srcBitmap.unlockPixels(); + return nullptr; + } + srcBitmap.unlockPixels(); + + tgfx::Bitmap dstBitmap(dstW, dstH, false, false); + if (dstBitmap.isEmpty()) { + return nullptr; + } + + auto* dstPixels = dstBitmap.lockPixels(); + if (dstPixels == nullptr) { + return nullptr; + } + + auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); + ScalePixelsBilinear(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), + static_cast(dstPixels), dstW, dstH, dstBitmap.info().rowBytes()); + srcBitmap.unlockPixels(); + dstBitmap.unlockPixels(); + + auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); + if (pngData == nullptr) { + return nullptr; + } + + auto glyph = document->makeNode(); + auto image = document->makeNode(); + image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); + glyph->image = image; + glyph->offset.x = imageMatrix.getTranslateX(); + glyph->offset.y = imageMatrix.getTranslateY(); + + return glyph; + } + + void createGlyphRuns(Text* text, const std::shared_ptr& textBlob) { + // Clear existing glyph runs + text->glyphRuns.clear(); + + for (const auto& run : *textBlob) { + auto* typeface = run.font.getTypeface().get(); + + // Separate glyphs into vector and bitmap runs + std::vector vectorIndices = {}; + std::vector bitmapIndices = {}; + + for (size_t i = 0; i < run.glyphCount; ++i) { + GlyphKey key = {typeface, run.glyphs[i]}; + if (vectorGlyphMapping.find(key) != vectorGlyphMapping.end()) { + vectorIndices.push_back(i); + } else if (bitmapGlyphMapping.find(key) != bitmapGlyphMapping.end()) { + bitmapIndices.push_back(i); + } + } + + // Create vector glyph run + if (!vectorIndices.empty() && vectorFont != nullptr) { + auto glyphRun = createGlyphRun(run, vectorIndices, vectorFont, vectorGlyphMapping, typeface); + if (glyphRun != nullptr) { + text->glyphRuns.push_back(glyphRun); + } + } + + // Create bitmap glyph run + if (!bitmapIndices.empty() && bitmapFont != nullptr) { + auto glyphRun = createGlyphRun(run, bitmapIndices, bitmapFont, bitmapGlyphMapping, typeface); + if (glyphRun != nullptr) { + text->glyphRuns.push_back(glyphRun); + } + } + } + } + + GlyphRun* createGlyphRun(const tgfx::GlyphRun& run, const std::vector& indices, + Font* font, + const std::unordered_map& mapping, + const tgfx::Typeface* typeface) { + auto glyphRun = document->makeNode(); + glyphRun->font = font; + + for (size_t i : indices) { + GlyphKey key = {typeface, run.glyphs[i]}; + auto it = mapping.find(key); + if (it != mapping.end()) { + glyphRun->glyphs.push_back(it->second); + } else { + glyphRun->glyphs.push_back(0); + } + } + + // Copy positions based on positioning mode + switch (run.positioning) { + case tgfx::GlyphPositioning::Horizontal: { + glyphRun->y = run.offsetY; + for (size_t i : indices) { + glyphRun->xPositions.push_back(run.positions[i]); + } + break; + } + case tgfx::GlyphPositioning::Point: { + auto* points = reinterpret_cast(run.positions); + for (size_t i : indices) { + glyphRun->positions.push_back({points[i].x, points[i].y}); + } + break; + } + case tgfx::GlyphPositioning::RSXform: { + auto* xforms = reinterpret_cast(run.positions); + for (size_t i : indices) { + RSXform xform = {}; + xform.scos = xforms[i].scos; + xform.ssin = xforms[i].ssin; + xform.tx = xforms[i].tx; + xform.ty = xforms[i].ty; + glyphRun->xforms.push_back(xform); + } + break; + } + case tgfx::GlyphPositioning::Matrix: { + auto* matrices = reinterpret_cast(run.positions); + for (size_t i : indices) { + Matrix m = {}; + m.a = matrices[i * 6 + 0]; + m.b = matrices[i * 6 + 1]; + m.c = matrices[i * 6 + 2]; + m.d = matrices[i * 6 + 3]; + m.tx = matrices[i * 6 + 4]; + m.ty = matrices[i * 6 + 5]; + glyphRun->matrices.push_back(m); + } + break; + } + } + + return glyphRun; + } + + PAGXDocument* document = nullptr; + Font* vectorFont = nullptr; + Font* bitmapFont = nullptr; + + std::vector> pendingVectorGlyphs = {}; + std::vector> pendingBitmapGlyphs = {}; + + std::unordered_map vectorGlyphMapping = {}; + std::unordered_map bitmapGlyphMapping = {}; +}; + +bool FontEmbedder::Embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { + FontEmbedderImpl impl(document); + return impl.embed(textGlyphs); +} + +} // namespace pagx diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index c3ae8ce12a..0b31f82901 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -19,7 +19,6 @@ #include "pagx/LayerBuilder.h" #include #include -#include "pagx/PAGXImporter.h" #include "pagx/nodes/BackgroundBlurStyle.h" #include "pagx/nodes/BlurFilter.h" #include "pagx/types/ColorSpace.h" @@ -351,20 +350,16 @@ static tgfx::LayerMaskType ToTGFXMaskType(MaskType type) { // Internal builder class class LayerBuilderImpl { public: - explicit LayerBuilderImpl(const LayerBuilder::Options& options) : _options(options) { + explicit LayerBuilderImpl(const TextGlyphs* textGlyphs) : _textGlyphs(textGlyphs) { } - PAGXContent build(const PAGXDocument& document) { + std::shared_ptr build(const PAGXDocument& document) { _document = &document; // Clear mappings from previous builds. _tgfxLayerByPagxLayer.clear(); _pendingMasks.clear(); _fontCache.clear(); - PAGXContent content; - content.width = document.width; - content.height = document.height; - // Build layer tree. auto rootLayer = tgfx::Layer::Make(); for (const auto& layer : document.layers) { @@ -387,12 +382,11 @@ class LayerBuilderImpl { } } - content.root = rootLayer; _document = nullptr; _tgfxLayerByPagxLayer.clear(); _pendingMasks.clear(); _fontCache.clear(); - return content; + return rootLayer; } private: @@ -530,13 +524,23 @@ class LayerBuilderImpl { std::shared_ptr convertText(const Text* node) { auto tgfxText = std::make_shared(); - // Text must be pre-typeset with GlyphRuns. Use Typesetter before calling LayerBuilder. + // Priority 1: Use TextGlyphs mapping (original typeface, best quality) + if (_textGlyphs != nullptr) { + auto textBlob = _textGlyphs->get(node); + if (textBlob) { + tgfxText->setTextBlob(textBlob); + tgfxText->setPosition(tgfx::Point::Make(node->position.x, node->position.y)); + return tgfxText; + } + } + + // Priority 2: Build from glyphRuns (embedded font, for imported files) if (node->glyphRuns.empty()) { - DEBUG_ASSERT(false && "Text element has no GlyphRun data. Use Typesetter to typeset first."); + DEBUG_ASSERT(false && "Text element has no GlyphRun data and no TextGlyphs provided."); return tgfxText; } - auto textBlob = buildTextBlob(node); + auto textBlob = buildTextBlobFromGlyphRuns(node); if (textBlob) { tgfxText->setTextBlob(textBlob); } @@ -544,7 +548,7 @@ class LayerBuilderImpl { return tgfxText; } - std::shared_ptr buildTextBlob(const Text* node) { + std::shared_ptr buildTextBlobFromGlyphRuns(const Text* node) { tgfx::TextBlobBuilder builder; for (const auto& run : node->glyphRuns) { @@ -978,7 +982,7 @@ class LayerBuilderImpl { } } - LayerBuilder::Options _options = {}; + const TextGlyphs* _textGlyphs = nullptr; const PAGXDocument* _document = nullptr; std::unordered_map> _tgfxLayerByPagxLayer = {}; std::vector, const Layer*, tgfx::LayerMaskType>> @@ -988,25 +992,10 @@ class LayerBuilderImpl { // Public API implementation -PAGXContent LayerBuilder::Build(const PAGXDocument& document, const Options& options) { - LayerBuilderImpl builder(options); +std::shared_ptr LayerBuilder::Build(const PAGXDocument& document, + const TextGlyphs* textGlyphs) { + LayerBuilderImpl builder(textGlyphs); return builder.build(document); } -PAGXContent LayerBuilder::FromFile(const std::string& filePath, const Options& options) { - auto document = PAGXImporter::FromFile(filePath); - if (!document) { - return {}; - } - return Build(*document, options); -} - -PAGXContent LayerBuilder::FromData(const uint8_t* data, size_t length, const Options& options) { - auto document = PAGXImporter::FromXML(data, length); - if (!document) { - return {}; - } - return Build(*document, options); -} - } // namespace pagx diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index cd54776789..951d028a82 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -2,7 +2,7 @@ // // Tencent is pleased to support the open source community by making libpag available. // -// Copyright (C) 2026 THL A29 Limited, a Tencent company. All rights reserved. +// Copyright (C) 2026 Tencent. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at @@ -17,210 +17,90 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "pagx/Typesetter.h" -#include #include -#include -#include -#include "SVGPathParser.h" #include "pagx/nodes/Composition.h" -#include "pagx/nodes/Font.h" #include "pagx/nodes/Group.h" -#include "pagx/nodes/Image.h" #include "pagx/nodes/Text.h" #include "pagx/nodes/TextLayout.h" -#include "tgfx/core/Bitmap.h" #include "tgfx/core/Font.h" -#include "tgfx/core/ImageCodec.h" #include "tgfx/core/Path.h" -#include "tgfx/core/PathTypes.h" +#include "tgfx/core/TextBlobBuilder.h" namespace pagx { -// Scales RGBA8888 pixels using bilinear interpolation for smooth downscaling. -static void ScalePixelsBilinear(const uint8_t* srcPixels, int srcW, int srcH, size_t srcRowBytes, - uint8_t* dstPixels, int dstW, int dstH, size_t dstRowBytes) { - float scaleX = static_cast(srcW) / static_cast(dstW); - float scaleY = static_cast(srcH) / static_cast(dstH); - - for (int y = 0; y < dstH; y++) { - float srcY = (static_cast(y) + 0.5f) * scaleY - 0.5f; - int y0 = static_cast(std::floor(srcY)); - int y1 = y0 + 1; - float fy = srcY - static_cast(y0); - - y0 = std::max(0, std::min(y0, srcH - 1)); - y1 = std::max(0, std::min(y1, srcH - 1)); - - auto* dstRow = dstPixels + y * dstRowBytes; - - for (int x = 0; x < dstW; x++) { - float srcX = (static_cast(x) + 0.5f) * scaleX - 0.5f; - int x0 = static_cast(std::floor(srcX)); - int x1 = x0 + 1; - float fx = srcX - static_cast(x0); - - x0 = std::max(0, std::min(x0, srcW - 1)); - x1 = std::max(0, std::min(x1, srcW - 1)); - - // Sample 4 neighboring pixels - const auto* p00 = srcPixels + y0 * srcRowBytes + x0 * 4; - const auto* p10 = srcPixels + y0 * srcRowBytes + x1 * 4; - const auto* p01 = srcPixels + y1 * srcRowBytes + x0 * 4; - const auto* p11 = srcPixels + y1 * srcRowBytes + x1 * 4; - - // Bilinear interpolation for each channel (RGBA) - for (int c = 0; c < 4; c++) { - float v00 = static_cast(p00[c]); - float v10 = static_cast(p10[c]); - float v01 = static_cast(p01[c]); - float v11 = static_cast(p11[c]); - - float v0 = v00 + (v10 - v00) * fx; - float v1 = v01 + (v11 - v01) * fx; - float v = v0 + (v1 - v0) * fy; - - dstRow[x * 4 + c] = static_cast(std::round(std::max(0.0f, std::min(255.0f, v)))); - } - } +void Typesetter::registerTypeface(std::shared_ptr typeface) { + if (typeface == nullptr) { + return; } + FontKey key = {}; + key.family = typeface->fontFamily(); + key.style = typeface->fontStyle(); + registeredTypefaces[key] = std::move(typeface); } -// Converts a tgfx::Path to SVG path string -static std::string PathToSVGString(const tgfx::Path& path) { - std::string result = {}; - result.reserve(256); - char buf[64] = {}; - - path.decompose([&](tgfx::PathVerb verb, const tgfx::Point pts[4], void*) { - switch (verb) { - case tgfx::PathVerb::Move: - snprintf(buf, sizeof(buf), "M%g %g", pts[0].x, pts[0].y); - result += buf; - break; - case tgfx::PathVerb::Line: - snprintf(buf, sizeof(buf), "L%g %g", pts[1].x, pts[1].y); - result += buf; - break; - case tgfx::PathVerb::Quad: - snprintf(buf, sizeof(buf), "Q%g %g %g %g", pts[1].x, pts[1].y, pts[2].x, pts[2].y); - result += buf; - break; - case tgfx::PathVerb::Cubic: - snprintf(buf, sizeof(buf), "C%g %g %g %g %g %g", pts[1].x, pts[1].y, pts[2].x, pts[2].y, - pts[3].x, pts[3].y); - result += buf; - break; - case tgfx::PathVerb::Close: - result += "Z"; - break; - } - }); - - return result; +void Typesetter::setFallbackTypefaces(std::vector> typefaces) { + fallbackTypefaces = std::move(typefaces); } -// Key for font registration: fontFamily + fontStyle -struct FontKey { - std::string family = {}; - std::string style = {}; - - bool operator==(const FontKey& other) const { - return family == other.family && style == other.style; - } -}; - -struct FontKeyHash { - size_t operator()(const FontKey& key) const { - return std::hash()(key.family) ^ (std::hash()(key.style) << 1); - } -}; - -// Shaped glyph run for a specific font -struct ShapedGlyphRun { - std::shared_ptr typeface = nullptr; - tgfx::Font font = {}; - std::vector glyphIDs = {}; - std::vector xPositions = {}; -}; - -// Intermediate shaping result for a single Text element -struct ShapedTextInfo { - Text* text = nullptr; - std::vector runs = {}; - float totalWidth = 0; -}; - -// Private implementation class -class TypesetterImpl : public Typesetter { +// Internal implementation class for createTextGlyphs. +class TypesetterContext { public: - TypesetterImpl() = default; - - void registerTypeface(std::shared_ptr typeface) override { - if (!typeface) { - return; - } - FontKey key; - key.family = typeface->fontFamily(); - key.style = typeface->fontStyle(); - _registeredTypefaces[key] = std::move(typeface); + TypesetterContext(const Typesetter* typesetter, PAGXDocument* document) + : typesetter(typesetter), document(document) { } - void setFallbackTypefaces(std::vector> typefaces) override { - _fallbackTypefaces = std::move(typefaces); - } - - bool typeset(PAGXDocument* document, bool force) override { - if (!document) { - return false; + TextGlyphs run() { + if (document == nullptr) { + return result; } - _document = document; - _force = force; - _textTypeset = false; - _fontResources.clear(); - _glyphMapping.clear(); - _fontKeyToId.clear(); - _nextFontId = 0; - // Process all layers - for (auto& layer : _document->layers) { + for (auto* layer : document->layers) { processLayer(layer); } // Process compositions in nodes - for (auto& node : _document->nodes) { + for (auto& node : document->nodes) { if (node->nodeType() == NodeType::Composition) { - auto comp = static_cast(node.get()); - for (auto& layer : comp->layers) { + auto* comp = static_cast(node.get()); + for (auto* layer : comp->layers) { processLayer(layer); } } } - return _textTypeset; + return std::move(result); } private: + // Shaped text information for a single Text element. + struct ShapedInfo { + Text* text = nullptr; + std::vector glyphIDs = {}; + std::vector xPositions = {}; + tgfx::Font font = {}; + float totalWidth = 0; + }; + + using FontKey = Typesetter::FontKey; + void processLayer(Layer* layer) { - if (!layer) { + if (layer == nullptr) { return; } - // Process layer contents, looking for Text + TextLayout combinations processLayerContents(layer->contents); - // Process child layers - for (auto& child : layer->children) { + for (auto* child : layer->children) { processLayer(child); } } - void processLayerContents(std::vector& contents) { - // Find TextLayout in layer contents + void processLayerContents(const std::vector& contents) { const TextLayout* textLayout = nullptr; std::vector textElements = {}; - for (auto& element : contents) { + for (auto* element : contents) { if (element->nodeType() == NodeType::TextLayout) { textLayout = static_cast(element); } else if (element->nodeType() == NodeType::Text) { @@ -230,143 +110,97 @@ class TypesetterImpl : public Typesetter { } } - // If we found Text elements at Layer level (not in Group) if (!textElements.empty()) { processTextWithLayout(textElements, textLayout); } } void processGroup(Group* group) { - if (!group) { + if (group == nullptr) { return; } - // Find TextLayout modifier and collect Text elements const TextLayout* textLayout = nullptr; std::vector textElements = {}; - for (auto& element : group->elements) { + for (auto* element : group->elements) { if (element->nodeType() == NodeType::TextLayout) { textLayout = static_cast(element); } else if (element->nodeType() == NodeType::Text) { textElements.push_back(static_cast(element)); } else if (element->nodeType() == NodeType::Group) { - // Recursively process nested groups processGroup(static_cast(element)); } } - // Process Text elements with optional TextLayout if (!textElements.empty()) { processTextWithLayout(textElements, textLayout); } } void processTextWithLayout(std::vector& textElements, const TextLayout* textLayout) { - // Check if any Text needs typesetting - bool needsTypesetting = _force; - if (!needsTypesetting) { - for (auto* text : textElements) { - if (text->glyphRuns.empty() && !text->text.empty()) { - needsTypesetting = true; - break; - } - } - } - - if (!needsTypesetting) { - return; - } + std::vector shapedInfos = {}; - // Clear all GlyphRuns for re-typesetting for (auto* text : textElements) { - text->glyphRuns.clear(); - } + ShapedInfo info = {}; + info.text = text; - // Shape all Text elements first - std::vector shapedInfos = {}; - for (auto* text : textElements) { - if (text->text.empty()) { - shapedInfos.push_back({}); - continue; + if (!text->text.empty()) { + shapeText(text, info); } - shapedInfos.push_back(shapeText(text)); - } - // Apply TextLayout if present - for (size_t i = 0; i < textElements.size(); ++i) { - auto* text = textElements[i]; - auto& shapedInfo = shapedInfos[i]; + shapedInfos.push_back(std::move(info)); + } - if (shapedInfo.runs.empty()) { + // Apply TextLayout and create TextBlobs + for (auto& info : shapedInfos) { + if (info.glyphIDs.empty()) { continue; } - // Calculate alignment offset from TextLayout - float alignOffset = 0; - float positionOffsetX = 0; - float positionOffsetY = 0; + float xOffset = 0; + float yOffset = 0; if (textLayout != nullptr) { - alignOffset = calculateLayoutOffset(textLayout, shapedInfo.totalWidth); + xOffset = calculateLayoutOffset(textLayout, info.totalWidth); - // If TextLayout has a non-zero position, it overrides Text.position. - // Calculate the offset needed to move from Text.position to TextLayout.position. if (textLayout->position.x != 0 || textLayout->position.y != 0) { - positionOffsetX = textLayout->position.x - text->position.x; - positionOffsetY = textLayout->position.y - text->position.y; + xOffset += textLayout->position.x - info.text->position.x; + yOffset = textLayout->position.y - info.text->position.y; } } - // GlyphRun positions are relative to Text.position (applied by LayerBuilder). - // We only need to include alignment offset and any position override from TextLayout. - float xOffset = alignOffset + positionOffsetX; - float yOffset = positionOffsetY; + // Apply offsets to positions + std::vector adjustedPositions = {}; + adjustedPositions.reserve(info.xPositions.size()); + for (float x : info.xPositions) { + adjustedPositions.push_back(x + xOffset); + } - // Create GlyphRuns for each shaped run (different fonts) - createGlyphRuns(text, shapedInfo, xOffset, yOffset); - _textTypeset = true; - } - } + // Build TextBlob + tgfx::TextBlobBuilder builder = {}; + auto& buffer = builder.allocRunPosH(info.font, info.glyphIDs.size(), yOffset); + memcpy(buffer.glyphs, info.glyphIDs.data(), info.glyphIDs.size() * sizeof(tgfx::GlyphID)); + memcpy(buffer.positions, adjustedPositions.data(), adjustedPositions.size() * sizeof(float)); - float calculateLayoutOffset(const TextLayout* layout, float textWidth) { - float xOffset = 0; - switch (layout->textAlign) { - case TextAlign::Start: - // No offset needed - break; - case TextAlign::Center: - xOffset = -0.5f * textWidth; - break; - case TextAlign::End: - xOffset = -textWidth; - break; - case TextAlign::Justify: - // Justify requires more complex handling, treat as start for now - break; + auto textBlob = builder.build(); + if (textBlob != nullptr) { + result.add(info.text, textBlob); + } } - return xOffset; } - ShapedTextInfo shapeText(Text* text) { - ShapedTextInfo info = {}; - info.text = text; - - // Find primary typeface for this text + void shapeText(Text* text, ShapedInfo& info) { auto primaryTypeface = findTypeface(text->fontFamily, text->fontStyle); - if (!primaryTypeface) { - return info; + if (primaryTypeface == nullptr) { + return; } tgfx::Font primaryFont(primaryTypeface, text->fontSize); + info.font = primaryFont; float currentX = 0; const std::string& content = text->text; - // Current run being built - ShapedGlyphRun* currentRun = nullptr; - std::shared_ptr currentTypeface = nullptr; - - // Simple text shaping: iterate through characters size_t i = 0; while (i < content.size()) { // Decode UTF-8 character @@ -402,135 +236,95 @@ class TypesetterImpl : public Typesetter { continue; } - // Try to find a typeface that can render this character - std::shared_ptr glyphTypeface = nullptr; - tgfx::Font glyphFont; - tgfx::GlyphID glyphID = 0; + // Try to find glyph in primary font or fallbacks + tgfx::GlyphID glyphID = primaryFont.getGlyphID(unichar); + tgfx::Font glyphFont = primaryFont; - // First try primary font - glyphID = primaryFont.getGlyphID(unichar); - if (glyphID != 0) { - glyphTypeface = primaryTypeface; - glyphFont = primaryFont; - } else { - // Try fallback typefaces - for (const auto& fallback : _fallbackTypefaces) { - if (!fallback || fallback == primaryTypeface) { + if (glyphID == 0) { + for (const auto& fallback : typesetter->fallbackTypefaces) { + if (fallback == nullptr || fallback == primaryTypeface) { continue; } tgfx::Font fallbackFont(fallback, text->fontSize); glyphID = fallbackFont.getGlyphID(unichar); if (glyphID != 0) { - glyphTypeface = fallback; glyphFont = fallbackFont; + // Note: for simplicity, we use the primary font for all glyphs. + // A more complete implementation would track font changes per run. break; } } } - if (glyphID == 0 || !glyphTypeface) { - // No font can render this character, skip it + if (glyphID == 0) { continue; } - // Get glyph advance float advance = glyphFont.getAdvance(glyphID); - // Check if glyph has renderable content (outline or image) - tgfx::Path testPath; + // Check if glyph has renderable content + tgfx::Path testPath = {}; bool hasOutline = glyphFont.getPath(glyphID, &testPath) && !testPath.isEmpty(); bool hasImage = glyphFont.getImage(glyphID, nullptr, nullptr) != nullptr; if (hasOutline || hasImage) { - // Check if we need to start a new run (different typeface) - if (currentTypeface != glyphTypeface) { - info.runs.emplace_back(); - currentRun = &info.runs.back(); - currentRun->typeface = glyphTypeface; - currentRun->font = glyphFont; - currentTypeface = glyphTypeface; - } - - currentRun->xPositions.push_back(currentX); - currentRun->glyphIDs.push_back(glyphID); + info.xPositions.push_back(currentX); + info.glyphIDs.push_back(glyphID); } - // Always advance position currentX += advance + text->letterSpacing; } info.totalWidth = currentX; - return info; } - void createGlyphRuns(Text* text, const ShapedTextInfo& info, float xOffset, float yOffset) { - for (const auto& run : info.runs) { - if (run.glyphIDs.empty()) { - continue; - } - - // Get or create Font resource for this typeface - std::string fontId = getOrCreateFontResource(run.typeface, run.font, run.glyphIDs); - - // Create GlyphRun with remapped glyph IDs - auto glyphRun = _document->makeNode(); - glyphRun->font = _fontResources[fontId]; - - // Remap glyph IDs to font-specific indices - for (tgfx::GlyphID glyphID : run.glyphIDs) { - auto it = _glyphMapping[fontId].find(glyphID); - if (it != _glyphMapping[fontId].end()) { - glyphRun->glyphs.push_back(it->second); - } else { - glyphRun->glyphs.push_back(0); // Missing glyph - } - } - - // Apply layout offset to x positions - glyphRun->xPositions.reserve(run.xPositions.size()); - for (float x : run.xPositions) { - glyphRun->xPositions.push_back(x + xOffset); - } - - // Apply y offset (for TextLayout position override) - glyphRun->y = yOffset; - - text->glyphRuns.push_back(glyphRun); + float calculateLayoutOffset(const TextLayout* layout, float textWidth) { + switch (layout->textAlign) { + case TextAlign::Start: + return 0; + case TextAlign::Center: + return -0.5f * textWidth; + case TextAlign::End: + return -textWidth; + case TextAlign::Justify: + // TODO: Justify requires more complex handling + return 0; } + return 0; } std::shared_ptr findTypeface(const std::string& fontFamily, const std::string& fontStyle) { // First, try exact match from registered typefaces if (!fontFamily.empty()) { - FontKey key; + FontKey key = {}; key.family = fontFamily; key.style = fontStyle.empty() ? "Regular" : fontStyle; - auto it = _registeredTypefaces.find(key); - if (it != _registeredTypefaces.end()) { + auto it = typesetter->registeredTypefaces.find(key); + if (it != typesetter->registeredTypefaces.end()) { return it->second; } // Try matching family only (any style) - for (const auto& pair : _registeredTypefaces) { + for (const auto& pair : typesetter->registeredTypefaces) { if (pair.first.family == fontFamily) { return pair.second; } } } - // Then, try fallback typefaces + // Then, try fallback typefaces by family name if (!fontFamily.empty()) { - for (const auto& tf : _fallbackTypefaces) { - if (tf && tf->fontFamily() == fontFamily) { + for (const auto& tf : typesetter->fallbackTypefaces) { + if (tf != nullptr && tf->fontFamily() == fontFamily) { return tf; } } } // Use first fallback typeface - if (!_fallbackTypefaces.empty()) { - return _fallbackTypefaces[0]; + if (!typesetter->fallbackTypefaces.empty()) { + return typesetter->fallbackTypefaces[0]; } // Last resort: try system font @@ -541,154 +335,14 @@ class TypesetterImpl : public Typesetter { return nullptr; } - std::string getOrCreateFontResource(const std::shared_ptr& typeface, - const tgfx::Font& font, - const std::vector& glyphIDs) { - // Create a key combining typeface pointer and font size for lookup. - // Different font sizes produce different glyph paths, so they need separate Font resources. - std::string lookupKey = std::to_string(reinterpret_cast(typeface.get())) + "_" + - std::to_string(static_cast(font.getSize())); - - // Check if we already have a font ID for this key - auto keyIt = _fontKeyToId.find(lookupKey); - if (keyIt != _fontKeyToId.end()) { - // Font resource already exists, just add any new glyphs - std::string fontId = keyIt->second; - addGlyphsToFont(fontId, font, glyphIDs); - return fontId; - } - - // Create new font ID using incremental counter - std::string fontId = "font_" + std::to_string(_nextFontId++); - _fontKeyToId[lookupKey] = fontId; - - // Create new Font resource with the ID so it gets registered in nodeMap - auto fontNode = _document->makeNode(fontId); - _fontResources[fontId] = fontNode; - _glyphMapping[fontId] = {}; - - // Add glyphs to the new font - addGlyphsToFont(fontId, font, glyphIDs); - return fontId; - } - - void addGlyphsToFont(const std::string& fontId, const tgfx::Font& font, - const std::vector& glyphIDs) { - Font* fontNode = _fontResources[fontId]; - auto& glyphMap = _glyphMapping[fontId]; - - for (tgfx::GlyphID glyphID : glyphIDs) { - if (glyphMap.find(glyphID) != glyphMap.end()) { - continue; // Already added - } - - // Try to get glyph path first (vector outline) - tgfx::Path glyphPath; - bool hasPath = font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); - - // Try to get glyph image (bitmap, e.g., color emoji) - tgfx::Matrix imageMatrix; - auto imageCodec = font.getImage(glyphID, nullptr, &imageMatrix); - - if (!hasPath && !imageCodec) { - continue; // No renderable content - } - - // Create Glyph node - auto glyph = _document->makeNode(); - - if (hasPath) { - // Vector glyph - std::string pathStr = PathToSVGString(glyphPath); - if (!pathStr.empty()) { - glyph->path = _document->makeNode(); - *glyph->path = PathDataFromSVGString(pathStr); - } - } else if (imageCodec) { - // Bitmap glyph (e.g., color emoji) - // The imageMatrix contains scale and translation to transform the glyph image. - // We scale the image to the target size during encoding, so only offset is needed at render. - int srcW = imageCodec->width(); - int srcH = imageCodec->height(); - float scaleX = imageMatrix.getScaleX(); - float scaleY = imageMatrix.getScaleY(); - int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); - int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); - if (dstW > 0 && dstH > 0) { - // Read original pixels first - tgfx::Bitmap srcBitmap(srcW, srcH, false, false); - if (!srcBitmap.isEmpty()) { - auto* srcPixels = srcBitmap.lockPixels(); - if (srcPixels && imageCodec->readPixels(srcBitmap.info(), srcPixels)) { - srcBitmap.unlockPixels(); - // Scale using bilinear interpolation for smooth downscaling - tgfx::Bitmap dstBitmap(dstW, dstH, false, false); - if (!dstBitmap.isEmpty()) { - auto* dstPixels = dstBitmap.lockPixels(); - if (dstPixels) { - auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); - ScalePixelsBilinear(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), - static_cast(dstPixels), dstW, dstH, - dstBitmap.info().rowBytes()); - srcBitmap.unlockPixels(); - dstBitmap.unlockPixels(); - auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); - if (pngData) { - auto image = _document->makeNode(); - image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); - glyph->image = image; - // Store offset; scale is already applied to the image - glyph->offset.x = imageMatrix.getTranslateX(); - glyph->offset.y = imageMatrix.getTranslateY(); - } - } else { - dstBitmap.unlockPixels(); - } - } - } else { - srcBitmap.unlockPixels(); - } - } - } - } - - // Only add glyph if it has content - if (glyph->path || glyph->image) { - // Map original glyph ID to new index (1-based, since PathTypefaceBuilder uses 1-based IDs) - glyphMap[glyphID] = static_cast(fontNode->glyphs.size() + 1); - fontNode->glyphs.push_back(glyph); - } - } - } - - // Registered typefaces by family + style - std::unordered_map, FontKeyHash> _registeredTypefaces = - {}; - - // Fallback typefaces - std::vector> _fallbackTypefaces = {}; - - // Current processing state - PAGXDocument* _document = nullptr; - bool _force = false; - bool _textTypeset = false; - - // Font ID -> Font resource - std::unordered_map _fontResources = {}; - - // Font ID -> (original GlyphID -> new GlyphID) - std::unordered_map> _glyphMapping = - {}; - - // Lookup key (typeface_ptr + font_size) -> Font ID - std::unordered_map _fontKeyToId = {}; - - // Counter for generating incremental font IDs - int _nextFontId = 0; + const Typesetter* typesetter = nullptr; + PAGXDocument* document = nullptr; + TextGlyphs result = {}; }; -std::shared_ptr Typesetter::Make() { - return std::make_shared(); +TextGlyphs Typesetter::createTextGlyphs(PAGXDocument* document) { + TypesetterContext context(this, document); + return context.run(); } } // namespace pagx diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 6d379710b5..08aa6d0e89 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -26,6 +26,8 @@ #include "pagx/PAGXImporter.h" #include "pagx/SVGImporter.h" #include "pagx/Typesetter.h" +#include "pagx/FontEmbedder.h" +#include "pagx/TextGlyphs.h" #include "../../pagx/src/StringParser.h" #include "../../pagx/src/SVGPathParser.h" #include "pagx/nodes/BlurFilter.h" @@ -122,8 +124,8 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { auto textShaper = TextShaper::Make(GetFallbackTypefaces()); // Create Typesetter for text shaping - auto typesetter = pagx::Typesetter::Make(); - typesetter->setFallbackTypefaces(GetFallbackTypefaces()); + pagx::Typesetter typesetter; + typesetter.setFallbackTypefaces(GetFallbackTypefaces()); for (const auto& svgPath : svgFiles) { std::string baseName = std::filesystem::path(svgPath).stem().string(); @@ -140,16 +142,21 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { continue; } - // Step 2: Typeset text elements - typesetter->typeset(doc.get()); + // Step 2: Typeset text elements and embed fonts + auto textGlyphs = typesetter.createTextGlyphs(doc.get()); + pagx::FontEmbedder::Embed(doc.get(), textGlyphs); // Step 3: Export to XML and save as PAGX file std::string xml = pagx::PAGXExporter::ToXML(*doc); std::string pagxPath = SavePAGXFile(xml, "PAGXTest/" + baseName + ".pagx"); // Step 4: Load PAGX file and build layer tree (this is the viewer's actual path) - auto content = pagx::LayerBuilder::FromFile(pagxPath); - if (content.root == nullptr) { + auto reloadedDoc = pagx::PAGXImporter::FromFile(pagxPath); + if (reloadedDoc == nullptr) { + continue; + } + auto layer = pagx::LayerBuilder::Build(*reloadedDoc); + if (layer == nullptr) { continue; } @@ -186,7 +193,7 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { DisplayList displayList; auto container = tgfx::Layer::Make(); container->setMatrix(tgfx::Matrix::MakeScale(scale, scale)); - container->addChild(content.root); + container->addChild(layer); displayList.root()->addChild(container); displayList.render(pagxSurface.get(), false); EXPECT_TRUE(Baseline::Compare(pagxSurface, "PAGXTest/" + baseName + "_pagx")); @@ -226,15 +233,15 @@ PAG_TEST(PAGXTest, ColorRefRender) { auto context = device->lockContext(); ASSERT_TRUE(context != nullptr); - auto content = pagx::LayerBuilder::FromData(reinterpret_cast(pagxXml), - strlen(pagxXml)); - ASSERT_TRUE(content.root != nullptr); - EXPECT_FLOAT_EQ(content.width, 200.0f); - EXPECT_FLOAT_EQ(content.height, 200.0f); + auto doc = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), + strlen(pagxXml)); + ASSERT_TRUE(doc != nullptr); + auto layer = pagx::LayerBuilder::Build(*doc); + ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 200); DisplayList displayList; - displayList.root()->addChild(content.root); + displayList.root()->addChild(layer); displayList.render(surface.get(), false); EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/ColorRefRender")); @@ -271,14 +278,15 @@ PAG_TEST(PAGXTest, StrokeColorRefRender) { auto context = device->lockContext(); ASSERT_TRUE(context != nullptr); - pagx::LayerBuilder::Options options; - auto content = pagx::LayerBuilder::FromData(reinterpret_cast(pagxXml), - strlen(pagxXml), options); - ASSERT_TRUE(content.root != nullptr); + auto doc = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), + strlen(pagxXml)); + ASSERT_TRUE(doc != nullptr); + auto layer = pagx::LayerBuilder::Build(*doc); + ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 200); DisplayList displayList; - displayList.root()->addChild(content.root); + displayList.root()->addChild(layer); displayList.render(surface.get(), false); EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/StrokeColorRefRender")); @@ -286,7 +294,7 @@ PAG_TEST(PAGXTest, StrokeColorRefRender) { } /** - * Test case: Verify LayerBuilder::FromFile and FromData produce identical results. + * Test case: Verify PAGXImporter::FromFile and FromXML produce identical results when rendered. */ PAG_TEST(PAGXTest, LayerBuilderAPIConsistency) { const char* pagxXml = R"( @@ -308,26 +316,28 @@ PAG_TEST(PAGXTest, LayerBuilderAPIConsistency) { auto context = device->lockContext(); ASSERT_TRUE(context != nullptr); - pagx::LayerBuilder::Options options; - // Load via FromFile - auto contentFromFile = pagx::LayerBuilder::FromFile(pagxPath, options); - ASSERT_TRUE(contentFromFile.root != nullptr); - - // Load via FromData - auto contentFromData = pagx::LayerBuilder::FromData(reinterpret_cast(pagxXml), - strlen(pagxXml), options); - ASSERT_TRUE(contentFromData.root != nullptr); + auto docFromFile = pagx::PAGXImporter::FromFile(pagxPath); + ASSERT_TRUE(docFromFile != nullptr); + auto layerFromFile = pagx::LayerBuilder::Build(*docFromFile); + ASSERT_TRUE(layerFromFile != nullptr); + + // Load via FromXML + auto docFromData = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), + strlen(pagxXml)); + ASSERT_TRUE(docFromData != nullptr); + auto layerFromData = pagx::LayerBuilder::Build(*docFromData); + ASSERT_TRUE(layerFromData != nullptr); // Render both and compare auto surfaceFile = Surface::Make(context, 100, 100); DisplayList displayListFile; - displayListFile.root()->addChild(contentFromFile.root); + displayListFile.root()->addChild(layerFromFile); displayListFile.render(surfaceFile.get(), false); auto surfaceData = Surface::Make(context, 100, 100); DisplayList displayListData; - displayListData.root()->addChild(contentFromData.root); + displayListData.root()->addChild(layerFromData); displayListData.render(surfaceData.get(), false); // Both should match the same baseline @@ -789,14 +799,13 @@ PAG_TEST(PAGXTest, PrecomposedTextRender) { auto context = device->lockContext(); ASSERT_TRUE(context != nullptr); - pagx::LayerBuilder::Options options; - auto content = pagx::LayerBuilder::FromData(reinterpret_cast(pagxXml), - strlen(pagxXml), options); - ASSERT_TRUE(content.root != nullptr); + // Build layer tree (doc already parsed above) + auto layer = pagx::LayerBuilder::Build(*doc); + ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 100); DisplayList displayList; - displayList.root()->addChild(content.root); + displayList.root()->addChild(layer); displayList.render(surface.get(), false); EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/PrecomposedTextRender")); @@ -831,14 +840,15 @@ PAG_TEST(PAGXTest, PrecomposedTextPointPositions) { auto context = device->lockContext(); ASSERT_TRUE(context != nullptr); - pagx::LayerBuilder::Options options; - auto content = pagx::LayerBuilder::FromData(reinterpret_cast(pagxXml), - strlen(pagxXml), options); - ASSERT_TRUE(content.root != nullptr); + auto doc = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), + strlen(pagxXml)); + ASSERT_TRUE(doc != nullptr); + auto layer = pagx::LayerBuilder::Build(*doc); + ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 150); DisplayList displayList; - displayList.root()->addChild(content.root); + displayList.root()->addChild(layer); displayList.render(surface.get(), false); EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/PrecomposedTextPointPositions")); @@ -872,14 +882,15 @@ PAG_TEST(PAGXTest, PrecomposedTextMissingGlyph) { auto context = device->lockContext(); ASSERT_TRUE(context != nullptr); - pagx::LayerBuilder::Options options; - auto content = pagx::LayerBuilder::FromData(reinterpret_cast(pagxXml), - strlen(pagxXml), options); - ASSERT_TRUE(content.root != nullptr); + auto doc = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), + strlen(pagxXml)); + ASSERT_TRUE(doc != nullptr); + auto layer = pagx::LayerBuilder::Build(*doc); + ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 100); DisplayList displayList; - displayList.root()->addChild(content.root); + displayList.root()->addChild(layer); displayList.render(surface.get(), false); // GlyphID 0 should not be rendered, so only two glyphs appear EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/PrecomposedTextMissingGlyph")); @@ -974,12 +985,12 @@ PAG_TEST(PAGXTest, TextShaperRoundTrip) { auto typefaces = GetFallbackTypefaces(); ASSERT_FALSE(typefaces.empty()); - // Step 2: Typeset text - auto typesetter = pagx::Typesetter::Make(); - ASSERT_TRUE(typesetter != nullptr); - typesetter->setFallbackTypefaces(typefaces); - bool typeset = typesetter->typeset(doc.get()); - EXPECT_TRUE(typeset); + // Step 2: Typeset text and embed fonts + pagx::Typesetter typesetter; + typesetter.setFallbackTypefaces(typefaces); + auto textGlyphs = typesetter.createTextGlyphs(doc.get()); + EXPECT_FALSE(textGlyphs.empty()); + pagx::FontEmbedder::Embed(doc.get(), textGlyphs); // Verify Font resources were added bool hasFontResource = false; @@ -994,12 +1005,12 @@ PAG_TEST(PAGXTest, TextShaperRoundTrip) { EXPECT_TRUE(hasFontResource); // Step 3: Render typeset document - auto originalContent = pagx::LayerBuilder::Build(*doc); - ASSERT_TRUE(originalContent.root != nullptr); + auto originalLayer = pagx::LayerBuilder::Build(*doc, &textGlyphs); + ASSERT_TRUE(originalLayer != nullptr); auto originalSurface = Surface::Make(context, canvasWidth, canvasHeight); DisplayList originalDL; - originalDL.root()->addChild(originalContent.root); + originalDL.root()->addChild(originalLayer); originalDL.render(originalSurface.get(), false); // Step 4: Export to PAGX @@ -1045,15 +1056,14 @@ PAG_TEST(PAGXTest, TextShaperRoundTrip) { } EXPECT_TRUE(glyphRunFound); - pagx::LayerBuilder::Options reloadOptions; - // Intentionally not providing fallback typefaces to verify embedded font works - auto reloadedContent = pagx::LayerBuilder::Build(*reloadedDoc, reloadOptions); - ASSERT_TRUE(reloadedContent.root != nullptr); + // Intentionally not providing TextGlyphs to verify embedded font works + auto reloadedLayer = pagx::LayerBuilder::Build(*reloadedDoc); + ASSERT_TRUE(reloadedLayer != nullptr); // Step 6: Render pre-shaped version auto preshapedSurface = Surface::Make(context, canvasWidth, canvasHeight); DisplayList preshapedDL; - preshapedDL.root()->addChild(reloadedContent.root); + preshapedDL.root()->addChild(reloadedLayer); preshapedDL.render(preshapedSurface.get(), false); // Step 7: Compare renders - they should be identical @@ -1088,33 +1098,35 @@ PAG_TEST(PAGXTest, TextShaperMultipleText) { auto typefaces = GetFallbackTypefaces(); - // Typeset text - auto typesetter = pagx::Typesetter::Make(); - ASSERT_TRUE(typesetter != nullptr); - typesetter->setFallbackTypefaces(typefaces); - bool typeset = typesetter->typeset(doc.get()); - EXPECT_TRUE(typeset); + // Typeset text and embed fonts + pagx::Typesetter typesetter; + typesetter.setFallbackTypefaces(typefaces); + auto textGlyphs = typesetter.createTextGlyphs(doc.get()); + EXPECT_FALSE(textGlyphs.empty()); + pagx::FontEmbedder::Embed(doc.get(), textGlyphs); // Render typeset document - auto originalContent = pagx::LayerBuilder::Build(*doc); - ASSERT_TRUE(originalContent.root != nullptr); + auto originalLayer = pagx::LayerBuilder::Build(*doc, &textGlyphs); + ASSERT_TRUE(originalLayer != nullptr); auto originalSurface = Surface::Make(context, canvasWidth, canvasHeight); DisplayList originalDL; - originalDL.root()->addChild(originalContent.root); + originalDL.root()->addChild(originalLayer); originalDL.render(originalSurface.get(), false); // Export and reload std::string xml = pagx::PAGXExporter::ToXML(*doc); std::string pagxPath = SavePAGXFile(xml, "PAGXTest/textFont_preshaped.pagx"); - auto reloadedContent = pagx::LayerBuilder::FromFile(pagxPath); - ASSERT_TRUE(reloadedContent.root != nullptr); + auto reloadedDoc = pagx::PAGXImporter::FromFile(pagxPath); + ASSERT_TRUE(reloadedDoc != nullptr); + auto reloadedLayer = pagx::LayerBuilder::Build(*reloadedDoc); + ASSERT_TRUE(reloadedLayer != nullptr); // Render pre-shaped auto preshapedSurface = Surface::Make(context, canvasWidth, canvasHeight); DisplayList preshapedDL; - preshapedDL.root()->addChild(reloadedContent.root); + preshapedDL.root()->addChild(reloadedLayer); preshapedDL.render(preshapedSurface.get(), false); EXPECT_TRUE(Baseline::Compare(originalSurface, "PAGXTest/TextShaperMultiple_Original")); @@ -1407,20 +1419,19 @@ PAG_TEST(PAGXTest, CompleteExample) { } } - // Typeset text elements - auto typesetter = pagx::Typesetter::Make(); - typesetter->setFallbackTypefaces(GetFallbackTypefaces()); - typesetter->typeset(doc.get()); + // Typeset text elements and embed fonts + pagx::Typesetter typesetter; + typesetter.setFallbackTypefaces(GetFallbackTypefaces()); + auto textGlyphs = typesetter.createTextGlyphs(doc.get()); + pagx::FontEmbedder::Embed(doc.get(), textGlyphs); // Build layer tree - auto content = pagx::LayerBuilder::Build(*doc); - ASSERT_TRUE(content.root != nullptr); - EXPECT_FLOAT_EQ(content.width, 800.0f); - EXPECT_FLOAT_EQ(content.height, 520.0f); + auto layer = pagx::LayerBuilder::Build(*doc); + ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 800, 520); DisplayList displayList; - displayList.root()->addChild(content.root); + displayList.root()->addChild(layer); displayList.render(surface.get(), false); EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/CompleteExample")); From 7d953e7caff22b2e8f624e3b6a0814f0b36fca8d Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 22:37:51 +0800 Subject: [PATCH 251/678] Polish remaining non-native English expressions in PAGX specification. --- pagx/spec/pagx_spec.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 21d4138817..3c32097a17 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -18,7 +18,7 @@ ### 1.2 File Structure -PAGX is a pure XML file (`.pagx`) that can reference external resource files (images, videos, audio, fonts, etc.) or embed resources via data URIs. PAGX and binary PAG formats are bidirectionally convertible: convert to PAG for optimized loading performance during publishing; use PAGX format for reading and editing during development, review, or editing. +PAGX is a plain XML file (`.pagx`) that can reference external resource files (images, videos, audio, fonts, etc.) or embed resources via data URIs. PAGX and binary PAG formats are bidirectionally convertible: convert to PAG for optimized loading performance during publishing; use PAGX format for reading and editing during development and review. ### 1.3 Document Organization @@ -145,7 +145,7 @@ Matrix form: ### 2.8 Color -PAGX supports two color representation methods: +PAGX supports two color formats: #### HEX Format (Hexadecimal) @@ -171,7 +171,7 @@ Floating-point format uses `colorspace(r, g, b)` or `colorspace(r, g, b, a)` to **Notes**: - Color space identifiers (`srgb` or `p3`) and parentheses **cannot be omitted** - Wide color gamut (Display P3) component values may exceed the [0, 1] range to represent colors outside the sRGB gamut -- sRGB floating-point format and HEX format represent the same color space; choose as needed +- sRGB floating-point format and HEX format represent the same color space; use whichever best suits your needs #### Color Source Reference @@ -274,7 +274,7 @@ PAGX uses a standard 2D Cartesian coordinate system: #### 3.3.1 Image -Image resources define bitmap data that can be referenced throughout the document. +Image resources define bitmap data for use throughout the document. ```xml @@ -485,7 +485,7 @@ Compositions are used for content reuse (similar to After Effects pre-comps). #### 3.3.5 Font -Font defines embedded font resources containing subsetted glyph data (vector outlines or bitmaps). PAGX files achieve complete self-containment through embedded glyph data, ensuring cross-platform rendering consistency. +Font defines embedded font resources containing subsetted glyph data (vector outlines or bitmaps). Embedding glyph data makes PAGX files fully self-contained, ensuring consistent rendering across platforms. ```xml @@ -555,7 +555,7 @@ PAGX documents organize content in a hierarchical structure: ## 4. Layer System -Layers are the fundamental organizational units for PAGX content, providing rich visual effect control capabilities. +Layers are the fundamental organizational units for PAGX content, offering comprehensive control over visual effects. ### 4.1 Core Concepts @@ -700,7 +700,7 @@ Blend modes define how source color (S) combines with destination color (D). ### 4.3 Layer Styles -Layer styles add visual effects above or below layer content without replacing the original content. +Layer styles add visual effects above or below layer content without modifying the original. **Input Sources for Layer Styles**: @@ -784,7 +784,7 @@ Draws an inner shadow **above** the layer, appearing inside the layer content. C Layer filters are the final stage of layer rendering. All previously rendered results (including layer styles) accumulated in order serve as filter input. Filters are applied in chain fashion according to document order, with each filter's output becoming the next filter's input. -Key difference from layer styles (Section 4.3): Layer styles **independently render** visual effects above or below layer content, while filters **modify** the layer's overall rendering output. Layer styles are applied before filters. +Unlike layer styles (Section 4.3), which **independently render** visual effects above or below layer content, filters **modify** the layer's overall rendering output. Layer styles are applied before filters. ```xml @@ -806,7 +806,7 @@ Key difference from layer styles (Section 4.3): Layer styles **independently ren #### 4.4.2 DropShadowFilter -Generates shadow effect based on filter input. Key difference from DropShadowStyle: the filter projects from original rendering content and preserves semi-transparency, while the style projects from opaque layer content. Additionally, the two support different attribute features. +Generates shadow effect based on filter input. Unlike DropShadowStyle, the filter projects from original rendering content and preserves semi-transparency, whereas the style projects from opaque layer content. The two also support different attribute features. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| @@ -1071,7 +1071,7 @@ y = center.y + outerRadius * sin(angle) **Fractional Point Count**: - `pointCount` supports decimal values (e.g., `5.5`) -- The fractional part represents the "completion degree" of the last vertex, producing an incomplete final corner +- The fractional part determines how much of the final vertex is drawn, producing an incomplete corner - `pointCount <= 0` generates no path **Roundness**: @@ -1133,7 +1133,7 @@ Line 3]]> ``` -**Rendering Modes**: Text supports **pre-layout** and **runtime layout** modes. Pre-layout provides pre-computed glyphs and positions via GlyphRun child nodes, rendering with embedded fonts for cross-platform consistency. Runtime layout performs shaping and layout at runtime; due to platform differences in fonts and layout features, minor inconsistencies may occur. For exact reproduction of design tool layouts, pre-layout is recommended. +**Rendering Modes**: Text supports **pre-layout** and **runtime layout** modes. Pre-layout provides pre-computed glyphs and positions via GlyphRun child nodes, rendering with embedded fonts for cross-platform consistency. Runtime layout performs shaping and layout at runtime; due to platform differences in fonts and layout features, minor inconsistencies may occur. For pixel-perfect reproduction of design tool layouts, pre-layout is recommended. **Runtime Layout Rendering Flow**: 1. Find system font based on `fontFamily` and `fontStyle`; if unavailable, select fallback font according to runtime-configured fallback list @@ -1745,7 +1745,7 @@ Rich text is achieved through multiple Text elements within a Group, each Text h ``` -**Note**: Each Group's Text + Fill/Stroke defines a text segment with independent styling. TextLayout treats all segments as a whole for typography, enabling auto-wrapping and alignment. +**Note**: Each Group's Text + Fill/Stroke defines a text segment with independent styling. TextLayout treats all segments as a single unit for typography, enabling auto-wrapping and alignment. ### 5.6 Repeater @@ -1796,7 +1796,7 @@ When `copies` is a decimal (e.g., `3.5`), partial copies are achieved through ** 1. **Geometry copying**: Shapes and text are copied by `ceil(copies)` (i.e., 4), geometry itself is not scaled or clipped 2. **Opacity adjustment**: The last copy's opacity is multiplied by the fractional part (e.g., 0.5), producing semi-transparent effect -3. **Visual effect**: Simulates "partially existing" copies through opacity gradation +3. **Visual effect**: Simulates partial copies through opacity gradation **Example**: When `copies="2.3"`: - Copy 3 complete geometry copies From 9e8a955d5f50a5b9d4fca83c44a75642b3261d2a Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 22:45:01 +0800 Subject: [PATCH 252/678] Simplify FontEmbedder to match original Typesetter font handling logic. --- pagx/src/tgfx/FontEmbedder.cpp | 410 ++++++++++++++------------------- 1 file changed, 177 insertions(+), 233 deletions(-) diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index f8ef06937f..2f9d59c71c 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -113,29 +113,6 @@ static std::string PathToSVGString(const tgfx::Path& path) { return result; } -// Key for identifying unique glyphs: typeface pointer + glyph ID. -struct GlyphKey { - const tgfx::Typeface* typeface = nullptr; - tgfx::GlyphID glyphID = 0; - - bool operator==(const GlyphKey& other) const { - return typeface == other.typeface && glyphID == other.glyphID; - } -}; - -struct GlyphKeyHash { - size_t operator()(const GlyphKey& key) const { - return std::hash()(key.typeface) ^ (std::hash()(key.glyphID) << 1); - } -}; - -// Information about a glyph to be embedded. -struct GlyphInfo { - std::string pathString = {}; - std::shared_ptr imageCodec = nullptr; - tgfx::Matrix imageMatrix = {}; -}; - // Internal implementation class for FontEmbedder. class FontEmbedderImpl { public: @@ -147,18 +124,7 @@ class FontEmbedderImpl { return false; } - // First pass: collect all glyphs and create Font nodes - textGlyphs.forEach([this](Text* text, std::shared_ptr textBlob) { - if (textBlob == nullptr) { - return; - } - collectGlyphs(textBlob); - }); - - // Create Font nodes if we have glyphs - createFontNodes(); - - // Second pass: create GlyphRuns for each Text + // Process each Text and create GlyphRuns with embedded fonts textGlyphs.forEach([this](Text* text, std::shared_ptr textBlob) { if (textBlob == nullptr) { return; @@ -170,241 +136,219 @@ class FontEmbedderImpl { } private: - void collectGlyphs(const std::shared_ptr& textBlob) { - for (const auto& run : *textBlob) { - auto* typeface = run.font.getTypeface().get(); - for (size_t i = 0; i < run.glyphCount; ++i) { - tgfx::GlyphID glyphID = run.glyphs[i]; - GlyphKey key = {typeface, glyphID}; + void createGlyphRuns(Text* text, const std::shared_ptr& textBlob) { + // Clear existing glyph runs + text->glyphRuns.clear(); - if (vectorGlyphMapping.find(key) != vectorGlyphMapping.end() || - bitmapGlyphMapping.find(key) != bitmapGlyphMapping.end()) { - continue; // Already processed - } + for (const auto& run : *textBlob) { + if (run.glyphCount == 0) { + continue; + } - // Try to get glyph path first (vector outline) - tgfx::Path glyphPath = {}; - bool hasPath = run.font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); + // Get or create Font resource for this typeface + font size combination + std::string fontId = getOrCreateFontResource(run.font, run.glyphs, run.glyphCount); + if (fontId.empty()) { + continue; + } - // Try to get glyph image (bitmap, e.g., color emoji) - tgfx::Matrix imageMatrix = {}; - auto imageCodec = run.font.getImage(glyphID, nullptr, &imageMatrix); + // Create GlyphRun with remapped glyph IDs + auto glyphRun = document->makeNode(); + glyphRun->font = fontResources[fontId]; - if (hasPath) { - // Vector glyph - GlyphInfo info = {}; - info.pathString = PathToSVGString(glyphPath); - if (!info.pathString.empty()) { - pendingVectorGlyphs.push_back({key, info}); - } - } else if (imageCodec) { - // Bitmap glyph - GlyphInfo info = {}; - info.imageCodec = imageCodec; - info.imageMatrix = imageMatrix; - pendingBitmapGlyphs.push_back({key, info}); + // Remap glyph IDs to font-specific indices + auto& glyphMap = glyphMapping[fontId]; + for (size_t i = 0; i < run.glyphCount; ++i) { + tgfx::GlyphID glyphID = run.glyphs[i]; + auto it = glyphMap.find(glyphID); + if (it != glyphMap.end()) { + glyphRun->glyphs.push_back(it->second); + } else { + glyphRun->glyphs.push_back(0); // Missing glyph } } - } - } - - void createFontNodes() { - // Create vector font if needed - if (!pendingVectorGlyphs.empty()) { - vectorFont = document->makeNode("vector_font"); - tgfx::GlyphID nextID = 1; - for (auto& [key, info] : pendingVectorGlyphs) { - auto glyph = document->makeNode(); - glyph->path = document->makeNode(); - *glyph->path = PathDataFromSVGString(info.pathString); - vectorFont->glyphs.push_back(glyph); - vectorGlyphMapping[key] = nextID++; - } - } - // Create bitmap font if needed - if (!pendingBitmapGlyphs.empty()) { - bitmapFont = document->makeNode("bitmap_font"); - tgfx::GlyphID nextID = 1; - for (auto& [key, info] : pendingBitmapGlyphs) { - auto glyph = createBitmapGlyph(info); - if (glyph != nullptr) { - bitmapFont->glyphs.push_back(glyph); - bitmapGlyphMapping[key] = nextID++; + // Copy positions based on positioning mode + switch (run.positioning) { + case tgfx::GlyphPositioning::Horizontal: { + glyphRun->y = run.offsetY; + for (size_t i = 0; i < run.glyphCount; ++i) { + glyphRun->xPositions.push_back(run.positions[i]); + } + break; + } + case tgfx::GlyphPositioning::Point: { + auto* points = reinterpret_cast(run.positions); + for (size_t i = 0; i < run.glyphCount; ++i) { + glyphRun->positions.push_back({points[i].x, points[i].y}); + } + break; + } + case tgfx::GlyphPositioning::RSXform: { + auto* xforms = reinterpret_cast(run.positions); + for (size_t i = 0; i < run.glyphCount; ++i) { + RSXform xform = {}; + xform.scos = xforms[i].scos; + xform.ssin = xforms[i].ssin; + xform.tx = xforms[i].tx; + xform.ty = xforms[i].ty; + glyphRun->xforms.push_back(xform); + } + break; + } + case tgfx::GlyphPositioning::Matrix: { + auto* matrices = reinterpret_cast(run.positions); + for (size_t i = 0; i < run.glyphCount; ++i) { + Matrix m = {}; + m.a = matrices[i * 6 + 0]; + m.b = matrices[i * 6 + 1]; + m.c = matrices[i * 6 + 2]; + m.d = matrices[i * 6 + 3]; + m.tx = matrices[i * 6 + 4]; + m.ty = matrices[i * 6 + 5]; + glyphRun->matrices.push_back(m); + } + break; } } - } - } - - Glyph* createBitmapGlyph(const GlyphInfo& info) { - auto imageCodec = info.imageCodec; - auto imageMatrix = info.imageMatrix; - - int srcW = imageCodec->width(); - int srcH = imageCodec->height(); - float scaleX = imageMatrix.getScaleX(); - float scaleY = imageMatrix.getScaleY(); - int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); - int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); - - if (dstW <= 0 || dstH <= 0) { - return nullptr; - } - - tgfx::Bitmap srcBitmap(srcW, srcH, false, false); - if (srcBitmap.isEmpty()) { - return nullptr; - } - auto* srcPixels = srcBitmap.lockPixels(); - if (srcPixels == nullptr || !imageCodec->readPixels(srcBitmap.info(), srcPixels)) { - srcBitmap.unlockPixels(); - return nullptr; + text->glyphRuns.push_back(glyphRun); } - srcBitmap.unlockPixels(); + } - tgfx::Bitmap dstBitmap(dstW, dstH, false, false); - if (dstBitmap.isEmpty()) { - return nullptr; + std::string getOrCreateFontResource(const tgfx::Font& font, const tgfx::GlyphID* glyphs, + size_t glyphCount) { + auto typeface = font.getTypeface(); + if (typeface == nullptr) { + return ""; } - auto* dstPixels = dstBitmap.lockPixels(); - if (dstPixels == nullptr) { - return nullptr; + // Create a key combining typeface pointer and font size for lookup. + // Different font sizes produce different glyph paths, so they need separate Font resources. + std::string lookupKey = std::to_string(reinterpret_cast(typeface.get())) + "_" + + std::to_string(static_cast(font.getSize())); + + // Check if we already have a font ID for this key + auto keyIt = fontKeyToId.find(lookupKey); + if (keyIt != fontKeyToId.end()) { + // Font resource already exists, just add any new glyphs + std::string fontId = keyIt->second; + addGlyphsToFont(fontId, font, glyphs, glyphCount); + return fontId; } - auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); - ScalePixelsBilinear(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), - static_cast(dstPixels), dstW, dstH, dstBitmap.info().rowBytes()); - srcBitmap.unlockPixels(); - dstBitmap.unlockPixels(); - - auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); - if (pngData == nullptr) { - return nullptr; - } + // Create new font ID using incremental counter + std::string fontId = "font_" + std::to_string(nextFontId++); + fontKeyToId[lookupKey] = fontId; - auto glyph = document->makeNode(); - auto image = document->makeNode(); - image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); - glyph->image = image; - glyph->offset.x = imageMatrix.getTranslateX(); - glyph->offset.y = imageMatrix.getTranslateY(); + // Create new Font resource with the ID so it gets registered in nodeMap + auto fontNode = document->makeNode(fontId); + fontResources[fontId] = fontNode; + glyphMapping[fontId] = {}; - return glyph; + // Add glyphs to the new font + addGlyphsToFont(fontId, font, glyphs, glyphCount); + return fontId; } - void createGlyphRuns(Text* text, const std::shared_ptr& textBlob) { - // Clear existing glyph runs - text->glyphRuns.clear(); - - for (const auto& run : *textBlob) { - auto* typeface = run.font.getTypeface().get(); - - // Separate glyphs into vector and bitmap runs - std::vector vectorIndices = {}; - std::vector bitmapIndices = {}; + void addGlyphsToFont(const std::string& fontId, const tgfx::Font& font, + const tgfx::GlyphID* glyphs, size_t glyphCount) { + Font* fontNode = fontResources[fontId]; + auto& glyphMap = glyphMapping[fontId]; - for (size_t i = 0; i < run.glyphCount; ++i) { - GlyphKey key = {typeface, run.glyphs[i]}; - if (vectorGlyphMapping.find(key) != vectorGlyphMapping.end()) { - vectorIndices.push_back(i); - } else if (bitmapGlyphMapping.find(key) != bitmapGlyphMapping.end()) { - bitmapIndices.push_back(i); - } + for (size_t i = 0; i < glyphCount; ++i) { + tgfx::GlyphID glyphID = glyphs[i]; + if (glyphMap.find(glyphID) != glyphMap.end()) { + continue; // Already added } - // Create vector glyph run - if (!vectorIndices.empty() && vectorFont != nullptr) { - auto glyphRun = createGlyphRun(run, vectorIndices, vectorFont, vectorGlyphMapping, typeface); - if (glyphRun != nullptr) { - text->glyphRuns.push_back(glyphRun); - } - } + // Try to get glyph path first (vector outline) + tgfx::Path glyphPath = {}; + bool hasPath = font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); - // Create bitmap glyph run - if (!bitmapIndices.empty() && bitmapFont != nullptr) { - auto glyphRun = createGlyphRun(run, bitmapIndices, bitmapFont, bitmapGlyphMapping, typeface); - if (glyphRun != nullptr) { - text->glyphRuns.push_back(glyphRun); - } - } - } - } + // Try to get glyph image (bitmap, e.g., color emoji) + tgfx::Matrix imageMatrix = {}; + auto imageCodec = font.getImage(glyphID, nullptr, &imageMatrix); - GlyphRun* createGlyphRun(const tgfx::GlyphRun& run, const std::vector& indices, - Font* font, - const std::unordered_map& mapping, - const tgfx::Typeface* typeface) { - auto glyphRun = document->makeNode(); - glyphRun->font = font; - - for (size_t i : indices) { - GlyphKey key = {typeface, run.glyphs[i]}; - auto it = mapping.find(key); - if (it != mapping.end()) { - glyphRun->glyphs.push_back(it->second); - } else { - glyphRun->glyphs.push_back(0); + if (!hasPath && !imageCodec) { + continue; // No renderable content } - } - // Copy positions based on positioning mode - switch (run.positioning) { - case tgfx::GlyphPositioning::Horizontal: { - glyphRun->y = run.offsetY; - for (size_t i : indices) { - glyphRun->xPositions.push_back(run.positions[i]); - } - break; - } - case tgfx::GlyphPositioning::Point: { - auto* points = reinterpret_cast(run.positions); - for (size_t i : indices) { - glyphRun->positions.push_back({points[i].x, points[i].y}); + // Create Glyph node + auto glyph = document->makeNode(); + + if (hasPath) { + // Vector glyph + std::string pathStr = PathToSVGString(glyphPath); + if (!pathStr.empty()) { + glyph->path = document->makeNode(); + *glyph->path = PathDataFromSVGString(pathStr); } - break; - } - case tgfx::GlyphPositioning::RSXform: { - auto* xforms = reinterpret_cast(run.positions); - for (size_t i : indices) { - RSXform xform = {}; - xform.scos = xforms[i].scos; - xform.ssin = xforms[i].ssin; - xform.tx = xforms[i].tx; - xform.ty = xforms[i].ty; - glyphRun->xforms.push_back(xform); + } else if (imageCodec) { + // Bitmap glyph (e.g., color emoji) + int srcW = imageCodec->width(); + int srcH = imageCodec->height(); + float scaleX = imageMatrix.getScaleX(); + float scaleY = imageMatrix.getScaleY(); + int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); + int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); + if (dstW > 0 && dstH > 0) { + tgfx::Bitmap srcBitmap(srcW, srcH, false, false); + if (!srcBitmap.isEmpty()) { + auto* srcPixels = srcBitmap.lockPixels(); + if (srcPixels && imageCodec->readPixels(srcBitmap.info(), srcPixels)) { + srcBitmap.unlockPixels(); + tgfx::Bitmap dstBitmap(dstW, dstH, false, false); + if (!dstBitmap.isEmpty()) { + auto* dstPixels = dstBitmap.lockPixels(); + if (dstPixels) { + auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); + ScalePixelsBilinear(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), + static_cast(dstPixels), dstW, dstH, + dstBitmap.info().rowBytes()); + srcBitmap.unlockPixels(); + dstBitmap.unlockPixels(); + auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); + if (pngData) { + auto image = document->makeNode(); + image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); + glyph->image = image; + glyph->offset.x = imageMatrix.getTranslateX(); + glyph->offset.y = imageMatrix.getTranslateY(); + } + } else { + dstBitmap.unlockPixels(); + } + } + } else { + srcBitmap.unlockPixels(); + } + } } - break; } - case tgfx::GlyphPositioning::Matrix: { - auto* matrices = reinterpret_cast(run.positions); - for (size_t i : indices) { - Matrix m = {}; - m.a = matrices[i * 6 + 0]; - m.b = matrices[i * 6 + 1]; - m.c = matrices[i * 6 + 2]; - m.d = matrices[i * 6 + 3]; - m.tx = matrices[i * 6 + 4]; - m.ty = matrices[i * 6 + 5]; - glyphRun->matrices.push_back(m); - } - break; + + // Only add glyph if it has content + if (glyph->path || glyph->image) { + // Map original glyph ID to new index (1-based, since PathTypefaceBuilder uses 1-based IDs) + glyphMap[glyphID] = static_cast(fontNode->glyphs.size() + 1); + fontNode->glyphs.push_back(glyph); } } - - return glyphRun; } PAGXDocument* document = nullptr; - Font* vectorFont = nullptr; - Font* bitmapFont = nullptr; - std::vector> pendingVectorGlyphs = {}; - std::vector> pendingBitmapGlyphs = {}; + // Font ID -> Font resource + std::unordered_map fontResources = {}; + + // Font ID -> (original GlyphID -> new GlyphID) + std::unordered_map> glyphMapping = + {}; + + // Lookup key (typeface_ptr + font_size) -> Font ID + std::unordered_map fontKeyToId = {}; - std::unordered_map vectorGlyphMapping = {}; - std::unordered_map bitmapGlyphMapping = {}; + // Counter for generating incremental font IDs + int nextFontId = 0; }; bool FontEmbedder::Embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { From c983b971297bffaae1f54f3545b141d2b4044128 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 22:51:27 +0800 Subject: [PATCH 253/678] Fix emoji rendering by tracking font changes per glyph run in Typesetter. --- pagx/src/tgfx/FontEmbedder.cpp | 419 +++++++++++++++++++-------------- pagx/src/tgfx/Typesetter.cpp | 64 +++-- 2 files changed, 288 insertions(+), 195 deletions(-) diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 2f9d59c71c..4de61ce4be 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -113,6 +113,34 @@ static std::string PathToSVGString(const tgfx::Path& path) { return result; } +// Key for identifying unique glyphs: typeface + fontSize + glyphID. +// Different font sizes produce different glyph paths/images, so they need separate entries. +struct GlyphKey { + const tgfx::Typeface* typeface = nullptr; + int fontSize = 0; + tgfx::GlyphID glyphID = 0; + + bool operator==(const GlyphKey& other) const { + return typeface == other.typeface && fontSize == other.fontSize && glyphID == other.glyphID; + } +}; + +struct GlyphKeyHash { + size_t operator()(const GlyphKey& key) const { + size_t h = std::hash()(key.typeface); + h ^= std::hash()(key.fontSize) << 1; + h ^= std::hash()(key.glyphID) << 2; + return h; + } +}; + +// Information about a glyph to be embedded. +struct GlyphInfo { + std::string pathString = {}; + std::shared_ptr imageCodec = nullptr; + tgfx::Matrix imageMatrix = {}; +}; + // Internal implementation class for FontEmbedder. class FontEmbedderImpl { public: @@ -124,7 +152,18 @@ class FontEmbedderImpl { return false; } - // Process each Text and create GlyphRuns with embedded fonts + // First pass: collect all glyphs from all TextBlobs + textGlyphs.forEach([this](Text*, std::shared_ptr textBlob) { + if (textBlob == nullptr) { + return; + } + collectGlyphs(textBlob); + }); + + // Create Font nodes (one for vector glyphs, one for bitmap glyphs) + createFontNodes(); + + // Second pass: create GlyphRuns for each Text with remapped glyph IDs textGlyphs.forEach([this](Text* text, std::shared_ptr textBlob) { if (textBlob == nullptr) { return; @@ -136,219 +175,249 @@ class FontEmbedderImpl { } private: - void createGlyphRuns(Text* text, const std::shared_ptr& textBlob) { - // Clear existing glyph runs - text->glyphRuns.clear(); - + void collectGlyphs(const std::shared_ptr& textBlob) { for (const auto& run : *textBlob) { - if (run.glyphCount == 0) { - continue; - } + auto* typeface = run.font.getTypeface().get(); + int fontSize = static_cast(run.font.getSize()); - // Get or create Font resource for this typeface + font size combination - std::string fontId = getOrCreateFontResource(run.font, run.glyphs, run.glyphCount); - if (fontId.empty()) { - continue; - } - - // Create GlyphRun with remapped glyph IDs - auto glyphRun = document->makeNode(); - glyphRun->font = fontResources[fontId]; - - // Remap glyph IDs to font-specific indices - auto& glyphMap = glyphMapping[fontId]; for (size_t i = 0; i < run.glyphCount; ++i) { tgfx::GlyphID glyphID = run.glyphs[i]; - auto it = glyphMap.find(glyphID); - if (it != glyphMap.end()) { - glyphRun->glyphs.push_back(it->second); - } else { - glyphRun->glyphs.push_back(0); // Missing glyph - } - } + GlyphKey key = {typeface, fontSize, glyphID}; - // Copy positions based on positioning mode - switch (run.positioning) { - case tgfx::GlyphPositioning::Horizontal: { - glyphRun->y = run.offsetY; - for (size_t i = 0; i < run.glyphCount; ++i) { - glyphRun->xPositions.push_back(run.positions[i]); - } - break; - } - case tgfx::GlyphPositioning::Point: { - auto* points = reinterpret_cast(run.positions); - for (size_t i = 0; i < run.glyphCount; ++i) { - glyphRun->positions.push_back({points[i].x, points[i].y}); - } - break; - } - case tgfx::GlyphPositioning::RSXform: { - auto* xforms = reinterpret_cast(run.positions); - for (size_t i = 0; i < run.glyphCount; ++i) { - RSXform xform = {}; - xform.scos = xforms[i].scos; - xform.ssin = xforms[i].ssin; - xform.tx = xforms[i].tx; - xform.ty = xforms[i].ty; - glyphRun->xforms.push_back(xform); - } - break; + // Skip if already collected + if (vectorGlyphMapping.find(key) != vectorGlyphMapping.end() || + bitmapGlyphMapping.find(key) != bitmapGlyphMapping.end()) { + continue; } - case tgfx::GlyphPositioning::Matrix: { - auto* matrices = reinterpret_cast(run.positions); - for (size_t i = 0; i < run.glyphCount; ++i) { - Matrix m = {}; - m.a = matrices[i * 6 + 0]; - m.b = matrices[i * 6 + 1]; - m.c = matrices[i * 6 + 2]; - m.d = matrices[i * 6 + 3]; - m.tx = matrices[i * 6 + 4]; - m.ty = matrices[i * 6 + 5]; - glyphRun->matrices.push_back(m); + + // Try to get glyph path first (vector outline) + tgfx::Path glyphPath = {}; + bool hasPath = run.font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); + + // Try to get glyph image (bitmap, e.g., color emoji) + tgfx::Matrix imageMatrix = {}; + auto imageCodec = run.font.getImage(glyphID, nullptr, &imageMatrix); + + if (hasPath) { + GlyphInfo info = {}; + info.pathString = PathToSVGString(glyphPath); + if (!info.pathString.empty()) { + pendingVectorGlyphs.push_back({key, info}); } - break; + } else if (imageCodec) { + GlyphInfo info = {}; + info.imageCodec = imageCodec; + info.imageMatrix = imageMatrix; + pendingBitmapGlyphs.push_back({key, info}); } } + } + } + + void createFontNodes() { + // Create vector font if needed + if (!pendingVectorGlyphs.empty()) { + vectorFont = document->makeNode("vector_font"); + tgfx::GlyphID nextID = 1; + for (auto& [key, info] : pendingVectorGlyphs) { + auto glyph = document->makeNode(); + glyph->path = document->makeNode(); + *glyph->path = PathDataFromSVGString(info.pathString); + vectorFont->glyphs.push_back(glyph); + vectorGlyphMapping[key] = nextID++; + } + } - text->glyphRuns.push_back(glyphRun); + // Create bitmap font if needed + if (!pendingBitmapGlyphs.empty()) { + bitmapFont = document->makeNode("bitmap_font"); + tgfx::GlyphID nextID = 1; + for (auto& [key, info] : pendingBitmapGlyphs) { + auto glyph = createBitmapGlyph(info); + if (glyph != nullptr) { + bitmapFont->glyphs.push_back(glyph); + bitmapGlyphMapping[key] = nextID++; + } + } } } - std::string getOrCreateFontResource(const tgfx::Font& font, const tgfx::GlyphID* glyphs, - size_t glyphCount) { - auto typeface = font.getTypeface(); - if (typeface == nullptr) { - return ""; + Glyph* createBitmapGlyph(const GlyphInfo& info) { + auto imageCodec = info.imageCodec; + auto imageMatrix = info.imageMatrix; + + int srcW = imageCodec->width(); + int srcH = imageCodec->height(); + float scaleX = imageMatrix.getScaleX(); + float scaleY = imageMatrix.getScaleY(); + int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); + int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); + + if (dstW <= 0 || dstH <= 0) { + return nullptr; } - // Create a key combining typeface pointer and font size for lookup. - // Different font sizes produce different glyph paths, so they need separate Font resources. - std::string lookupKey = std::to_string(reinterpret_cast(typeface.get())) + "_" + - std::to_string(static_cast(font.getSize())); - - // Check if we already have a font ID for this key - auto keyIt = fontKeyToId.find(lookupKey); - if (keyIt != fontKeyToId.end()) { - // Font resource already exists, just add any new glyphs - std::string fontId = keyIt->second; - addGlyphsToFont(fontId, font, glyphs, glyphCount); - return fontId; + tgfx::Bitmap srcBitmap(srcW, srcH, false, false); + if (srcBitmap.isEmpty()) { + return nullptr; } - // Create new font ID using incremental counter - std::string fontId = "font_" + std::to_string(nextFontId++); - fontKeyToId[lookupKey] = fontId; + auto* srcPixels = srcBitmap.lockPixels(); + if (srcPixels == nullptr || !imageCodec->readPixels(srcBitmap.info(), srcPixels)) { + srcBitmap.unlockPixels(); + return nullptr; + } + srcBitmap.unlockPixels(); - // Create new Font resource with the ID so it gets registered in nodeMap - auto fontNode = document->makeNode(fontId); - fontResources[fontId] = fontNode; - glyphMapping[fontId] = {}; + tgfx::Bitmap dstBitmap(dstW, dstH, false, false); + if (dstBitmap.isEmpty()) { + return nullptr; + } - // Add glyphs to the new font - addGlyphsToFont(fontId, font, glyphs, glyphCount); - return fontId; - } + auto* dstPixels = dstBitmap.lockPixels(); + if (dstPixels == nullptr) { + return nullptr; + } - void addGlyphsToFont(const std::string& fontId, const tgfx::Font& font, - const tgfx::GlyphID* glyphs, size_t glyphCount) { - Font* fontNode = fontResources[fontId]; - auto& glyphMap = glyphMapping[fontId]; + auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); + ScalePixelsBilinear(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), + static_cast(dstPixels), dstW, dstH, dstBitmap.info().rowBytes()); + srcBitmap.unlockPixels(); + dstBitmap.unlockPixels(); - for (size_t i = 0; i < glyphCount; ++i) { - tgfx::GlyphID glyphID = glyphs[i]; - if (glyphMap.find(glyphID) != glyphMap.end()) { - continue; // Already added - } + auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); + if (pngData == nullptr) { + return nullptr; + } - // Try to get glyph path first (vector outline) - tgfx::Path glyphPath = {}; - bool hasPath = font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); + auto glyph = document->makeNode(); + auto image = document->makeNode(); + image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); + glyph->image = image; + glyph->offset.x = imageMatrix.getTranslateX(); + glyph->offset.y = imageMatrix.getTranslateY(); + + return glyph; + } - // Try to get glyph image (bitmap, e.g., color emoji) - tgfx::Matrix imageMatrix = {}; - auto imageCodec = font.getImage(glyphID, nullptr, &imageMatrix); + void createGlyphRuns(Text* text, const std::shared_ptr& textBlob) { + text->glyphRuns.clear(); - if (!hasPath && !imageCodec) { - continue; // No renderable content + for (const auto& run : *textBlob) { + if (run.glyphCount == 0) { + continue; } - // Create Glyph node - auto glyph = document->makeNode(); + auto* typeface = run.font.getTypeface().get(); + int fontSize = static_cast(run.font.getSize()); - if (hasPath) { - // Vector glyph - std::string pathStr = PathToSVGString(glyphPath); - if (!pathStr.empty()) { - glyph->path = document->makeNode(); - *glyph->path = PathDataFromSVGString(pathStr); + // Separate glyphs into vector and bitmap based on their mapping + std::vector vectorIndices = {}; + std::vector bitmapIndices = {}; + + for (size_t i = 0; i < run.glyphCount; ++i) { + GlyphKey key = {typeface, fontSize, run.glyphs[i]}; + if (vectorGlyphMapping.find(key) != vectorGlyphMapping.end()) { + vectorIndices.push_back(i); + } else if (bitmapGlyphMapping.find(key) != bitmapGlyphMapping.end()) { + bitmapIndices.push_back(i); } - } else if (imageCodec) { - // Bitmap glyph (e.g., color emoji) - int srcW = imageCodec->width(); - int srcH = imageCodec->height(); - float scaleX = imageMatrix.getScaleX(); - float scaleY = imageMatrix.getScaleY(); - int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); - int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); - if (dstW > 0 && dstH > 0) { - tgfx::Bitmap srcBitmap(srcW, srcH, false, false); - if (!srcBitmap.isEmpty()) { - auto* srcPixels = srcBitmap.lockPixels(); - if (srcPixels && imageCodec->readPixels(srcBitmap.info(), srcPixels)) { - srcBitmap.unlockPixels(); - tgfx::Bitmap dstBitmap(dstW, dstH, false, false); - if (!dstBitmap.isEmpty()) { - auto* dstPixels = dstBitmap.lockPixels(); - if (dstPixels) { - auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); - ScalePixelsBilinear(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), - static_cast(dstPixels), dstW, dstH, - dstBitmap.info().rowBytes()); - srcBitmap.unlockPixels(); - dstBitmap.unlockPixels(); - auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); - if (pngData) { - auto image = document->makeNode(); - image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); - glyph->image = image; - glyph->offset.x = imageMatrix.getTranslateX(); - glyph->offset.y = imageMatrix.getTranslateY(); - } - } else { - dstBitmap.unlockPixels(); - } - } - } else { - srcBitmap.unlockPixels(); - } - } + } + + // Create vector glyph run + if (!vectorIndices.empty() && vectorFont != nullptr) { + auto glyphRun = createGlyphRunForIndices(run, vectorIndices, vectorFont, vectorGlyphMapping, + typeface, fontSize); + if (glyphRun != nullptr) { + text->glyphRuns.push_back(glyphRun); } } - // Only add glyph if it has content - if (glyph->path || glyph->image) { - // Map original glyph ID to new index (1-based, since PathTypefaceBuilder uses 1-based IDs) - glyphMap[glyphID] = static_cast(fontNode->glyphs.size() + 1); - fontNode->glyphs.push_back(glyph); + // Create bitmap glyph run + if (!bitmapIndices.empty() && bitmapFont != nullptr) { + auto glyphRun = createGlyphRunForIndices(run, bitmapIndices, bitmapFont, bitmapGlyphMapping, + typeface, fontSize); + if (glyphRun != nullptr) { + text->glyphRuns.push_back(glyphRun); + } } } } - PAGXDocument* document = nullptr; + GlyphRun* createGlyphRunForIndices( + const tgfx::GlyphRun& run, const std::vector& indices, Font* font, + const std::unordered_map& mapping, + const tgfx::Typeface* typeface, int fontSize) { + auto glyphRun = document->makeNode(); + glyphRun->font = font; + + // Remap glyph IDs + for (size_t i : indices) { + GlyphKey key = {typeface, fontSize, run.glyphs[i]}; + auto it = mapping.find(key); + if (it != mapping.end()) { + glyphRun->glyphs.push_back(it->second); + } else { + glyphRun->glyphs.push_back(0); + } + } + + // Copy positions for selected indices + switch (run.positioning) { + case tgfx::GlyphPositioning::Horizontal: { + glyphRun->y = run.offsetY; + for (size_t i : indices) { + glyphRun->xPositions.push_back(run.positions[i]); + } + break; + } + case tgfx::GlyphPositioning::Point: { + auto* points = reinterpret_cast(run.positions); + for (size_t i : indices) { + glyphRun->positions.push_back({points[i].x, points[i].y}); + } + break; + } + case tgfx::GlyphPositioning::RSXform: { + auto* xforms = reinterpret_cast(run.positions); + for (size_t i : indices) { + RSXform xform = {}; + xform.scos = xforms[i].scos; + xform.ssin = xforms[i].ssin; + xform.tx = xforms[i].tx; + xform.ty = xforms[i].ty; + glyphRun->xforms.push_back(xform); + } + break; + } + case tgfx::GlyphPositioning::Matrix: { + auto* matrices = reinterpret_cast(run.positions); + for (size_t i : indices) { + Matrix m = {}; + m.a = matrices[i * 6 + 0]; + m.b = matrices[i * 6 + 1]; + m.c = matrices[i * 6 + 2]; + m.d = matrices[i * 6 + 3]; + m.tx = matrices[i * 6 + 4]; + m.ty = matrices[i * 6 + 5]; + glyphRun->matrices.push_back(m); + } + break; + } + } - // Font ID -> Font resource - std::unordered_map fontResources = {}; + return glyphRun; + } - // Font ID -> (original GlyphID -> new GlyphID) - std::unordered_map> glyphMapping = - {}; + PAGXDocument* document = nullptr; + Font* vectorFont = nullptr; + Font* bitmapFont = nullptr; - // Lookup key (typeface_ptr + font_size) -> Font ID - std::unordered_map fontKeyToId = {}; + std::vector> pendingVectorGlyphs = {}; + std::vector> pendingBitmapGlyphs = {}; - // Counter for generating incremental font IDs - int nextFontId = 0; + std::unordered_map vectorGlyphMapping = {}; + std::unordered_map bitmapGlyphMapping = {}; }; bool FontEmbedder::Embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index 951d028a82..b80c716983 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -73,12 +73,17 @@ class TypesetterContext { } private: + // A run of glyphs with the same font. + struct GlyphRun { + tgfx::Font font = {}; + std::vector glyphIDs = {}; + std::vector xPositions = {}; + }; + // Shaped text information for a single Text element. struct ShapedInfo { Text* text = nullptr; - std::vector glyphIDs = {}; - std::vector xPositions = {}; - tgfx::Font font = {}; + std::vector runs = {}; float totalWidth = 0; }; @@ -154,7 +159,7 @@ class TypesetterContext { // Apply TextLayout and create TextBlobs for (auto& info : shapedInfos) { - if (info.glyphIDs.empty()) { + if (info.runs.empty()) { continue; } @@ -170,18 +175,26 @@ class TypesetterContext { } } - // Apply offsets to positions - std::vector adjustedPositions = {}; - adjustedPositions.reserve(info.xPositions.size()); - for (float x : info.xPositions) { - adjustedPositions.push_back(x + xOffset); - } - - // Build TextBlob + // Build TextBlob with multiple runs tgfx::TextBlobBuilder builder = {}; - auto& buffer = builder.allocRunPosH(info.font, info.glyphIDs.size(), yOffset); - memcpy(buffer.glyphs, info.glyphIDs.data(), info.glyphIDs.size() * sizeof(tgfx::GlyphID)); - memcpy(buffer.positions, adjustedPositions.data(), adjustedPositions.size() * sizeof(float)); + + for (auto& run : info.runs) { + if (run.glyphIDs.empty()) { + continue; + } + + // Apply offsets to positions + std::vector adjustedPositions = {}; + adjustedPositions.reserve(run.xPositions.size()); + for (float x : run.xPositions) { + adjustedPositions.push_back(x + xOffset); + } + + auto& buffer = builder.allocRunPosH(run.font, run.glyphIDs.size(), yOffset); + memcpy(buffer.glyphs, run.glyphIDs.data(), run.glyphIDs.size() * sizeof(tgfx::GlyphID)); + memcpy(buffer.positions, adjustedPositions.data(), + adjustedPositions.size() * sizeof(float)); + } auto textBlob = builder.build(); if (textBlob != nullptr) { @@ -197,10 +210,13 @@ class TypesetterContext { } tgfx::Font primaryFont(primaryTypeface, text->fontSize); - info.font = primaryFont; float currentX = 0; const std::string& content = text->text; + // Current run being built + GlyphRun* currentRun = nullptr; + std::shared_ptr currentTypeface = nullptr; + size_t i = 0; while (i < content.size()) { // Decode UTF-8 character @@ -239,6 +255,7 @@ class TypesetterContext { // Try to find glyph in primary font or fallbacks tgfx::GlyphID glyphID = primaryFont.getGlyphID(unichar); tgfx::Font glyphFont = primaryFont; + std::shared_ptr glyphTypeface = primaryTypeface; if (glyphID == 0) { for (const auto& fallback : typesetter->fallbackTypefaces) { @@ -249,8 +266,7 @@ class TypesetterContext { glyphID = fallbackFont.getGlyphID(unichar); if (glyphID != 0) { glyphFont = fallbackFont; - // Note: for simplicity, we use the primary font for all glyphs. - // A more complete implementation would track font changes per run. + glyphTypeface = fallback; break; } } @@ -268,8 +284,16 @@ class TypesetterContext { bool hasImage = glyphFont.getImage(glyphID, nullptr, nullptr) != nullptr; if (hasOutline || hasImage) { - info.xPositions.push_back(currentX); - info.glyphIDs.push_back(glyphID); + // Start new run if typeface changed + if (currentTypeface != glyphTypeface) { + info.runs.emplace_back(); + currentRun = &info.runs.back(); + currentRun->font = glyphFont; + currentTypeface = glyphTypeface; + } + + currentRun->xPositions.push_back(currentX); + currentRun->glyphIDs.push_back(glyphID); } currentX += advance + text->letterSpacing; From 687d71b37e9abf944a82cc4120d021f97b55d243 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 23:13:47 +0800 Subject: [PATCH 254/678] Simplify TextGlyphs and FontEmbedder APIs, unify Typesetter for embedded fonts. --- pagx/include/pagx/FontEmbedder.h | 4 +- pagx/include/pagx/LayerBuilder.h | 13 +- pagx/include/pagx/TextGlyphs.h | 30 +- pagx/include/pagx/Typesetter.h | 6 +- pagx/src/TextGlyphs.cpp | 19 +- pagx/src/tgfx/FontEmbedder.cpp | 479 +++++++++++++++---------------- pagx/src/tgfx/LayerBuilder.cpp | 178 ++---------- pagx/src/tgfx/Typesetter.cpp | 250 +++++++++++++++- test/src/PAGXTest.cpp | 34 +-- 9 files changed, 528 insertions(+), 485 deletions(-) diff --git a/pagx/include/pagx/FontEmbedder.h b/pagx/include/pagx/FontEmbedder.h index e09333799d..8841235a2c 100644 --- a/pagx/include/pagx/FontEmbedder.h +++ b/pagx/include/pagx/FontEmbedder.h @@ -34,6 +34,8 @@ namespace pagx { */ class FontEmbedder { public: + FontEmbedder() = default; + /** * Embeds font data from TextGlyphs into the document. * @@ -46,7 +48,7 @@ class FontEmbedder { * @param textGlyphs The typesetting results containing Text -> TextBlob mappings. * @return true if embedding succeeded, false otherwise. */ - static bool Embed(PAGXDocument* document, const TextGlyphs& textGlyphs); + bool embed(PAGXDocument* document, const TextGlyphs& textGlyphs); }; } // namespace pagx diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index 906bc940ba..ee8adbfce7 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -20,26 +20,27 @@ #include #include "pagx/PAGXDocument.h" -#include "pagx/TextGlyphs.h" +#include "pagx/Typesetter.h" #include "tgfx/layers/Layer.h" namespace pagx { /** * LayerBuilder converts PAGXDocument to tgfx::Layer tree for rendering. - * This is the bridge between the independent pagx module and tgfx rendering. + * Text elements are rendered using the Typesetter to create TextGlyphs. */ class LayerBuilder { public: /** * Builds a layer tree from a PAGXDocument. * @param document The document to build from. - * @param textGlyphs Optional typesetting results. If provided, uses original typefaces for - * rendering (best quality). If nullptr, builds from embedded GlyphRun data. + * @param typesetter Optional typesetter for text rendering. If nullptr, a default Typesetter is + * created internally. Pass a custom Typesetter to use registered typefaces + * and fallback fonts. * @return The root layer of the built layer tree. */ - static std::shared_ptr Build(const PAGXDocument& document, - const TextGlyphs* textGlyphs = nullptr); + static std::shared_ptr Build(PAGXDocument* document, + Typesetter* typesetter = nullptr); }; } // namespace pagx diff --git a/pagx/include/pagx/TextGlyphs.h b/pagx/include/pagx/TextGlyphs.h index 6d93c1d87b..1e79ebfabd 100644 --- a/pagx/include/pagx/TextGlyphs.h +++ b/pagx/include/pagx/TextGlyphs.h @@ -18,7 +18,6 @@ #pragma once -#include #include #include #include "tgfx/core/TextBlob.h" @@ -26,41 +25,26 @@ namespace pagx { class Text; +class FontEmbedderImpl; /** * TextGlyphs holds the text typesetting results, mapping Text nodes to their shaped TextBlob - * representations. This class serves as the bridge between text typesetting (by Typesetter or - * external typesetters) and downstream consumers (LayerBuilder for rendering, FontEmbedder for - * font embedding). + * representations. This class serves as the bridge between text typesetting (by Typesetter) and + * downstream consumers (LayerBuilder for rendering, FontEmbedder for font embedding). */ class TextGlyphs { public: TextGlyphs() = default; /** - * Adds a mapping from a Text node to its shaped TextBlob. + * Sets the TextBlob for a Text node. */ - void add(Text* text, std::shared_ptr textBlob); + void setTextBlob(Text* text, std::shared_ptr textBlob); /** * Returns the TextBlob for the given Text node, or nullptr if not found. */ - std::shared_ptr get(const Text* text) const; - - /** - * Returns true if this TextGlyphs contains a mapping for the given Text node. - */ - bool contains(const Text* text) const; - - /** - * Iterates over all Text-TextBlob mappings. - */ - void forEach(std::function)> callback) const; - - /** - * Returns the number of Text-TextBlob mappings. - */ - size_t size() const; + std::shared_ptr getTextBlob(const Text* text) const; /** * Returns true if there are no mappings. @@ -68,6 +52,8 @@ class TextGlyphs { bool empty() const; private: + friend class FontEmbedder; + std::unordered_map> textBlobs = {}; }; diff --git a/pagx/include/pagx/Typesetter.h b/pagx/include/pagx/Typesetter.h index 031b90d4ef..c1036b604a 100644 --- a/pagx/include/pagx/Typesetter.h +++ b/pagx/include/pagx/Typesetter.h @@ -49,8 +49,10 @@ class Typesetter { void setFallbackTypefaces(std::vector> typefaces); /** - * Creates TextGlyphs for all Text nodes in the document. TextLayout modifiers are processed to - * apply alignment, line breaking, and other layout properties. + * Creates TextGlyphs for all Text nodes in the document. If a Text node has embedded GlyphRun + * data (from a loaded PAGX file), it uses that data directly. Otherwise, it performs text + * shaping using registered/fallback typefaces. TextLayout modifiers are processed to apply + * alignment, line breaking, and other layout properties. * @param document The document containing Text nodes to typeset. * @return TextGlyphs containing Text -> TextBlob mappings. */ diff --git a/pagx/src/TextGlyphs.cpp b/pagx/src/TextGlyphs.cpp index ecb1155e4d..1c585030d3 100644 --- a/pagx/src/TextGlyphs.cpp +++ b/pagx/src/TextGlyphs.cpp @@ -20,13 +20,13 @@ namespace pagx { -void TextGlyphs::add(Text* text, std::shared_ptr textBlob) { +void TextGlyphs::setTextBlob(Text* text, std::shared_ptr textBlob) { if (text != nullptr && textBlob != nullptr) { textBlobs[text] = std::move(textBlob); } } -std::shared_ptr TextGlyphs::get(const Text* text) const { +std::shared_ptr TextGlyphs::getTextBlob(const Text* text) const { auto it = textBlobs.find(const_cast(text)); if (it != textBlobs.end()) { return it->second; @@ -34,21 +34,6 @@ std::shared_ptr TextGlyphs::get(const Text* text) const { return nullptr; } -bool TextGlyphs::contains(const Text* text) const { - return textBlobs.find(const_cast(text)) != textBlobs.end(); -} - -void TextGlyphs::forEach( - std::function)> callback) const { - for (const auto& pair : textBlobs) { - callback(pair.first, pair.second); - } -} - -size_t TextGlyphs::size() const { - return textBlobs.size(); -} - bool TextGlyphs::empty() const { return textBlobs.empty(); } diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 4de61ce4be..21230f7be2 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -32,54 +32,71 @@ namespace pagx { -// Scales RGBA8888 pixels using bilinear interpolation for smooth downscaling. -static void ScalePixelsBilinear(const uint8_t* srcPixels, int srcW, int srcH, size_t srcRowBytes, - uint8_t* dstPixels, int dstW, int dstH, size_t dstRowBytes) { +// Scales RGBA8888 pixels using area averaging (box filter) for high-quality downscaling. +static void ScalePixelsAreaAverage(const uint8_t* srcPixels, int srcW, int srcH, size_t srcRowBytes, + uint8_t* dstPixels, int dstW, int dstH, size_t dstRowBytes) { float scaleX = static_cast(srcW) / static_cast(dstW); float scaleY = static_cast(srcH) / static_cast(dstH); - for (int y = 0; y < dstH; y++) { - float srcY = (static_cast(y) + 0.5f) * scaleY - 0.5f; - int y0 = static_cast(std::floor(srcY)); - int y1 = y0 + 1; - float fy = srcY - static_cast(y0); - + for (int dstY = 0; dstY < dstH; dstY++) { + float srcY0 = static_cast(dstY) * scaleY; + float srcY1 = static_cast(dstY + 1) * scaleY; + int y0 = static_cast(std::floor(srcY0)); + int y1 = static_cast(std::ceil(srcY1)); y0 = std::max(0, std::min(y0, srcH - 1)); - y1 = std::max(0, std::min(y1, srcH - 1)); - - auto* dstRow = dstPixels + y * dstRowBytes; + y1 = std::max(0, std::min(y1, srcH)); - for (int x = 0; x < dstW; x++) { - float srcX = (static_cast(x) + 0.5f) * scaleX - 0.5f; - int x0 = static_cast(std::floor(srcX)); - int x1 = x0 + 1; - float fx = srcX - static_cast(x0); + auto* dstRow = dstPixels + dstY * dstRowBytes; + for (int dstX = 0; dstX < dstW; dstX++) { + float srcX0 = static_cast(dstX) * scaleX; + float srcX1 = static_cast(dstX + 1) * scaleX; + int x0 = static_cast(std::floor(srcX0)); + int x1 = static_cast(std::ceil(srcX1)); x0 = std::max(0, std::min(x0, srcW - 1)); - x1 = std::max(0, std::min(x1, srcW - 1)); + x1 = std::max(0, std::min(x1, srcW)); + + float sumR = 0, sumG = 0, sumB = 0, sumA = 0; + float totalWeight = 0; - const auto* p00 = srcPixels + y0 * srcRowBytes + x0 * 4; - const auto* p10 = srcPixels + y0 * srcRowBytes + x1 * 4; - const auto* p01 = srcPixels + y1 * srcRowBytes + x0 * 4; - const auto* p11 = srcPixels + y1 * srcRowBytes + x1 * 4; + for (int sy = y0; sy < y1; sy++) { + float wy = 1.0f; + if (sy == y0) { + wy = 1.0f - (srcY0 - static_cast(y0)); + } + if (sy == y1 - 1 && y1 > y0 + 1) { + wy = srcY1 - static_cast(y1 - 1); + } - for (int c = 0; c < 4; c++) { - float v00 = static_cast(p00[c]); - float v10 = static_cast(p10[c]); - float v01 = static_cast(p01[c]); - float v11 = static_cast(p11[c]); + for (int sx = x0; sx < x1; sx++) { + float wx = 1.0f; + if (sx == x0) { + wx = 1.0f - (srcX0 - static_cast(x0)); + } + if (sx == x1 - 1 && x1 > x0 + 1) { + wx = srcX1 - static_cast(x1 - 1); + } - float v0 = v00 + (v10 - v00) * fx; - float v1 = v01 + (v11 - v01) * fx; - float v = v0 + (v1 - v0) * fy; + float weight = wx * wy; + const auto* p = srcPixels + sy * srcRowBytes + sx * 4; + sumR += static_cast(p[0]) * weight; + sumG += static_cast(p[1]) * weight; + sumB += static_cast(p[2]) * weight; + sumA += static_cast(p[3]) * weight; + totalWeight += weight; + } + } - dstRow[x * 4 + c] = static_cast(std::round(std::max(0.0f, std::min(255.0f, v)))); + if (totalWeight > 0) { + dstRow[dstX * 4 + 0] = static_cast(std::round(sumR / totalWeight)); + dstRow[dstX * 4 + 1] = static_cast(std::round(sumG / totalWeight)); + dstRow[dstX * 4 + 2] = static_cast(std::round(sumB / totalWeight)); + dstRow[dstX * 4 + 3] = static_cast(std::round(sumA / totalWeight)); } } } } -// Converts a tgfx::Path to SVG path string. static std::string PathToSVGString(const tgfx::Path& path) { std::string result = {}; result.reserve(256); @@ -113,8 +130,6 @@ static std::string PathToSVGString(const tgfx::Path& path) { return result; } -// Key for identifying unique glyphs: typeface + fontSize + glyphID. -// Different font sizes produce different glyph paths/images, so they need separate entries. struct GlyphKey { const tgfx::Typeface* typeface = nullptr; int fontSize = 0; @@ -134,173 +149,224 @@ struct GlyphKeyHash { } }; -// Information about a glyph to be embedded. struct GlyphInfo { std::string pathString = {}; std::shared_ptr imageCodec = nullptr; tgfx::Matrix imageMatrix = {}; }; -// Internal implementation class for FontEmbedder. -class FontEmbedderImpl { - public: - explicit FontEmbedderImpl(PAGXDocument* document) : document(document) { - } +static void CollectGlyphs( + const std::shared_ptr& textBlob, + std::vector>& pendingVectorGlyphs, + std::vector>& pendingBitmapGlyphs, + std::unordered_map& vectorGlyphMapping, + std::unordered_map& bitmapGlyphMapping) { + for (const auto& run : *textBlob) { + auto* typeface = run.font.getTypeface().get(); + int fontSize = static_cast(run.font.getSize()); + + for (size_t i = 0; i < run.glyphCount; ++i) { + tgfx::GlyphID glyphID = run.glyphs[i]; + GlyphKey key = {typeface, fontSize, glyphID}; + + if (vectorGlyphMapping.find(key) != vectorGlyphMapping.end() || + bitmapGlyphMapping.find(key) != bitmapGlyphMapping.end()) { + continue; + } - bool embed(const TextGlyphs& textGlyphs) { - if (document == nullptr) { - return false; - } + tgfx::Path glyphPath = {}; + bool hasPath = run.font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); + + tgfx::Matrix imageMatrix = {}; + auto imageCodec = run.font.getImage(glyphID, nullptr, &imageMatrix); - // First pass: collect all glyphs from all TextBlobs - textGlyphs.forEach([this](Text*, std::shared_ptr textBlob) { - if (textBlob == nullptr) { - return; + if (hasPath) { + GlyphInfo info = {}; + info.pathString = PathToSVGString(glyphPath); + if (!info.pathString.empty()) { + pendingVectorGlyphs.push_back({key, info}); + } + } else if (imageCodec) { + GlyphInfo info = {}; + info.imageCodec = imageCodec; + info.imageMatrix = imageMatrix; + pendingBitmapGlyphs.push_back({key, info}); } - collectGlyphs(textBlob); - }); + } + } +} - // Create Font nodes (one for vector glyphs, one for bitmap glyphs) - createFontNodes(); +static Glyph* CreateBitmapGlyph(PAGXDocument* document, const GlyphInfo& info) { + auto imageCodec = info.imageCodec; + auto imageMatrix = info.imageMatrix; - // Second pass: create GlyphRuns for each Text with remapped glyph IDs - textGlyphs.forEach([this](Text* text, std::shared_ptr textBlob) { - if (textBlob == nullptr) { - return; - } - createGlyphRuns(text, textBlob); - }); + int srcW = imageCodec->width(); + int srcH = imageCodec->height(); + float scaleX = imageMatrix.getScaleX(); + float scaleY = imageMatrix.getScaleY(); + int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); + int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); - return true; + if (dstW <= 0 || dstH <= 0) { + return nullptr; } - private: - void collectGlyphs(const std::shared_ptr& textBlob) { - for (const auto& run : *textBlob) { - auto* typeface = run.font.getTypeface().get(); - int fontSize = static_cast(run.font.getSize()); + tgfx::Bitmap srcBitmap(srcW, srcH, false, false); + if (srcBitmap.isEmpty()) { + return nullptr; + } + auto* srcPixels = srcBitmap.lockPixels(); + if (srcPixels == nullptr || !imageCodec->readPixels(srcBitmap.info(), srcPixels)) { + srcBitmap.unlockPixels(); + return nullptr; + } + srcBitmap.unlockPixels(); - for (size_t i = 0; i < run.glyphCount; ++i) { - tgfx::GlyphID glyphID = run.glyphs[i]; - GlyphKey key = {typeface, fontSize, glyphID}; + tgfx::Bitmap dstBitmap(dstW, dstH, false, false); + if (dstBitmap.isEmpty()) { + return nullptr; + } - // Skip if already collected - if (vectorGlyphMapping.find(key) != vectorGlyphMapping.end() || - bitmapGlyphMapping.find(key) != bitmapGlyphMapping.end()) { - continue; - } + auto* dstPixels = dstBitmap.lockPixels(); + if (dstPixels == nullptr) { + return nullptr; + } + auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); + ScalePixelsAreaAverage(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), + static_cast(dstPixels), dstW, dstH, dstBitmap.info().rowBytes()); + srcBitmap.unlockPixels(); + dstBitmap.unlockPixels(); + + auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); + if (pngData == nullptr) { + return nullptr; + } - // Try to get glyph path first (vector outline) - tgfx::Path glyphPath = {}; - bool hasPath = run.font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); + auto glyph = document->makeNode(); + auto image = document->makeNode(); + image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); + glyph->image = image; + glyph->offset.x = imageMatrix.getTranslateX(); + glyph->offset.y = imageMatrix.getTranslateY(); - // Try to get glyph image (bitmap, e.g., color emoji) - tgfx::Matrix imageMatrix = {}; - auto imageCodec = run.font.getImage(glyphID, nullptr, &imageMatrix); + return glyph; +} - if (hasPath) { - GlyphInfo info = {}; - info.pathString = PathToSVGString(glyphPath); - if (!info.pathString.empty()) { - pendingVectorGlyphs.push_back({key, info}); - } - } else if (imageCodec) { - GlyphInfo info = {}; - info.imageCodec = imageCodec; - info.imageMatrix = imageMatrix; - pendingBitmapGlyphs.push_back({key, info}); - } - } +static GlyphRun* CreateGlyphRunForIndices( + PAGXDocument* document, const tgfx::GlyphRun& run, const std::vector& indices, + Font* font, const std::unordered_map& mapping, + const tgfx::Typeface* typeface, int fontSize) { + auto glyphRun = document->makeNode(); + glyphRun->font = font; + + for (size_t i : indices) { + GlyphKey key = {typeface, fontSize, run.glyphs[i]}; + auto it = mapping.find(key); + if (it != mapping.end()) { + glyphRun->glyphs.push_back(it->second); + } else { + glyphRun->glyphs.push_back(0); } } - void createFontNodes() { - // Create vector font if needed - if (!pendingVectorGlyphs.empty()) { - vectorFont = document->makeNode("vector_font"); - tgfx::GlyphID nextID = 1; - for (auto& [key, info] : pendingVectorGlyphs) { - auto glyph = document->makeNode(); - glyph->path = document->makeNode(); - *glyph->path = PathDataFromSVGString(info.pathString); - vectorFont->glyphs.push_back(glyph); - vectorGlyphMapping[key] = nextID++; + switch (run.positioning) { + case tgfx::GlyphPositioning::Horizontal: { + glyphRun->y = run.offsetY; + for (size_t i : indices) { + glyphRun->xPositions.push_back(run.positions[i]); } + break; } - - // Create bitmap font if needed - if (!pendingBitmapGlyphs.empty()) { - bitmapFont = document->makeNode("bitmap_font"); - tgfx::GlyphID nextID = 1; - for (auto& [key, info] : pendingBitmapGlyphs) { - auto glyph = createBitmapGlyph(info); - if (glyph != nullptr) { - bitmapFont->glyphs.push_back(glyph); - bitmapGlyphMapping[key] = nextID++; - } + case tgfx::GlyphPositioning::Point: { + auto* points = reinterpret_cast(run.positions); + for (size_t i : indices) { + glyphRun->positions.push_back({points[i].x, points[i].y}); + } + break; + } + case tgfx::GlyphPositioning::RSXform: { + auto* xforms = reinterpret_cast(run.positions); + for (size_t i : indices) { + RSXform xform = {}; + xform.scos = xforms[i].scos; + xform.ssin = xforms[i].ssin; + xform.tx = xforms[i].tx; + xform.ty = xforms[i].ty; + glyphRun->xforms.push_back(xform); + } + break; + } + case tgfx::GlyphPositioning::Matrix: { + auto* matrices = reinterpret_cast(run.positions); + for (size_t i : indices) { + Matrix m = {}; + m.a = matrices[i * 6 + 0]; + m.b = matrices[i * 6 + 1]; + m.c = matrices[i * 6 + 2]; + m.d = matrices[i * 6 + 3]; + m.tx = matrices[i * 6 + 4]; + m.ty = matrices[i * 6 + 5]; + glyphRun->matrices.push_back(m); } + break; } } - Glyph* createBitmapGlyph(const GlyphInfo& info) { - auto imageCodec = info.imageCodec; - auto imageMatrix = info.imageMatrix; + return glyphRun; +} - int srcW = imageCodec->width(); - int srcH = imageCodec->height(); - float scaleX = imageMatrix.getScaleX(); - float scaleY = imageMatrix.getScaleY(); - int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); - int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); +bool FontEmbedder::embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { + if (document == nullptr) { + return false; + } - if (dstW <= 0 || dstH <= 0) { - return nullptr; - } + std::vector> pendingVectorGlyphs = {}; + std::vector> pendingBitmapGlyphs = {}; + std::unordered_map vectorGlyphMapping = {}; + std::unordered_map bitmapGlyphMapping = {}; - tgfx::Bitmap srcBitmap(srcW, srcH, false, false); - if (srcBitmap.isEmpty()) { - return nullptr; + // First pass: collect all glyphs from all TextBlobs + for (const auto& [text, textBlob] : textGlyphs.textBlobs) { + if (textBlob != nullptr) { + CollectGlyphs(textBlob, pendingVectorGlyphs, pendingBitmapGlyphs, vectorGlyphMapping, + bitmapGlyphMapping); } + } - auto* srcPixels = srcBitmap.lockPixels(); - if (srcPixels == nullptr || !imageCodec->readPixels(srcBitmap.info(), srcPixels)) { - srcBitmap.unlockPixels(); - return nullptr; - } - srcBitmap.unlockPixels(); + // Create Font nodes + Font* vectorFont = nullptr; + Font* bitmapFont = nullptr; - tgfx::Bitmap dstBitmap(dstW, dstH, false, false); - if (dstBitmap.isEmpty()) { - return nullptr; + if (!pendingVectorGlyphs.empty()) { + vectorFont = document->makeNode("vector_font"); + tgfx::GlyphID nextID = 1; + for (auto& [key, info] : pendingVectorGlyphs) { + auto glyph = document->makeNode(); + glyph->path = document->makeNode(); + *glyph->path = PathDataFromSVGString(info.pathString); + vectorFont->glyphs.push_back(glyph); + vectorGlyphMapping[key] = nextID++; } + } - auto* dstPixels = dstBitmap.lockPixels(); - if (dstPixels == nullptr) { - return nullptr; + if (!pendingBitmapGlyphs.empty()) { + bitmapFont = document->makeNode("bitmap_font"); + tgfx::GlyphID nextID = 1; + for (auto& [key, info] : pendingBitmapGlyphs) { + auto glyph = CreateBitmapGlyph(document, info); + if (glyph != nullptr) { + bitmapFont->glyphs.push_back(glyph); + bitmapGlyphMapping[key] = nextID++; + } } + } - auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); - ScalePixelsBilinear(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), - static_cast(dstPixels), dstW, dstH, dstBitmap.info().rowBytes()); - srcBitmap.unlockPixels(); - dstBitmap.unlockPixels(); - - auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); - if (pngData == nullptr) { - return nullptr; + // Second pass: create GlyphRuns for each Text + for (const auto& [text, textBlob] : textGlyphs.textBlobs) { + if (textBlob == nullptr) { + continue; } - auto glyph = document->makeNode(); - auto image = document->makeNode(); - image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); - glyph->image = image; - glyph->offset.x = imageMatrix.getTranslateX(); - glyph->offset.y = imageMatrix.getTranslateY(); - - return glyph; - } - - void createGlyphRuns(Text* text, const std::shared_ptr& textBlob) { text->glyphRuns.clear(); for (const auto& run : *textBlob) { @@ -311,7 +377,6 @@ class FontEmbedderImpl { auto* typeface = run.font.getTypeface().get(); int fontSize = static_cast(run.font.getSize()); - // Separate glyphs into vector and bitmap based on their mapping std::vector vectorIndices = {}; std::vector bitmapIndices = {}; @@ -324,19 +389,17 @@ class FontEmbedderImpl { } } - // Create vector glyph run if (!vectorIndices.empty() && vectorFont != nullptr) { - auto glyphRun = createGlyphRunForIndices(run, vectorIndices, vectorFont, vectorGlyphMapping, - typeface, fontSize); + auto glyphRun = CreateGlyphRunForIndices(document, run, vectorIndices, vectorFont, + vectorGlyphMapping, typeface, fontSize); if (glyphRun != nullptr) { text->glyphRuns.push_back(glyphRun); } } - // Create bitmap glyph run if (!bitmapIndices.empty() && bitmapFont != nullptr) { - auto glyphRun = createGlyphRunForIndices(run, bitmapIndices, bitmapFont, bitmapGlyphMapping, - typeface, fontSize); + auto glyphRun = CreateGlyphRunForIndices(document, run, bitmapIndices, bitmapFont, + bitmapGlyphMapping, typeface, fontSize); if (glyphRun != nullptr) { text->glyphRuns.push_back(glyphRun); } @@ -344,85 +407,7 @@ class FontEmbedderImpl { } } - GlyphRun* createGlyphRunForIndices( - const tgfx::GlyphRun& run, const std::vector& indices, Font* font, - const std::unordered_map& mapping, - const tgfx::Typeface* typeface, int fontSize) { - auto glyphRun = document->makeNode(); - glyphRun->font = font; - - // Remap glyph IDs - for (size_t i : indices) { - GlyphKey key = {typeface, fontSize, run.glyphs[i]}; - auto it = mapping.find(key); - if (it != mapping.end()) { - glyphRun->glyphs.push_back(it->second); - } else { - glyphRun->glyphs.push_back(0); - } - } - - // Copy positions for selected indices - switch (run.positioning) { - case tgfx::GlyphPositioning::Horizontal: { - glyphRun->y = run.offsetY; - for (size_t i : indices) { - glyphRun->xPositions.push_back(run.positions[i]); - } - break; - } - case tgfx::GlyphPositioning::Point: { - auto* points = reinterpret_cast(run.positions); - for (size_t i : indices) { - glyphRun->positions.push_back({points[i].x, points[i].y}); - } - break; - } - case tgfx::GlyphPositioning::RSXform: { - auto* xforms = reinterpret_cast(run.positions); - for (size_t i : indices) { - RSXform xform = {}; - xform.scos = xforms[i].scos; - xform.ssin = xforms[i].ssin; - xform.tx = xforms[i].tx; - xform.ty = xforms[i].ty; - glyphRun->xforms.push_back(xform); - } - break; - } - case tgfx::GlyphPositioning::Matrix: { - auto* matrices = reinterpret_cast(run.positions); - for (size_t i : indices) { - Matrix m = {}; - m.a = matrices[i * 6 + 0]; - m.b = matrices[i * 6 + 1]; - m.c = matrices[i * 6 + 2]; - m.d = matrices[i * 6 + 3]; - m.tx = matrices[i * 6 + 4]; - m.ty = matrices[i * 6 + 5]; - glyphRun->matrices.push_back(m); - } - break; - } - } - - return glyphRun; - } - - PAGXDocument* document = nullptr; - Font* vectorFont = nullptr; - Font* bitmapFont = nullptr; - - std::vector> pendingVectorGlyphs = {}; - std::vector> pendingBitmapGlyphs = {}; - - std::unordered_map vectorGlyphMapping = {}; - std::unordered_map bitmapGlyphMapping = {}; -}; - -bool FontEmbedder::Embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { - FontEmbedderImpl impl(document); - return impl.embed(textGlyphs); + return true; } } // namespace pagx diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 0b31f82901..f81358602e 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -350,7 +350,7 @@ static tgfx::LayerMaskType ToTGFXMaskType(MaskType type) { // Internal builder class class LayerBuilderImpl { public: - explicit LayerBuilderImpl(const TextGlyphs* textGlyphs) : _textGlyphs(textGlyphs) { + explicit LayerBuilderImpl(const TextGlyphs& textGlyphs) : _textGlyphs(textGlyphs) { } std::shared_ptr build(const PAGXDocument& document) { @@ -358,7 +358,6 @@ class LayerBuilderImpl { // Clear mappings from previous builds. _tgfxLayerByPagxLayer.clear(); _pendingMasks.clear(); - _fontCache.clear(); // Build layer tree. auto rootLayer = tgfx::Layer::Make(); @@ -385,7 +384,6 @@ class LayerBuilderImpl { _document = nullptr; _tgfxLayerByPagxLayer.clear(); _pendingMasks.clear(); - _fontCache.clear(); return rootLayer; } @@ -524,23 +522,7 @@ class LayerBuilderImpl { std::shared_ptr convertText(const Text* node) { auto tgfxText = std::make_shared(); - // Priority 1: Use TextGlyphs mapping (original typeface, best quality) - if (_textGlyphs != nullptr) { - auto textBlob = _textGlyphs->get(node); - if (textBlob) { - tgfxText->setTextBlob(textBlob); - tgfxText->setPosition(tgfx::Point::Make(node->position.x, node->position.y)); - return tgfxText; - } - } - - // Priority 2: Build from glyphRuns (embedded font, for imported files) - if (node->glyphRuns.empty()) { - DEBUG_ASSERT(false && "Text element has no GlyphRun data and no TextGlyphs provided."); - return tgfxText; - } - - auto textBlob = buildTextBlobFromGlyphRuns(node); + auto textBlob = _textGlyphs.getTextBlob(node); if (textBlob) { tgfxText->setTextBlob(textBlob); } @@ -548,141 +530,6 @@ class LayerBuilderImpl { return tgfxText; } - std::shared_ptr buildTextBlobFromGlyphRuns(const Text* node) { - tgfx::TextBlobBuilder builder; - - for (const auto& run : node->glyphRuns) { - if (run->glyphs.empty()) { - continue; - } - - // Resolve font reference - auto typeface = buildTypefaceFromFont(run->font); - if (!typeface) { - continue; - } - - // For embedded fonts (CustomTypeface from TextPrecomposer), the glyph paths and positions - // are already scaled to the target fontSize. Use fontSize=1.0 since paths are absolute. - tgfx::Font font(typeface, 1); - size_t count = run->glyphs.size(); - - // Determine positioning mode (priority: matrices > xforms > positions > xPositions) - if (!run->matrices.empty() && run->matrices.size() >= count) { - // Matrix mode - auto& buffer = builder.allocRunMatrix(font, count); - memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); - auto* matrices = reinterpret_cast(buffer.positions); - for (size_t i = 0; i < count; i++) { - const auto& m = run->matrices[i]; - matrices[i * 6 + 0] = m.a; - matrices[i * 6 + 1] = m.b; - matrices[i * 6 + 2] = m.c; - matrices[i * 6 + 3] = m.d; - matrices[i * 6 + 4] = m.tx; - matrices[i * 6 + 5] = m.ty; - } - } else if (!run->xforms.empty() && run->xforms.size() >= count) { - // RSXform mode - auto& buffer = builder.allocRunRSXform(font, count); - memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); - auto* xforms = reinterpret_cast(buffer.positions); - for (size_t i = 0; i < count; i++) { - const auto& x = run->xforms[i]; - xforms[i] = tgfx::RSXform::Make(x.scos, x.ssin, x.tx, x.ty); - } - } else if (!run->positions.empty() && run->positions.size() >= count) { - // Point mode - auto& buffer = builder.allocRunPos(font, count); - memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); - auto* positions = reinterpret_cast(buffer.positions); - for (size_t i = 0; i < count; i++) { - positions[i] = tgfx::Point::Make(run->positions[i].x, run->positions[i].y); - } - } else if (!run->xPositions.empty() && run->xPositions.size() >= count) { - // Horizontal mode - auto& buffer = builder.allocRunPosH(font, count, run->y); - memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); - memcpy(buffer.positions, run->xPositions.data(), count * sizeof(float)); - } - } - - return builder.build(); - } - - std::shared_ptr buildTypefaceFromFont(const Font* fontNode) { - if (!fontNode || fontNode->glyphs.empty()) { - return nullptr; - } - - auto it = _fontCache.find(fontNode); - if (it != _fontCache.end()) { - return it->second; - } - - // Determine if font is path-based or image-based - bool hasPath = false; - bool hasImage = false; - for (const auto& glyph : fontNode->glyphs) { - if (glyph->path != nullptr) { - hasPath = true; - } - if (glyph->image != nullptr) { - hasImage = true; - } - } - - std::shared_ptr typeface = nullptr; - if (hasPath && !hasImage) { - // Build path-based typeface - tgfx::PathTypefaceBuilder builder; - for (const auto& glyph : fontNode->glyphs) { - if (glyph->path != nullptr) { - builder.addGlyph(ToTGFX(*glyph->path)); - } - } - typeface = builder.detach(); - } else if (hasImage && !hasPath) { - // Build image-based typeface - tgfx::ImageTypefaceBuilder builder; - for (const auto& glyph : fontNode->glyphs) { - if (glyph->image != nullptr) { - std::shared_ptr codec = nullptr; - auto imageNode = glyph->image; - if (imageNode->data != nullptr) { - codec = tgfx::ImageCodec::MakeFrom(ToTGFX(imageNode->data)); - } else if (imageNode->filePath.find("data:") == 0) { - // Data URI - decode base64 image data - auto commaPos = imageNode->filePath.find(','); - if (commaPos != std::string::npos) { - auto header = imageNode->filePath.substr(0, commaPos); - if (header.find(";base64") != std::string::npos) { - auto base64Data = imageNode->filePath.substr(commaPos + 1); - auto data = Base64Decode(base64Data); - if (data) { - codec = tgfx::ImageCodec::MakeFrom(ToTGFX(data)); - } - } - } - } else if (!imageNode->filePath.empty()) { - // External file path (already resolved to absolute during import) - codec = tgfx::ImageCodec::MakeFrom(imageNode->filePath); - } - - if (codec) { - builder.addGlyph(codec, ToTGFX(glyph->offset)); - } - } - } - typeface = builder.detach(); - } - - if (typeface) { - _fontCache[fontNode] = typeface; - } - return typeface; - } - std::shared_ptr convertFill(const Fill* node) { auto fill = std::make_shared(); @@ -982,20 +829,31 @@ class LayerBuilderImpl { } } - const TextGlyphs* _textGlyphs = nullptr; + const TextGlyphs& _textGlyphs; const PAGXDocument* _document = nullptr; std::unordered_map> _tgfxLayerByPagxLayer = {}; std::vector, const Layer*, tgfx::LayerMaskType>> _pendingMasks = {}; - std::unordered_map> _fontCache = {}; }; // Public API implementation -std::shared_ptr LayerBuilder::Build(const PAGXDocument& document, - const TextGlyphs* textGlyphs) { +std::shared_ptr LayerBuilder::Build(PAGXDocument* document, Typesetter* typesetter) { + if (document == nullptr) { + return nullptr; + } + + // Create TextGlyphs using provided or default Typesetter + TextGlyphs textGlyphs; + if (typesetter != nullptr) { + textGlyphs = typesetter->createTextGlyphs(document); + } else { + Typesetter defaultTypesetter; + textGlyphs = defaultTypesetter.createTextGlyphs(document); + } + LayerBuilderImpl builder(textGlyphs); - return builder.build(document); + return builder.build(*document); } } // namespace pagx diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index b80c716983..2f489b136c 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -18,11 +18,17 @@ #include "pagx/Typesetter.h" #include +#include "Base64.h" +#include "SVGPathParser.h" #include "pagx/nodes/Composition.h" +#include "pagx/nodes/Font.h" #include "pagx/nodes/Group.h" +#include "pagx/nodes/Image.h" #include "pagx/nodes/Text.h" #include "pagx/nodes/TextLayout.h" +#include "tgfx/core/CustomTypeface.h" #include "tgfx/core/Font.h" +#include "tgfx/core/ImageCodec.h" #include "tgfx/core/Path.h" #include "tgfx/core/TextBlobBuilder.h" @@ -42,6 +48,42 @@ void Typesetter::setFallbackTypefaces(std::vector ToTGFXData(const std::shared_ptr& data) { + if (data == nullptr) { + return nullptr; + } + return tgfx::Data::MakeWithCopy(data->data(), data->size()); +} + // Internal implementation class for createTextGlyphs. class TypesetterContext { public: @@ -73,8 +115,8 @@ class TypesetterContext { } private: - // A run of glyphs with the same font. - struct GlyphRun { + // A run of glyphs with the same font (for shaping). + struct ShapedGlyphRun { tgfx::Font font = {}; std::vector glyphIDs = {}; std::vector xPositions = {}; @@ -83,7 +125,7 @@ class TypesetterContext { // Shaped text information for a single Text element. struct ShapedInfo { Text* text = nullptr; - std::vector runs = {}; + std::vector runs = {}; float totalWidth = 0; }; @@ -144,6 +186,35 @@ class TypesetterContext { } void processTextWithLayout(std::vector& textElements, const TextLayout* textLayout) { + // If TextLayout exists, check if ALL Text elements have embedded GlyphRun data. + // If any Text is missing embedded data, we must re-typeset all of them together. + bool allHaveEmbeddedData = true; + if (textLayout != nullptr) { + for (auto* text : textElements) { + if (text->glyphRuns.empty()) { + allHaveEmbeddedData = false; + break; + } + } + } + + // If no TextLayout or all have embedded data, process each Text individually + if (textLayout == nullptr || allHaveEmbeddedData) { + for (auto* text : textElements) { + if (!text->glyphRuns.empty()) { + auto textBlob = buildTextBlobFromEmbeddedGlyphRuns(text); + if (textBlob != nullptr) { + result.setTextBlob(text, textBlob); + } + } else { + processTextWithoutLayout(text); + } + } + return; + } + + // TextLayout exists but some Text elements need re-typesetting. + // Must re-typeset all Text elements together to apply layout correctly. std::vector shapedInfos = {}; for (auto* text : textElements) { @@ -163,16 +234,12 @@ class TypesetterContext { continue; } - float xOffset = 0; + float xOffset = calculateLayoutOffset(textLayout, info.totalWidth); float yOffset = 0; - if (textLayout != nullptr) { - xOffset = calculateLayoutOffset(textLayout, info.totalWidth); - - if (textLayout->position.x != 0 || textLayout->position.y != 0) { - xOffset += textLayout->position.x - info.text->position.x; - yOffset = textLayout->position.y - info.text->position.y; - } + if (textLayout->position.x != 0 || textLayout->position.y != 0) { + xOffset += textLayout->position.x - info.text->position.x; + yOffset = textLayout->position.y - info.text->position.y; } // Build TextBlob with multiple runs @@ -198,9 +265,165 @@ class TypesetterContext { auto textBlob = builder.build(); if (textBlob != nullptr) { - result.add(info.text, textBlob); + result.setTextBlob(info.text, textBlob); + } + } + } + + void processTextWithoutLayout(Text* text) { + ShapedInfo info = {}; + info.text = text; + + if (!text->text.empty()) { + shapeText(text, info); + } + + if (info.runs.empty()) { + return; + } + + // Build TextBlob without layout offset + tgfx::TextBlobBuilder builder = {}; + + for (auto& run : info.runs) { + if (run.glyphIDs.empty()) { + continue; } + + auto& buffer = builder.allocRunPosH(run.font, run.glyphIDs.size(), 0); + memcpy(buffer.glyphs, run.glyphIDs.data(), run.glyphIDs.size() * sizeof(tgfx::GlyphID)); + memcpy(buffer.positions, run.xPositions.data(), run.xPositions.size() * sizeof(float)); + } + + auto textBlob = builder.build(); + if (textBlob != nullptr) { + result.setTextBlob(text, textBlob); + } + } + + std::shared_ptr buildTextBlobFromEmbeddedGlyphRuns(const Text* text) { + tgfx::TextBlobBuilder builder; + + for (const auto& run : text->glyphRuns) { + if (run->glyphs.empty()) { + continue; + } + + auto typeface = buildTypefaceFromFont(run->font); + if (typeface == nullptr) { + continue; + } + + // Embedded fonts have pre-scaled glyph data, use fontSize=1.0 + tgfx::Font font(typeface, 1); + size_t count = run->glyphs.size(); + + // Determine positioning mode + if (!run->matrices.empty() && run->matrices.size() >= count) { + auto& buffer = builder.allocRunMatrix(font, count); + memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); + auto* matrices = reinterpret_cast(buffer.positions); + for (size_t i = 0; i < count; i++) { + const auto& m = run->matrices[i]; + matrices[i * 6 + 0] = m.a; + matrices[i * 6 + 1] = m.b; + matrices[i * 6 + 2] = m.c; + matrices[i * 6 + 3] = m.d; + matrices[i * 6 + 4] = m.tx; + matrices[i * 6 + 5] = m.ty; + } + } else if (!run->xforms.empty() && run->xforms.size() >= count) { + auto& buffer = builder.allocRunRSXform(font, count); + memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); + auto* xforms = reinterpret_cast(buffer.positions); + for (size_t i = 0; i < count; i++) { + const auto& x = run->xforms[i]; + xforms[i] = tgfx::RSXform::Make(x.scos, x.ssin, x.tx, x.ty); + } + } else if (!run->positions.empty() && run->positions.size() >= count) { + auto& buffer = builder.allocRunPos(font, count); + memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); + auto* positions = reinterpret_cast(buffer.positions); + for (size_t i = 0; i < count; i++) { + positions[i] = tgfx::Point::Make(run->positions[i].x, run->positions[i].y); + } + } else if (!run->xPositions.empty() && run->xPositions.size() >= count) { + auto& buffer = builder.allocRunPosH(font, count, run->y); + memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); + memcpy(buffer.positions, run->xPositions.data(), count * sizeof(float)); + } + } + + return builder.build(); + } + + std::shared_ptr buildTypefaceFromFont(const Font* fontNode) { + if (fontNode == nullptr || fontNode->glyphs.empty()) { + return nullptr; + } + + auto it = fontCache.find(fontNode); + if (it != fontCache.end()) { + return it->second; + } + + // Determine if font is path-based or image-based + bool hasPath = false; + bool hasImage = false; + for (const auto& glyph : fontNode->glyphs) { + if (glyph->path != nullptr) { + hasPath = true; + } + if (glyph->image != nullptr) { + hasImage = true; + } + } + + std::shared_ptr typeface = nullptr; + if (hasPath && !hasImage) { + tgfx::PathTypefaceBuilder builder; + for (const auto& glyph : fontNode->glyphs) { + if (glyph->path != nullptr) { + builder.addGlyph(ToTGFXPath(*glyph->path)); + } + } + typeface = builder.detach(); + } else if (hasImage && !hasPath) { + tgfx::ImageTypefaceBuilder builder; + for (const auto& glyph : fontNode->glyphs) { + if (glyph->image != nullptr) { + std::shared_ptr codec = nullptr; + auto imageNode = glyph->image; + if (imageNode->data != nullptr) { + codec = tgfx::ImageCodec::MakeFrom(ToTGFXData(imageNode->data)); + } else if (imageNode->filePath.find("data:") == 0) { + auto commaPos = imageNode->filePath.find(','); + if (commaPos != std::string::npos) { + auto header = imageNode->filePath.substr(0, commaPos); + if (header.find(";base64") != std::string::npos) { + auto base64Data = imageNode->filePath.substr(commaPos + 1); + auto data = Base64Decode(base64Data); + if (data) { + codec = tgfx::ImageCodec::MakeFrom(ToTGFXData(data)); + } + } + } + } else if (!imageNode->filePath.empty()) { + codec = tgfx::ImageCodec::MakeFrom(imageNode->filePath); + } + + if (codec) { + builder.addGlyph(codec, ToTGFXPoint(glyph->offset)); + } + } + } + typeface = builder.detach(); + } + + if (typeface) { + fontCache[fontNode] = typeface; } + return typeface; } void shapeText(Text* text, ShapedInfo& info) { @@ -214,7 +437,7 @@ class TypesetterContext { const std::string& content = text->text; // Current run being built - GlyphRun* currentRun = nullptr; + ShapedGlyphRun* currentRun = nullptr; std::shared_ptr currentTypeface = nullptr; size_t i = 0; @@ -362,6 +585,7 @@ class TypesetterContext { const Typesetter* typesetter = nullptr; PAGXDocument* document = nullptr; TextGlyphs result = {}; + std::unordered_map> fontCache = {}; }; TextGlyphs Typesetter::createTextGlyphs(PAGXDocument* document) { diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 08aa6d0e89..7128357dfe 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -144,7 +144,7 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { // Step 2: Typeset text elements and embed fonts auto textGlyphs = typesetter.createTextGlyphs(doc.get()); - pagx::FontEmbedder::Embed(doc.get(), textGlyphs); + pagx::FontEmbedder().embed(doc.get(), textGlyphs); // Step 3: Export to XML and save as PAGX file std::string xml = pagx::PAGXExporter::ToXML(*doc); @@ -155,7 +155,7 @@ PAG_TEST(PAGXTest, SVGToPAGXAll) { if (reloadedDoc == nullptr) { continue; } - auto layer = pagx::LayerBuilder::Build(*reloadedDoc); + auto layer = pagx::LayerBuilder::Build(reloadedDoc.get()); if (layer == nullptr) { continue; } @@ -236,7 +236,7 @@ PAG_TEST(PAGXTest, ColorRefRender) { auto doc = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), strlen(pagxXml)); ASSERT_TRUE(doc != nullptr); - auto layer = pagx::LayerBuilder::Build(*doc); + auto layer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 200); @@ -281,7 +281,7 @@ PAG_TEST(PAGXTest, StrokeColorRefRender) { auto doc = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), strlen(pagxXml)); ASSERT_TRUE(doc != nullptr); - auto layer = pagx::LayerBuilder::Build(*doc); + auto layer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 200); @@ -319,14 +319,14 @@ PAG_TEST(PAGXTest, LayerBuilderAPIConsistency) { // Load via FromFile auto docFromFile = pagx::PAGXImporter::FromFile(pagxPath); ASSERT_TRUE(docFromFile != nullptr); - auto layerFromFile = pagx::LayerBuilder::Build(*docFromFile); + auto layerFromFile = pagx::LayerBuilder::Build(docFromFile.get()); ASSERT_TRUE(layerFromFile != nullptr); // Load via FromXML auto docFromData = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), strlen(pagxXml)); ASSERT_TRUE(docFromData != nullptr); - auto layerFromData = pagx::LayerBuilder::Build(*docFromData); + auto layerFromData = pagx::LayerBuilder::Build(docFromData.get()); ASSERT_TRUE(layerFromData != nullptr); // Render both and compare @@ -800,7 +800,7 @@ PAG_TEST(PAGXTest, PrecomposedTextRender) { ASSERT_TRUE(context != nullptr); // Build layer tree (doc already parsed above) - auto layer = pagx::LayerBuilder::Build(*doc); + auto layer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 100); @@ -843,7 +843,7 @@ PAG_TEST(PAGXTest, PrecomposedTextPointPositions) { auto doc = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), strlen(pagxXml)); ASSERT_TRUE(doc != nullptr); - auto layer = pagx::LayerBuilder::Build(*doc); + auto layer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 150); @@ -885,7 +885,7 @@ PAG_TEST(PAGXTest, PrecomposedTextMissingGlyph) { auto doc = pagx::PAGXImporter::FromXML(reinterpret_cast(pagxXml), strlen(pagxXml)); ASSERT_TRUE(doc != nullptr); - auto layer = pagx::LayerBuilder::Build(*doc); + auto layer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 200, 100); @@ -990,7 +990,7 @@ PAG_TEST(PAGXTest, TextShaperRoundTrip) { typesetter.setFallbackTypefaces(typefaces); auto textGlyphs = typesetter.createTextGlyphs(doc.get()); EXPECT_FALSE(textGlyphs.empty()); - pagx::FontEmbedder::Embed(doc.get(), textGlyphs); + pagx::FontEmbedder().embed(doc.get(), textGlyphs); // Verify Font resources were added bool hasFontResource = false; @@ -1005,7 +1005,7 @@ PAG_TEST(PAGXTest, TextShaperRoundTrip) { EXPECT_TRUE(hasFontResource); // Step 3: Render typeset document - auto originalLayer = pagx::LayerBuilder::Build(*doc, &textGlyphs); + auto originalLayer = pagx::LayerBuilder::Build(doc.get(), &typesetter); ASSERT_TRUE(originalLayer != nullptr); auto originalSurface = Surface::Make(context, canvasWidth, canvasHeight); @@ -1057,7 +1057,7 @@ PAG_TEST(PAGXTest, TextShaperRoundTrip) { EXPECT_TRUE(glyphRunFound); // Intentionally not providing TextGlyphs to verify embedded font works - auto reloadedLayer = pagx::LayerBuilder::Build(*reloadedDoc); + auto reloadedLayer = pagx::LayerBuilder::Build(reloadedDoc.get()); ASSERT_TRUE(reloadedLayer != nullptr); // Step 6: Render pre-shaped version @@ -1103,10 +1103,10 @@ PAG_TEST(PAGXTest, TextShaperMultipleText) { typesetter.setFallbackTypefaces(typefaces); auto textGlyphs = typesetter.createTextGlyphs(doc.get()); EXPECT_FALSE(textGlyphs.empty()); - pagx::FontEmbedder::Embed(doc.get(), textGlyphs); + pagx::FontEmbedder().embed(doc.get(), textGlyphs); // Render typeset document - auto originalLayer = pagx::LayerBuilder::Build(*doc, &textGlyphs); + auto originalLayer = pagx::LayerBuilder::Build(doc.get(), &typesetter); ASSERT_TRUE(originalLayer != nullptr); auto originalSurface = Surface::Make(context, canvasWidth, canvasHeight); @@ -1120,7 +1120,7 @@ PAG_TEST(PAGXTest, TextShaperMultipleText) { auto reloadedDoc = pagx::PAGXImporter::FromFile(pagxPath); ASSERT_TRUE(reloadedDoc != nullptr); - auto reloadedLayer = pagx::LayerBuilder::Build(*reloadedDoc); + auto reloadedLayer = pagx::LayerBuilder::Build(reloadedDoc.get()); ASSERT_TRUE(reloadedLayer != nullptr); // Render pre-shaped @@ -1423,10 +1423,10 @@ PAG_TEST(PAGXTest, CompleteExample) { pagx::Typesetter typesetter; typesetter.setFallbackTypefaces(GetFallbackTypefaces()); auto textGlyphs = typesetter.createTextGlyphs(doc.get()); - pagx::FontEmbedder::Embed(doc.get(), textGlyphs); + pagx::FontEmbedder().embed(doc.get(), textGlyphs); // Build layer tree - auto layer = pagx::LayerBuilder::Build(*doc); + auto layer = pagx::LayerBuilder::Build(doc.get()); ASSERT_TRUE(layer != nullptr); auto surface = Surface::Make(context, 800, 520); From 8f1bc33fa5a39ab72c31de40ff76bd6502f7ec58 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 23:19:00 +0800 Subject: [PATCH 255/678] Fix emoji bitmap scaling by using absolute scale values and skip scaling when size unchanged. --- pagx/src/tgfx/FontEmbedder.cpp | 38 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 21230f7be2..8ee0f2a20a 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -202,8 +202,8 @@ static Glyph* CreateBitmapGlyph(PAGXDocument* document, const GlyphInfo& info) { int srcW = imageCodec->width(); int srcH = imageCodec->height(); - float scaleX = imageMatrix.getScaleX(); - float scaleY = imageMatrix.getScaleY(); + float scaleX = std::abs(imageMatrix.getScaleX()); + float scaleY = std::abs(imageMatrix.getScaleY()); int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); @@ -222,22 +222,30 @@ static Glyph* CreateBitmapGlyph(PAGXDocument* document, const GlyphInfo& info) { } srcBitmap.unlockPixels(); - tgfx::Bitmap dstBitmap(dstW, dstH, false, false); - if (dstBitmap.isEmpty()) { - return nullptr; - } + std::shared_ptr pngData = nullptr; - auto* dstPixels = dstBitmap.lockPixels(); - if (dstPixels == nullptr) { - return nullptr; + if (dstW == srcW && dstH == srcH) { + pngData = srcBitmap.encode(tgfx::EncodedFormat::PNG, 100); + } else { + tgfx::Bitmap dstBitmap(dstW, dstH, false, false); + if (dstBitmap.isEmpty()) { + return nullptr; + } + + auto* dstPixels = dstBitmap.lockPixels(); + if (dstPixels == nullptr) { + return nullptr; + } + auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); + ScalePixelsAreaAverage(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), + static_cast(dstPixels), dstW, dstH, + dstBitmap.info().rowBytes()); + srcBitmap.unlockPixels(); + dstBitmap.unlockPixels(); + + pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); } - auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); - ScalePixelsAreaAverage(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), - static_cast(dstPixels), dstW, dstH, dstBitmap.info().rowBytes()); - srcBitmap.unlockPixels(); - dstBitmap.unlockPixels(); - auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); if (pngData == nullptr) { return nullptr; } From 46c8dfd0c6e6849d4ae2333b207b392970ba0e8b Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 23:19:17 +0800 Subject: [PATCH 256/678] Update tgfx --- DEPS | 2 +- include/pag/types.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DEPS b/DEPS index 5f2da2b8da..13dce87d8e 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "ccb74935703c6a0a851e3c0f179062af53d05f2f", + "commit": "688d2f1dde8220270750c936e34a3fd9b32e0c6c", "dir": "third_party/tgfx" }, { diff --git a/include/pag/types.h b/include/pag/types.h index 9a6ed8de84..69b7f0ed50 100644 --- a/include/pag/types.h +++ b/include/pag/types.h @@ -1350,7 +1350,7 @@ class PAG_API Matrix { private: #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-private-field" - float values[6]; + float values[9]; mutable int32_t typeMask; /** * Matrix organizes its values in row order. These members correspond to each value in Matrix. From c630ed6b96cfb830ffc8004643808d4980e11ac1 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 23:22:26 +0800 Subject: [PATCH 257/678] Use ImageCodec readPixels for bitmap scaling instead of custom area averaging. --- pagx/src/tgfx/FontEmbedder.cpp | 103 +++------------------------------ 1 file changed, 9 insertions(+), 94 deletions(-) diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 8ee0f2a20a..1995840ab2 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -32,71 +32,6 @@ namespace pagx { -// Scales RGBA8888 pixels using area averaging (box filter) for high-quality downscaling. -static void ScalePixelsAreaAverage(const uint8_t* srcPixels, int srcW, int srcH, size_t srcRowBytes, - uint8_t* dstPixels, int dstW, int dstH, size_t dstRowBytes) { - float scaleX = static_cast(srcW) / static_cast(dstW); - float scaleY = static_cast(srcH) / static_cast(dstH); - - for (int dstY = 0; dstY < dstH; dstY++) { - float srcY0 = static_cast(dstY) * scaleY; - float srcY1 = static_cast(dstY + 1) * scaleY; - int y0 = static_cast(std::floor(srcY0)); - int y1 = static_cast(std::ceil(srcY1)); - y0 = std::max(0, std::min(y0, srcH - 1)); - y1 = std::max(0, std::min(y1, srcH)); - - auto* dstRow = dstPixels + dstY * dstRowBytes; - - for (int dstX = 0; dstX < dstW; dstX++) { - float srcX0 = static_cast(dstX) * scaleX; - float srcX1 = static_cast(dstX + 1) * scaleX; - int x0 = static_cast(std::floor(srcX0)); - int x1 = static_cast(std::ceil(srcX1)); - x0 = std::max(0, std::min(x0, srcW - 1)); - x1 = std::max(0, std::min(x1, srcW)); - - float sumR = 0, sumG = 0, sumB = 0, sumA = 0; - float totalWeight = 0; - - for (int sy = y0; sy < y1; sy++) { - float wy = 1.0f; - if (sy == y0) { - wy = 1.0f - (srcY0 - static_cast(y0)); - } - if (sy == y1 - 1 && y1 > y0 + 1) { - wy = srcY1 - static_cast(y1 - 1); - } - - for (int sx = x0; sx < x1; sx++) { - float wx = 1.0f; - if (sx == x0) { - wx = 1.0f - (srcX0 - static_cast(x0)); - } - if (sx == x1 - 1 && x1 > x0 + 1) { - wx = srcX1 - static_cast(x1 - 1); - } - - float weight = wx * wy; - const auto* p = srcPixels + sy * srcRowBytes + sx * 4; - sumR += static_cast(p[0]) * weight; - sumG += static_cast(p[1]) * weight; - sumB += static_cast(p[2]) * weight; - sumA += static_cast(p[3]) * weight; - totalWeight += weight; - } - } - - if (totalWeight > 0) { - dstRow[dstX * 4 + 0] = static_cast(std::round(sumR / totalWeight)); - dstRow[dstX * 4 + 1] = static_cast(std::round(sumG / totalWeight)); - dstRow[dstX * 4 + 2] = static_cast(std::round(sumB / totalWeight)); - dstRow[dstX * 4 + 3] = static_cast(std::round(sumA / totalWeight)); - } - } - } -} - static std::string PathToSVGString(const tgfx::Path& path) { std::string result = {}; result.reserve(256); @@ -211,41 +146,21 @@ static Glyph* CreateBitmapGlyph(PAGXDocument* document, const GlyphInfo& info) { return nullptr; } - tgfx::Bitmap srcBitmap(srcW, srcH, false, false); - if (srcBitmap.isEmpty()) { + tgfx::Bitmap dstBitmap(dstW, dstH, false, false); + if (dstBitmap.isEmpty()) { return nullptr; } - auto* srcPixels = srcBitmap.lockPixels(); - if (srcPixels == nullptr || !imageCodec->readPixels(srcBitmap.info(), srcPixels)) { - srcBitmap.unlockPixels(); + auto* dstPixels = dstBitmap.lockPixels(); + if (dstPixels == nullptr) { return nullptr; } - srcBitmap.unlockPixels(); - - std::shared_ptr pngData = nullptr; - - if (dstW == srcW && dstH == srcH) { - pngData = srcBitmap.encode(tgfx::EncodedFormat::PNG, 100); - } else { - tgfx::Bitmap dstBitmap(dstW, dstH, false, false); - if (dstBitmap.isEmpty()) { - return nullptr; - } - - auto* dstPixels = dstBitmap.lockPixels(); - if (dstPixels == nullptr) { - return nullptr; - } - auto* srcReadPixels = static_cast(srcBitmap.lockPixels()); - ScalePixelsAreaAverage(srcReadPixels, srcW, srcH, srcBitmap.info().rowBytes(), - static_cast(dstPixels), dstW, dstH, - dstBitmap.info().rowBytes()); - srcBitmap.unlockPixels(); - dstBitmap.unlockPixels(); - - pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); + bool success = imageCodec->readPixels(dstBitmap.info(), dstPixels); + dstBitmap.unlockPixels(); + if (!success) { + return nullptr; } + auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); if (pngData == nullptr) { return nullptr; } From 08b65afd824736d2f3fdf8e238fc571411c11a92 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 28 Jan 2026 23:33:04 +0800 Subject: [PATCH 258/678] Add scale attribute to Glyph for high-resolution bitmap storage and change PathData points to Point type. --- pagx/include/pagx/nodes/Font.h | 6 +++++ pagx/include/pagx/nodes/PathData.h | 16 +++++++------- pagx/spec/pagx_spec.md | 5 ++++- pagx/spec/pagx_spec.zh_CN.md | 5 ++++- pagx/src/PAGXExporter.cpp | 1 + pagx/src/PAGXImporter.cpp | 1 + pagx/src/PathData.cpp | 35 ++++++++++++------------------ pagx/src/SVGPathParser.cpp | 14 ++++++------ pagx/src/tgfx/FontEmbedder.cpp | 28 +++++++++++------------- pagx/src/tgfx/LayerBuilder.cpp | 10 ++++----- pagx/src/tgfx/Typesetter.cpp | 34 ++++++++++++++++++++++++----- test/src/PAGXTest.cpp | 2 +- 12 files changed, 93 insertions(+), 64 deletions(-) diff --git a/pagx/include/pagx/nodes/Font.h b/pagx/include/pagx/nodes/Font.h index 4a4ff0180a..e9bb853101 100644 --- a/pagx/include/pagx/nodes/Font.h +++ b/pagx/include/pagx/nodes/Font.h @@ -47,6 +47,12 @@ class Glyph : public Node { */ Point offset = {}; + /** + * Scale factor for image glyphs. The image is stored at a higher resolution and scaled down by + * this factor when rendering. The default value is 1.0 (no scaling). + */ + float scale = 1.0f; + NodeType nodeType() const override { return NodeType::Glyph; } diff --git a/pagx/include/pagx/nodes/PathData.h b/pagx/include/pagx/nodes/PathData.h index 3a575b15a8..1874fc3c95 100644 --- a/pagx/include/pagx/nodes/PathData.h +++ b/pagx/include/pagx/nodes/PathData.h @@ -22,6 +22,7 @@ #include #include "pagx/nodes/Node.h" #include "pagx/types/PathVerb.h" +#include "pagx/types/Point.h" #include "pagx/types/Rect.h" namespace pagx { @@ -76,18 +77,17 @@ class PathData : public Node { } /** - * Returns the array of point coordinates. - * Points are stored as [x0, y0, x1, y1, ...]. + * Returns the array of points. */ - const std::vector& points() const { + const std::vector& points() const { return _points; } /** - * Returns the number of point coordinates. + * Returns the number of points. */ size_t countPoints() const { - return _points.size() / 2; + return _points.size(); } /** @@ -98,9 +98,9 @@ class PathData : public Node { void forEach(Visitor&& visitor) const { size_t pointIndex = 0; for (auto verb : _verbs) { - const float* pts = _points.data() + pointIndex; + const Point* pts = _points.data() + pointIndex; visitor(verb, pts); - pointIndex += PointsPerVerb(verb) * 2; + pointIndex += PointsPerVerb(verb); } } @@ -129,7 +129,7 @@ class PathData : public Node { PathData() = default; std::vector _verbs = {}; - std::vector _points = {}; + std::vector _points = {}; Rect _cachedBounds = {}; bool _boundsDirty = true; diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 3c32097a17..a532af3b61 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -516,13 +516,16 @@ Glyph defines rendering data for a single glyph. Either `path` or `image` must b | `path` | string | - | SVG path data (vector outline) | | `image` | string | - | Image data (base64 data URI) or external file path | | `offset` | point | 0,0 | Bitmap offset (only used with `image`) | +| `scale` | float | 1 | Scale factor for image glyphs. The image is stored at higher resolution and scaled down by this factor when rendering | **Glyph Types**: - **Vector glyph**: Specifies the `path` attribute using SVG path syntax to describe the outline -- **Bitmap glyph**: Specifies the `image` attribute for colored glyphs like emoji; position can be adjusted with `offset` +- **Bitmap glyph**: Specifies the `image` attribute for colored glyphs like emoji; position can be adjusted with `offset`, and `scale` controls the display size relative to the stored image **Path Coordinate System**: Glyph paths use final rendering coordinates with font size scaling already applied. The same character at different font sizes should be stored as separate Glyphs, as fonts may have different glyph designs at different sizes. +**Image Scaling**: For bitmap glyphs, the `scale` attribute allows storing higher-resolution images that are scaled down at render time. For example, `scale="0.5"` means the stored image is 2x the display size and will be scaled to 50% when rendering. This enables sharper rendering on high-DPI displays while maintaining the correct visual size. + ### 3.4 Document Hierarchy PAGX documents organize content in a hierarchical structure: diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index d41fc7d703..c9bb947056 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -516,13 +516,16 @@ Glyph 定义单个字形的渲染数据。`path` 和 `image` 二选一必填, | `path` | string | - | SVG 路径数据(矢量轮廓) | | `image` | string | - | 图片数据(base64 数据 URI)或外部文件路径 | | `offset` | point | 0,0 | 位图偏移量(仅 `image` 时使用) | +| `scale` | float | 1 | 图片缩放系数。图片以高分辨率存储,渲染时按此系数缩小 | **字形类型**: - **矢量字形**:指定 `path` 属性,使用 SVG 路径语法描述轮廓 -- **位图字形**:指定 `image` 属性,用于 Emoji 等彩色字形,可通过 `offset` 调整位置 +- **位图字形**:指定 `image` 属性,用于 Emoji 等彩色字形,可通过 `offset` 调整位置,`scale` 控制显示大小与存储图片的比例 **路径坐标系**:字形路径使用最终渲染坐标,已包含字号缩放。不同字号的同一字符应作为独立 Glyph 存储,因为字体在不同字号下可能有不同的字形设计。 +**图片缩放**:对于位图字形,`scale` 属性允许存储更高分辨率的图片,在渲染时缩小。例如 `scale="0.5"` 表示存储的图片是显示大小的 2 倍,渲染时会缩放到 50%。这可以在高 DPI 显示器上获得更清晰的渲染效果,同时保持正确的视觉尺寸。 + ### 3.4 文档层级结构 PAGX 文档采用层级结构组织内容: diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index 5dc74dda93..0a6576bb11 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -1008,6 +1008,7 @@ static void writeResource(XMLBuilder& xml, const Node* node, const Options& opti if (glyph->offset.x != 0 || glyph->offset.y != 0) { xml.addAttribute("offset", pointToString(glyph->offset)); } + xml.addAttribute("scale", glyph->scale, 1.0f); xml.closeElementSelfClosing(); } xml.closeElement(); diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 9c5a69e4ce..c50994350c 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -1266,6 +1266,7 @@ static Glyph* parseGlyph(const XMLNode* node, PAGXDocument* doc) { if (!offsetStr.empty()) { glyph->offset = parsePoint(offsetStr); } + glyph->scale = getFloatAttribute(node, "scale", 1.0f); return glyph; } diff --git a/pagx/src/PathData.cpp b/pagx/src/PathData.cpp index 3c8f02e4c9..87418622c8 100644 --- a/pagx/src/PathData.cpp +++ b/pagx/src/PathData.cpp @@ -39,35 +39,28 @@ int PathData::PointsPerVerb(PathVerb verb) { void PathData::moveTo(float x, float y) { _verbs.push_back(PathVerb::Move); - _points.push_back(x); - _points.push_back(y); + _points.push_back({x, y}); _boundsDirty = true; } void PathData::lineTo(float x, float y) { _verbs.push_back(PathVerb::Line); - _points.push_back(x); - _points.push_back(y); + _points.push_back({x, y}); _boundsDirty = true; } void PathData::quadTo(float cx, float cy, float x, float y) { _verbs.push_back(PathVerb::Quad); - _points.push_back(cx); - _points.push_back(cy); - _points.push_back(x); - _points.push_back(y); + _points.push_back({cx, cy}); + _points.push_back({x, y}); _boundsDirty = true; } void PathData::cubicTo(float c1x, float c1y, float c2x, float c2y, float x, float y) { _verbs.push_back(PathVerb::Cubic); - _points.push_back(c1x); - _points.push_back(c1y); - _points.push_back(c2x); - _points.push_back(c2y); - _points.push_back(x); - _points.push_back(y); + _points.push_back({c1x, c1y}); + _points.push_back({c2x, c2y}); + _points.push_back({x, y}); _boundsDirty = true; } @@ -86,14 +79,14 @@ Rect PathData::getBounds() { return _cachedBounds; } - float minX = _points[0]; - float minY = _points[1]; - float maxX = _points[0]; - float maxY = _points[1]; + float minX = _points[0].x; + float minY = _points[0].y; + float maxX = _points[0].x; + float maxY = _points[0].y; - for (size_t i = 2; i < _points.size(); i += 2) { - float x = _points[i]; - float y = _points[i + 1]; + for (size_t i = 1; i < _points.size(); i++) { + float x = _points[i].x; + float y = _points[i].y; minX = std::min(minX, x); minY = std::min(minY, y); maxX = std::max(maxX, x); diff --git a/pagx/src/SVGPathParser.cpp b/pagx/src/SVGPathParser.cpp index a40d505ee5..7bcf16b927 100644 --- a/pagx/src/SVGPathParser.cpp +++ b/pagx/src/SVGPathParser.cpp @@ -34,30 +34,30 @@ std::string PathDataToSVGString(const PathData& pathData) { char buf[64] = {}; for (auto verb : verbs) { - const float* pts = points.data() + pointIndex; + const Point* pts = points.data() + pointIndex; switch (verb) { case PathVerb::Move: - snprintf(buf, sizeof(buf), "M%g %g", pts[0], pts[1]); + snprintf(buf, sizeof(buf), "M%g %g", pts[0].x, pts[0].y); result += buf; break; case PathVerb::Line: - snprintf(buf, sizeof(buf), "L%g %g", pts[0], pts[1]); + snprintf(buf, sizeof(buf), "L%g %g", pts[0].x, pts[0].y); result += buf; break; case PathVerb::Quad: - snprintf(buf, sizeof(buf), "Q%g %g %g %g", pts[0], pts[1], pts[2], pts[3]); + snprintf(buf, sizeof(buf), "Q%g %g %g %g", pts[0].x, pts[0].y, pts[1].x, pts[1].y); result += buf; break; case PathVerb::Cubic: - snprintf(buf, sizeof(buf), "C%g %g %g %g %g %g", pts[0], pts[1], pts[2], pts[3], pts[4], - pts[5]); + snprintf(buf, sizeof(buf), "C%g %g %g %g %g %g", pts[0].x, pts[0].y, pts[1].x, pts[1].y, + pts[2].x, pts[2].y); result += buf; break; case PathVerb::Close: result += "Z"; break; } - pointIndex += PathData::PointsPerVerb(verb) * 2; + pointIndex += PathData::PointsPerVerb(verb); } return result; diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 1995840ab2..ae709252b0 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -135,42 +135,40 @@ static Glyph* CreateBitmapGlyph(PAGXDocument* document, const GlyphInfo& info) { auto imageCodec = info.imageCodec; auto imageMatrix = info.imageMatrix; - int srcW = imageCodec->width(); - int srcH = imageCodec->height(); - float scaleX = std::abs(imageMatrix.getScaleX()); - float scaleY = std::abs(imageMatrix.getScaleY()); - int dstW = static_cast(std::round(static_cast(srcW) * scaleX)); - int dstH = static_cast(std::round(static_cast(srcH) * scaleY)); - - if (dstW <= 0 || dstH <= 0) { + int width = imageCodec->width(); + int height = imageCodec->height(); + if (width <= 0 || height <= 0) { return nullptr; } - tgfx::Bitmap dstBitmap(dstW, dstH, false, false); - if (dstBitmap.isEmpty()) { + tgfx::Bitmap bitmap(width, height, false, false); + if (bitmap.isEmpty()) { return nullptr; } - auto* dstPixels = dstBitmap.lockPixels(); - if (dstPixels == nullptr) { + auto* pixels = bitmap.lockPixels(); + if (pixels == nullptr) { return nullptr; } - bool success = imageCodec->readPixels(dstBitmap.info(), dstPixels); - dstBitmap.unlockPixels(); + bool success = imageCodec->readPixels(bitmap.info(), pixels); + bitmap.unlockPixels(); if (!success) { return nullptr; } - auto pngData = dstBitmap.encode(tgfx::EncodedFormat::PNG, 100); + auto pngData = bitmap.encode(tgfx::EncodedFormat::PNG, 100); if (pngData == nullptr) { return nullptr; } + float scale = std::abs(imageMatrix.getScaleX()); + auto glyph = document->makeNode(); auto image = document->makeNode(); image->data = pagx::Data::MakeWithCopy(pngData->data(), pngData->size()); glyph->image = image; glyph->offset.x = imageMatrix.getTranslateX(); glyph->offset.y = imageMatrix.getTranslateY(); + glyph->scale = scale; return glyph; } diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index f81358602e..5c4ff3011e 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -289,19 +289,19 @@ static tgfx::Matrix ToTGFX(const Matrix& m) { static tgfx::Path ToTGFX(const PathData& pathData) { tgfx::Path path; - pathData.forEach([&](PathVerb verb, const float* pts) { + pathData.forEach([&](PathVerb verb, const Point* pts) { switch (verb) { case PathVerb::Move: - path.moveTo(pts[0], pts[1]); + path.moveTo(pts[0].x, pts[0].y); break; case PathVerb::Line: - path.lineTo(pts[0], pts[1]); + path.lineTo(pts[0].x, pts[0].y); break; case PathVerb::Quad: - path.quadTo(pts[0], pts[1], pts[2], pts[3]); + path.quadTo(pts[0].x, pts[0].y, pts[1].x, pts[1].y); break; case PathVerb::Cubic: - path.cubicTo(pts[0], pts[1], pts[2], pts[3], pts[4], pts[5]); + path.cubicTo(pts[0].x, pts[0].y, pts[1].x, pts[1].y, pts[2].x, pts[2].y); break; case PathVerb::Close: path.close(); diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index 2f489b136c..24d48ebefc 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -26,6 +26,7 @@ #include "pagx/nodes/Image.h" #include "pagx/nodes/Text.h" #include "pagx/nodes/TextLayout.h" +#include "tgfx/core/Bitmap.h" #include "tgfx/core/CustomTypeface.h" #include "tgfx/core/Font.h" #include "tgfx/core/ImageCodec.h" @@ -51,19 +52,19 @@ void Typesetter::setFallbackTypefaces(std::vectorscale; + if (scale != 1.0f && scale > 0) { + int dstW = static_cast(std::round(static_cast(codec->width()) * scale)); + int dstH = static_cast(std::round(static_cast(codec->height()) * scale)); + if (dstW > 0 && dstH > 0) { + tgfx::Bitmap bitmap(dstW, dstH, false, false); + if (!bitmap.isEmpty()) { + auto* pixels = bitmap.lockPixels(); + if (pixels != nullptr && codec->readPixels(bitmap.info(), pixels)) { + bitmap.unlockPixels(); + auto pngData = bitmap.encode(tgfx::EncodedFormat::PNG, 100); + if (pngData) { + auto scaledCodec = tgfx::ImageCodec::MakeFrom(pngData); + if (scaledCodec) { + codec = scaledCodec; + } + } + } else { + bitmap.unlockPixels(); + } + } + } + } builder.addGlyph(codec, ToTGFXPoint(glyph->offset)); } } diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 7128357dfe..c184e683ae 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -380,7 +380,7 @@ PAG_TEST(PAGXTest, PathDataForEach) { pathData.close(); int verbCount = 0; - pathData.forEach([&verbCount](pagx::PathVerb, const float*) { verbCount++; }); + pathData.forEach([&verbCount](pagx::PathVerb, const pagx::Point*) { verbCount++; }); EXPECT_EQ(verbCount, 5); // M, L, L, L, Z } From f02191203e89e513fe68dc8615b42350b1d18cdc Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Thu, 29 Jan 2026 10:05:16 +0800 Subject: [PATCH 259/678] Update PAGX demo CDN URLs and disable fallback typefaces configuration. --- pagx/viewer/src/PAGXView.cpp | 2 +- pagx/wechat/src/PAGXView.cpp | 2 +- pagx/wechat/wx_demo/pages/viewer/viewer.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pagx/viewer/src/PAGXView.cpp b/pagx/viewer/src/PAGXView.cpp index f0437f346f..4f0b2f0119 100644 --- a/pagx/viewer/src/PAGXView.cpp +++ b/pagx/viewer/src/PAGXView.cpp @@ -77,7 +77,7 @@ void PAGXView::loadPAGX(const val& pagxData) { return; } LayerBuilder::Options options; - options.fallbackTypefaces = fallbackTypefaces; + // options.fallbackTypefaces = fallbackTypefaces; auto content = pagx::LayerBuilder::FromData(data->bytes(), data->size(), options); if (!content.root) { return; diff --git a/pagx/wechat/src/PAGXView.cpp b/pagx/wechat/src/PAGXView.cpp index 377e949181..1007edaf2b 100644 --- a/pagx/wechat/src/PAGXView.cpp +++ b/pagx/wechat/src/PAGXView.cpp @@ -75,7 +75,7 @@ void PAGXView::loadPAGX(const val& pagxData) { return; } LayerBuilder::Options options; - options.fallbackTypefaces = fallbackTypefaces; + // options.fallbackTypefaces = fallbackTypefaces; auto content = pagx::LayerBuilder::FromData(data->bytes(), data->size(), options); if (!content.root) { return; diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.js b/pagx/wechat/wx_demo/pages/viewer/viewer.js index 6cf0600183..4d4078aa35 100644 --- a/pagx/wechat/wx_demo/pages/viewer/viewer.js +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.js @@ -13,19 +13,19 @@ import { PerformanceMonitor } from '../../utils/performance-monitor'; const SAMPLE_FILES = [ { name: 'ColorPicker', - url: 'https://pag.io/pagx/testFiles/ColorPicker.pagx' + url: 'https://pag.io/wx_pagx_demo/ColorPicker.pagx' }, { name: 'Baseline', - url: 'https://pag.io/pagx/testFiles/Baseline.pagx' + url: 'https://pag.io/wx_pagx_demo/Baseline.pagx' }, { name: 'Guidelines', - url: 'https://pag.io/pagx/testFiles/Guidelines.pagx' + url: 'https://pag.io/wx_pagx_demo/Guidelines.pagx' }, { name: 'complex6', - url: 'https://pag.io/pagx/testFiles/complex6.pagx' + url: 'https://pag.io/wx_pagx_demo/complex6.pagx' }, ]; From 3ef651b41684fe160e666bd338d272d5a5c615a3 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Thu, 29 Jan 2026 10:21:28 +0800 Subject: [PATCH 260/678] Update TGFX dependency and fix emsdk path configuration. --- DEPS | 2 +- pagx/wechat/script/setup.emsdk.wx.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DEPS b/DEPS index 13dce87d8e..6a3e8aca34 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "688d2f1dde8220270750c936e34a3fd9b32e0c6c", + "commit": "ed9001104e3fdedc8409f984aa37d5b961a8dd8b", "dir": "third_party/tgfx" }, { diff --git a/pagx/wechat/script/setup.emsdk.wx.js b/pagx/wechat/script/setup.emsdk.wx.js index 4dfb60e43b..e694d75f9a 100644 --- a/pagx/wechat/script/setup.emsdk.wx.js +++ b/pagx/wechat/script/setup.emsdk.wx.js @@ -11,8 +11,8 @@ import Utils from "../../../third_party/vendor_tools/lib/Utils.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// const emsdkPath = path.resolve(__dirname, '../../third_party/emsdk'); -const emsdkPath = "/Users/billyjin/Desktop/project/tgfx/third_party/emsdk"; +const emsdkPath = path.resolve(__dirname, '../../../third_party/emsdk'); +// const emsdkPath = "/Users/billyjin/Desktop/project/tgfx/third_party/emsdk"; if (!fs.existsSync(emsdkPath)) { try { Utils.exec(`git clone https://github.com/emscripten-core/emsdk.git ${emsdkPath}`); From cc3a30c2f3ec1a0823f81b8c912d8c4304c2d1a3 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:22:14 +0800 Subject: [PATCH 261/678] Add viewer link to spec page header before language switcher. --- pagx/spec/script/publish.js | 77 ++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/pagx/spec/script/publish.js b/pagx/spec/script/publish.js index a8c8ccd048..8fab0cf215 100755 --- a/pagx/spec/script/publish.js +++ b/pagx/spec/script/publish.js @@ -208,10 +208,11 @@ function createMarkedInstance() { /** * Generate the complete HTML document. */ -function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl) { +function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl, viewerUrl) { const isEnglish = lang === 'en'; const htmlLang = isEnglish ? 'en' : 'zh-CN'; const tocTitle = isEnglish ? 'Table of Contents' : '目录'; + const viewerLabel = isEnglish ? 'Viewer' : '在线预览'; let draftBanner = ''; let draftStyles = ''; @@ -231,7 +232,7 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl) { .content { padding-top: 82px; } - .lang-switch { + .header-actions { top: 54px; } @media (max-width: 900px) { @@ -495,12 +496,8 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl) { .draft-banner strong { color: #5a4a06; } - /* Language switcher */ .lang-switch { - position: fixed; - top: 12px; - right: 20px; - z-index: 1001; + position: relative; } .lang-switch-btn { display: flex; @@ -562,18 +559,56 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl) { background: #e3f2fd; color: #1565c0; } + /* Header buttons container */ + .header-actions { + position: fixed; + top: 12px; + right: 20px; + z-index: 1001; + display: flex; + align-items: center; + gap: 8px; + } + /* Viewer link */ + .viewer-link { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: #f8f9fa; + border: 1px solid var(--border-color); + border-radius: 6px; + color: #555; + font-size: 13px; + text-decoration: none; + transition: all 0.2s; + } + .viewer-link:hover { + background: #e9ecef; + text-decoration: none; + } + .viewer-link svg { + width: 14px; + height: 14px; + } ${draftStyles} -${draftBanner}
    - -
    - English - 简体中文 +${draftBanner}
    + + + ${viewerLabel} + +
    + +
    @@ -614,7 +649,7 @@ ${tocHtml} /** * Publish a single spec file. */ -function publishSpec(specFile, outputDir, lang, version, showDraft, langSwitchUrl) { +function publishSpec(specFile, outputDir, lang, version, showDraft, langSwitchUrl, viewerUrl) { if (!fs.existsSync(specFile)) { console.log(` Skipped (file not found): ${specFile}`); return; @@ -652,7 +687,7 @@ function publishSpec(specFile, outputDir, lang, version, showDraft, langSwitchUr const htmlContent = marked.parse(mdContent); // Generate complete HTML document - const html = generateHtml(htmlContent, title, tocHtml, lang, showDraft, langSwitchUrl); + const html = generateHtml(htmlContent, title, tocHtml, lang, showDraft, langSwitchUrl, viewerUrl); // Create output directory fs.mkdirSync(outputDir, { recursive: true }); @@ -723,13 +758,17 @@ function main() { const baseOutputDir = path.join(SITE_DIR, version); + // Viewer URL (relative path from spec pages to viewer) + const viewerUrlFromRoot = '../viewer/'; + const viewerUrlFromCn = '../../viewer/'; + // Publish English version (default, at root) console.log('\nPublishing English version...'); - publishSpec(SPEC_FILE_EN, baseOutputDir, 'en', version, isDraft, 'cn/'); + publishSpec(SPEC_FILE_EN, baseOutputDir, 'en', version, isDraft, 'cn/', viewerUrlFromRoot); // Publish Chinese version (under /cn/) console.log('\nPublishing Chinese version...'); - publishSpec(SPEC_FILE_ZH, path.join(baseOutputDir, 'cn'), 'zh', version, isDraft, '../'); + publishSpec(SPEC_FILE_ZH, path.join(baseOutputDir, 'cn'), 'zh', version, isDraft, '../', viewerUrlFromCn); // Generate redirect index page (point to stableVersion if exists, otherwise current version) const redirectVersion = stableVersion || version; From ca18872113d7b67c45c244eb380d6427d4c66421 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:29:38 +0800 Subject: [PATCH 262/678] Change Chinese URL path from cn to zh for standard language code. --- pagx/spec/script/publish.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pagx/spec/script/publish.js b/pagx/spec/script/publish.js index 8fab0cf215..a6508ae30b 100755 --- a/pagx/spec/script/publish.js +++ b/pagx/spec/script/publish.js @@ -11,7 +11,7 @@ * * Output structure: * ../public//index.html - English (default) - * ../public//cn/index.html - Chinese + * ../public//zh/index.html - Chinese * * Usage: * cd pagx/spec && npm run publish @@ -727,7 +727,7 @@ Source files: Output structure: public/index.html - Redirect page public//index.html - English (default) - public//cn/index.html - Chinese + public//zh/index.html - Chinese Examples: npm run publish:spec @@ -760,15 +760,15 @@ function main() { // Viewer URL (relative path from spec pages to viewer) const viewerUrlFromRoot = '../viewer/'; - const viewerUrlFromCn = '../../viewer/'; + const viewerUrlFromZh = '../../viewer/'; // Publish English version (default, at root) console.log('\nPublishing English version...'); - publishSpec(SPEC_FILE_EN, baseOutputDir, 'en', version, isDraft, 'cn/', viewerUrlFromRoot); + publishSpec(SPEC_FILE_EN, baseOutputDir, 'en', version, isDraft, 'zh/', viewerUrlFromRoot); - // Publish Chinese version (under /cn/) + // Publish Chinese version (under /zh/) console.log('\nPublishing Chinese version...'); - publishSpec(SPEC_FILE_ZH, path.join(baseOutputDir, 'cn'), 'zh', version, isDraft, '../', viewerUrlFromCn); + publishSpec(SPEC_FILE_ZH, path.join(baseOutputDir, 'zh'), 'zh', version, isDraft, '../', viewerUrlFromZh); // Generate redirect index page (point to stableVersion if exists, otherwise current version) const redirectVersion = stableVersion || version; @@ -800,7 +800,7 @@ function generateRedirectPage(version) { // Build redirect URL var path = version + '/'; if (isChinese) { - path += 'cn/'; + path += 'zh/'; } // Redirect From 4bf68fa5b009febbeab116b835d3a9fdae6df1f1 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:30:03 +0800 Subject: [PATCH 263/678] Fix viewer button icon size to match language switcher. --- pagx/spec/script/publish.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pagx/spec/script/publish.js b/pagx/spec/script/publish.js index a6508ae30b..693fd327eb 100755 --- a/pagx/spec/script/publish.js +++ b/pagx/spec/script/publish.js @@ -588,8 +588,8 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl, v text-decoration: none; } .viewer-link svg { - width: 14px; - height: 14px; + width: 12px; + height: 12px; } ${draftStyles} From 69e8ccd7b705514e9075a7ac1d3b0069c2ad5787 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:32:20 +0800 Subject: [PATCH 264/678] Unify button styles with consistent font-family and line-height. --- pagx/spec/script/publish.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pagx/spec/script/publish.js b/pagx/spec/script/publish.js index 693fd327eb..cd3f24360d 100755 --- a/pagx/spec/script/publish.js +++ b/pagx/spec/script/publish.js @@ -508,7 +508,9 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl, v border: 1px solid var(--border-color); border-radius: 6px; color: #555; + font-family: inherit; font-size: 13px; + line-height: 1; cursor: pointer; transition: all 0.2s; } @@ -579,7 +581,9 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl, v border: 1px solid var(--border-color); border-radius: 6px; color: #555; + font-family: inherit; font-size: 13px; + line-height: 1; text-decoration: none; transition: all 0.2s; } From 41fb9c5b7abeb1906fcbeeab65f7c1b6d92a02fb Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:34:16 +0800 Subject: [PATCH 265/678] Increase header button size for better usability. --- pagx/spec/script/publish.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pagx/spec/script/publish.js b/pagx/spec/script/publish.js index cd3f24360d..b2280a2b47 100755 --- a/pagx/spec/script/publish.js +++ b/pagx/spec/script/publish.js @@ -503,14 +503,14 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl, v display: flex; align-items: center; gap: 6px; - padding: 6px 12px; + padding: 8px 14px; background: #f8f9fa; border: 1px solid var(--border-color); border-radius: 6px; color: #555; font-family: inherit; - font-size: 13px; - line-height: 1; + font-size: 14px; + line-height: 1.2; cursor: pointer; transition: all 0.2s; } @@ -518,8 +518,8 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl, v background: #e9ecef; } .lang-switch-btn svg { - width: 12px; - height: 12px; + width: 14px; + height: 14px; transition: transform 0.2s; } .lang-switch.open .lang-switch-btn svg { @@ -576,14 +576,14 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl, v display: flex; align-items: center; gap: 6px; - padding: 6px 12px; + padding: 8px 14px; background: #f8f9fa; border: 1px solid var(--border-color); border-radius: 6px; color: #555; font-family: inherit; - font-size: 13px; - line-height: 1; + font-size: 14px; + line-height: 1.2; text-decoration: none; transition: all 0.2s; } @@ -592,8 +592,8 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl, v text-decoration: none; } .viewer-link svg { - width: 12px; - height: 12px; + width: 14px; + height: 14px; } ${draftStyles} From 6d6530d188ad595cee459c6ba8de2676f112d6e6 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:35:43 +0800 Subject: [PATCH 266/678] Align language dropdown width to match button width. --- pagx/spec/script/publish.js | 1 + 1 file changed, 1 insertion(+) diff --git a/pagx/spec/script/publish.js b/pagx/spec/script/publish.js index b2280a2b47..1e8e05bf67 100755 --- a/pagx/spec/script/publish.js +++ b/pagx/spec/script/publish.js @@ -529,6 +529,7 @@ function generateHtml(content, title, tocHtml, lang, showDraft, langSwitchUrl, v position: absolute; top: 100%; right: 0; + min-width: 100%; margin-top: 4px; background: white; border: 1px solid var(--border-color); From 3a98eb19fee2c7c8357ccc25b7f116f76d421474 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:52:03 +0800 Subject: [PATCH 267/678] Add LayerBuilder::FromData API for convenient PAGX loading with fallback fonts. --- pagx/include/pagx/LayerBuilder.h | 43 ++++++++++++++++++++++++++++++++ pagx/src/tgfx/LayerBuilder.cpp | 22 ++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index ee8adbfce7..0c2f57300a 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -19,8 +19,10 @@ #pragma once #include +#include #include "pagx/PAGXDocument.h" #include "pagx/Typesetter.h" +#include "tgfx/core/Typeface.h" #include "tgfx/layers/Layer.h" namespace pagx { @@ -31,6 +33,47 @@ namespace pagx { */ class LayerBuilder { public: + /** + * Options for building layers from PAGX data. + */ + struct Options { + /** + * Fallback typefaces used when a character is not found in the primary font. Typefaces are + * tried in order until one containing the character is found. + */ + std::vector> fallbackTypefaces; + }; + + /** + * Result of building layers from PAGX data. + */ + struct Result { + /** + * The root layer of the built layer tree. nullptr if parsing or building failed. + */ + std::shared_ptr root = nullptr; + + /** + * The width of the PAGX document. + */ + float width = 0; + + /** + * The height of the PAGX document. + */ + float height = 0; + }; + + /** + * Parses PAGX data and builds a layer tree. This is a convenience method that combines parsing + * and building in one step. + * @param data The raw PAGX data bytes. + * @param length The length of the data in bytes. + * @param options Options for building, including fallback typefaces. + * @return Result containing the root layer and document dimensions. + */ + static Result FromData(const uint8_t* data, size_t length, const Options& options = Options{}); + /** * Builds a layer tree from a PAGXDocument. * @param document The document to build from. diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 5c4ff3011e..a7754d502e 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -52,6 +52,7 @@ #include "pagx/nodes/Stroke.h" #include "pagx/nodes/Text.h" #include "pagx/nodes/TrimPath.h" +#include "pagx/PAGXImporter.h" #include "tgfx/core/ColorSpace.h" #include "tgfx/core/CustomTypeface.h" #include "tgfx/core/Data.h" @@ -838,6 +839,27 @@ class LayerBuilderImpl { // Public API implementation +LayerBuilder::Result LayerBuilder::FromData(const uint8_t* data, size_t length, + const Options& options) { + Result result; + if (data == nullptr || length == 0) { + return result; + } + + auto document = PAGXImporter::FromXML(data, length); + if (!document) { + return result; + } + + Typesetter typesetter; + typesetter.setFallbackTypefaces(options.fallbackTypefaces); + + result.root = Build(document.get(), &typesetter); + result.width = document->width; + result.height = document->height; + return result; +} + std::shared_ptr LayerBuilder::Build(PAGXDocument* document, Typesetter* typesetter) { if (document == nullptr) { return nullptr; From 88cc16644fcab55fc99acfd5f8191de72ac3a37c Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:56:17 +0800 Subject: [PATCH 268/678] Refactor PAGXView to use existing PAGXImporter and LayerBuilder APIs. --- pagx/include/pagx/LayerBuilder.h | 43 -------------------------------- pagx/src/tgfx/LayerBuilder.cpp | 22 ---------------- pagx/viewer/src/PAGXView.cpp | 18 +++++++------ 3 files changed, 11 insertions(+), 72 deletions(-) diff --git a/pagx/include/pagx/LayerBuilder.h b/pagx/include/pagx/LayerBuilder.h index 0c2f57300a..ee8adbfce7 100644 --- a/pagx/include/pagx/LayerBuilder.h +++ b/pagx/include/pagx/LayerBuilder.h @@ -19,10 +19,8 @@ #pragma once #include -#include #include "pagx/PAGXDocument.h" #include "pagx/Typesetter.h" -#include "tgfx/core/Typeface.h" #include "tgfx/layers/Layer.h" namespace pagx { @@ -33,47 +31,6 @@ namespace pagx { */ class LayerBuilder { public: - /** - * Options for building layers from PAGX data. - */ - struct Options { - /** - * Fallback typefaces used when a character is not found in the primary font. Typefaces are - * tried in order until one containing the character is found. - */ - std::vector> fallbackTypefaces; - }; - - /** - * Result of building layers from PAGX data. - */ - struct Result { - /** - * The root layer of the built layer tree. nullptr if parsing or building failed. - */ - std::shared_ptr root = nullptr; - - /** - * The width of the PAGX document. - */ - float width = 0; - - /** - * The height of the PAGX document. - */ - float height = 0; - }; - - /** - * Parses PAGX data and builds a layer tree. This is a convenience method that combines parsing - * and building in one step. - * @param data The raw PAGX data bytes. - * @param length The length of the data in bytes. - * @param options Options for building, including fallback typefaces. - * @return Result containing the root layer and document dimensions. - */ - static Result FromData(const uint8_t* data, size_t length, const Options& options = Options{}); - /** * Builds a layer tree from a PAGXDocument. * @param document The document to build from. diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index a7754d502e..5c4ff3011e 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -52,7 +52,6 @@ #include "pagx/nodes/Stroke.h" #include "pagx/nodes/Text.h" #include "pagx/nodes/TrimPath.h" -#include "pagx/PAGXImporter.h" #include "tgfx/core/ColorSpace.h" #include "tgfx/core/CustomTypeface.h" #include "tgfx/core/Data.h" @@ -839,27 +838,6 @@ class LayerBuilderImpl { // Public API implementation -LayerBuilder::Result LayerBuilder::FromData(const uint8_t* data, size_t length, - const Options& options) { - Result result; - if (data == nullptr || length == 0) { - return result; - } - - auto document = PAGXImporter::FromXML(data, length); - if (!document) { - return result; - } - - Typesetter typesetter; - typesetter.setFallbackTypefaces(options.fallbackTypefaces); - - result.root = Build(document.get(), &typesetter); - result.width = document->width; - result.height = document->height; - return result; -} - std::shared_ptr LayerBuilder::Build(PAGXDocument* document, Typesetter* typesetter) { if (document == nullptr) { return nullptr; diff --git a/pagx/viewer/src/PAGXView.cpp b/pagx/viewer/src/PAGXView.cpp index f0437f346f..c7fe6166b8 100644 --- a/pagx/viewer/src/PAGXView.cpp +++ b/pagx/viewer/src/PAGXView.cpp @@ -19,6 +19,7 @@ #include "PAGXView.h" #include #include "GridBackground.h" +#include "pagx/PAGXImporter.h" #include "tgfx/core/Data.h" #include "tgfx/core/Typeface.h" @@ -76,15 +77,18 @@ void PAGXView::loadPAGX(const val& pagxData) { if (!data) { return; } - LayerBuilder::Options options; - options.fallbackTypefaces = fallbackTypefaces; - auto content = pagx::LayerBuilder::FromData(data->bytes(), data->size(), options); - if (!content.root) { + auto document = PAGXImporter::FromXML(data->bytes(), data->size()); + if (!document) { return; } - contentLayer = content.root; - pagxWidth = content.width; - pagxHeight = content.height; + Typesetter typesetter; + typesetter.setFallbackTypefaces(fallbackTypefaces); + contentLayer = LayerBuilder::Build(document.get(), &typesetter); + if (!contentLayer) { + return; + } + pagxWidth = document->width; + pagxHeight = document->height; displayList.root()->removeChildren(); displayList.root()->addChild(contentLayer); applyCenteringTransform(); From a98c103cc1556738fff3ae35a83c2b814066a057 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:58:00 +0800 Subject: [PATCH 269/678] Move Typesetter to PAGXView member for reuse across multiple loadPAGX calls. --- pagx/viewer/src/PAGXView.cpp | 7 ++----- pagx/viewer/src/PAGXView.h | 4 +++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pagx/viewer/src/PAGXView.cpp b/pagx/viewer/src/PAGXView.cpp index c7fe6166b8..2aab73a6f5 100644 --- a/pagx/viewer/src/PAGXView.cpp +++ b/pagx/viewer/src/PAGXView.cpp @@ -27,8 +27,6 @@ using namespace emscripten; namespace pagx { -static std::vector> fallbackTypefaces; - static std::shared_ptr GetDataFromEmscripten(const val& emscriptenData) { if (emscriptenData.isUndefined()) { return nullptr; @@ -55,7 +53,7 @@ PAGXView::PAGXView(const std::string& canvasID) : canvasID(canvasID) { } void PAGXView::registerFonts(const val& fontVal, const val& emojiFontVal) { - fallbackTypefaces.clear(); + std::vector> fallbackTypefaces; auto fontData = GetDataFromEmscripten(fontVal); if (fontData) { auto typeface = tgfx::Typeface::MakeFromData(fontData, 0); @@ -70,6 +68,7 @@ void PAGXView::registerFonts(const val& fontVal, const val& emojiFontVal) { fallbackTypefaces.push_back(std::move(typeface)); } } + typesetter.setFallbackTypefaces(std::move(fallbackTypefaces)); } void PAGXView::loadPAGX(const val& pagxData) { @@ -81,8 +80,6 @@ void PAGXView::loadPAGX(const val& pagxData) { if (!document) { return; } - Typesetter typesetter; - typesetter.setFallbackTypefaces(fallbackTypefaces); contentLayer = LayerBuilder::Build(document.get(), &typesetter); if (!contentLayer) { return; diff --git a/pagx/viewer/src/PAGXView.h b/pagx/viewer/src/PAGXView.h index 389afb9aba..5642b64e80 100644 --- a/pagx/viewer/src/PAGXView.h +++ b/pagx/viewer/src/PAGXView.h @@ -19,10 +19,11 @@ #pragma once #include +#include "pagx/LayerBuilder.h" +#include "pagx/Typesetter.h" #include "tgfx/gpu/Recording.h" #include "tgfx/gpu/opengl/webgl/WebGLWindow.h" #include "tgfx/layers/DisplayList.h" -#include "pagx/LayerBuilder.h" namespace pagx { @@ -61,6 +62,7 @@ class PAGXView { bool presentImmediately = true; float pagxWidth = 0.0f; float pagxHeight = 0.0f; + Typesetter typesetter = {}; }; } // namespace pagx From 5f29cc3255b3e530ecfd3e3d98d1c2a0c6f7e5d7 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 10:59:40 +0800 Subject: [PATCH 270/678] Add URL parameter support for loading remote PAGX files. --- pagx/viewer/index.ts | 84 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/pagx/viewer/index.ts b/pagx/viewer/index.ts index ff18fe8bed..83151dd0e5 100644 --- a/pagx/viewer/index.ts +++ b/pagx/viewer/index.ts @@ -481,10 +481,22 @@ function hideDropZone(): void { } } -async function loadPAGXFile(file: File) { +async function loadPAGXData(data: Uint8Array, name: string) { const toolbar = document.getElementById('toolbar') as HTMLDivElement; const fileName = document.getElementById('file-name') as HTMLSpanElement; + registerFontsToView(); + viewerState.pagxView!.loadPAGX(data); + gestureManager.resetTransform(viewerState); + updateSize(); + hideDropZone(); + toolbar.classList.remove('hidden'); + fileName.textContent = name; +} + +async function loadPAGXFile(file: File) { + const toolbar = document.getElementById('toolbar') as HTMLDivElement; + // Show loading UI with progress reset to 0% const loadingStartTime = Date.now(); showLoadingUI(); @@ -517,13 +529,7 @@ async function loadPAGXFile(file: File) { const fileBuffer = await file.arrayBuffer(); // Register fonts and load PAGX file - registerFontsToView(); - viewerState.pagxView!.loadPAGX(new Uint8Array(fileBuffer)); - gestureManager.resetTransform(viewerState); - updateSize(); - hideDropZone(); - toolbar.classList.remove('hidden'); - fileName.textContent = file.name; + await loadPAGXData(new Uint8Array(fileBuffer), file.name); } catch (error) { console.error('Failed to load PAGX file:', error); showDropZoneUI(); @@ -531,6 +537,62 @@ async function loadPAGXFile(file: File) { } } +async function loadPAGXFromURL(url: string) { + const toolbar = document.getElementById('toolbar') as HTMLDivElement; + + // Show loading UI with progress reset to 0% + const loadingStartTime = Date.now(); + showLoadingUI(); + toolbar.classList.add('hidden'); + resetProgressUI(); + // Wait for 0% to render before starting + await new Promise(resolve => requestAnimationFrame(resolve)); + + // Start loading resources if not already started + if (!wasmLoadPromise) { + wasmLoadPromise = loadWasm(); + } + if (!fontLoadPromise) { + fontLoadPromise = loadFonts(); + } + + try { + // Wait for WASM and fonts (progress goes from 0% to 99%) + await Promise.all([wasmLoadPromise, fontLoadPromise]); + updateProgressUI(); + + // Ensure minimum display time for loading UI (300ms) + const elapsed = Date.now() - loadingStartTime; + const minDisplayTime = 300; + if (elapsed < minDisplayTime) { + await new Promise(resolve => setTimeout(resolve, minDisplayTime - elapsed)); + } + + // Fetch PAGX file from URL + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); + } + const fileBuffer = await response.arrayBuffer(); + + // Extract filename from URL + const urlPath = new URL(url).pathname; + const name = urlPath.substring(urlPath.lastIndexOf('/') + 1) || 'remote.pagx'; + + // Register fonts and load PAGX file + await loadPAGXData(new Uint8Array(fileBuffer), name); + } catch (error) { + console.error('Failed to load PAGX from URL:', error); + showDropZoneUI(); + alert(`Failed to load PAGX from URL: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} + +function getPAGXUrlFromParams(): string | null { + const params = new URLSearchParams(window.location.search); + return params.get('url'); +} + function setupDragAndDrop() { const dropZone = document.getElementById('drop-zone') as HTMLDivElement; const container = document.getElementById('container') as HTMLDivElement; @@ -645,6 +707,12 @@ if (typeof window !== 'undefined') { console.error('Font load failed:', error); throw error; }); + + // Check for URL parameter and auto-load if present + const pagxUrl = getPAGXUrlFromParams(); + if (pagxUrl) { + loadPAGXFromURL(pagxUrl); + } }; window.onresize = () => { From f6bf47be07e561ab714c284c4c87168abb0a01a0 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 11:02:01 +0800 Subject: [PATCH 271/678] Add standalone PAGX sample files and update spec documents with viewer links. --- pagx/spec/pagx_spec.md | 62 ++++ pagx/spec/pagx_spec.zh_CN.md | 58 ++++ .../spec/samples/color-source-coordinate.pagx | 13 + pagx/spec/samples/complete-example.pagx | 275 ++++++++++++++++++ pagx/spec/samples/composition.pagx | 12 + pagx/spec/samples/conic-gradient.pagx | 11 + pagx/spec/samples/diamond-gradient.pagx | 11 + pagx/spec/samples/document-structure.pagx | 14 + pagx/spec/samples/fill.pagx | 18 ++ pagx/spec/samples/group-isolation.pagx | 12 + pagx/spec/samples/group-propagation.pagx | 15 + pagx/spec/samples/group.pagx | 11 + pagx/spec/samples/layer-filters.pagx | 10 + pagx/spec/samples/layer-styles.pagx | 10 + pagx/spec/samples/layer.pagx | 13 + pagx/spec/samples/linear-gradient.pagx | 11 + pagx/spec/samples/masking.pagx | 11 + pagx/spec/samples/merge-path.pagx | 9 + pagx/spec/samples/mixed-overlay.pagx | 15 + pagx/spec/samples/multiple-fills.pagx | 12 + pagx/spec/samples/multiple-painters.pagx | 9 + pagx/spec/samples/multiple-strokes.pagx | 10 + pagx/spec/samples/radial-gradient.pagx | 11 + pagx/spec/samples/repeater-text.pagx | 11 + pagx/spec/samples/repeater.pagx | 8 + pagx/spec/samples/resources.pagx | 15 + pagx/spec/samples/rich-text.pagx | 32 ++ pagx/spec/samples/scroll-rect.pagx | 7 + pagx/spec/samples/stroke.pagx | 23 ++ pagx/spec/samples/text-layout-paragraph.pagx | 9 + pagx/spec/samples/text-layout-point.pagx | 9 + pagx/spec/samples/text-modifier.pagx | 13 + pagx/spec/samples/text-path.pagx | 11 + pagx/spec/samples/trim-path.pagx | 9 + pagx/spec/script/publish.js | 34 +++ 35 files changed, 814 insertions(+) create mode 100644 pagx/spec/samples/color-source-coordinate.pagx create mode 100644 pagx/spec/samples/complete-example.pagx create mode 100644 pagx/spec/samples/composition.pagx create mode 100644 pagx/spec/samples/conic-gradient.pagx create mode 100644 pagx/spec/samples/diamond-gradient.pagx create mode 100644 pagx/spec/samples/document-structure.pagx create mode 100644 pagx/spec/samples/fill.pagx create mode 100644 pagx/spec/samples/group-isolation.pagx create mode 100644 pagx/spec/samples/group-propagation.pagx create mode 100644 pagx/spec/samples/group.pagx create mode 100644 pagx/spec/samples/layer-filters.pagx create mode 100644 pagx/spec/samples/layer-styles.pagx create mode 100644 pagx/spec/samples/layer.pagx create mode 100644 pagx/spec/samples/linear-gradient.pagx create mode 100644 pagx/spec/samples/masking.pagx create mode 100644 pagx/spec/samples/merge-path.pagx create mode 100644 pagx/spec/samples/mixed-overlay.pagx create mode 100644 pagx/spec/samples/multiple-fills.pagx create mode 100644 pagx/spec/samples/multiple-painters.pagx create mode 100644 pagx/spec/samples/multiple-strokes.pagx create mode 100644 pagx/spec/samples/radial-gradient.pagx create mode 100644 pagx/spec/samples/repeater-text.pagx create mode 100644 pagx/spec/samples/repeater.pagx create mode 100644 pagx/spec/samples/resources.pagx create mode 100644 pagx/spec/samples/rich-text.pagx create mode 100644 pagx/spec/samples/scroll-rect.pagx create mode 100644 pagx/spec/samples/stroke.pagx create mode 100644 pagx/spec/samples/text-layout-paragraph.pagx create mode 100644 pagx/spec/samples/text-layout-point.pagx create mode 100644 pagx/spec/samples/text-modifier.pagx create mode 100644 pagx/spec/samples/text-path.pagx create mode 100644 pagx/spec/samples/trim-path.pagx diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index a532af3b61..05ba9938d4 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -244,6 +244,8 @@ PAGX uses a standard 2D Cartesian coordinate system: ``` +[▶ View Sample](samples/document-structure.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `version` | string | (required) | Format version | @@ -272,6 +274,8 @@ PAGX uses a standard 2D Cartesian coordinate system: ``` +[▶ View Sample](samples/resources.pagx) + #### 3.3.1 Image Image resources define bitmap data for use throughout the document. @@ -327,6 +331,8 @@ Linear gradients interpolate along the direction from start point to end point. ``` +[▶ View Sample](samples/linear-gradient.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `startPoint` | point | (required) | Start point | @@ -346,6 +352,8 @@ Radial gradients radiate outward from the center. ``` +[▶ View Sample](samples/radial-gradient.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `center` | point | 0,0 | Center point | @@ -365,6 +373,8 @@ Conic gradients (also known as sweep gradients) interpolate along the circumfere ``` +[▶ View Sample](samples/conic-gradient.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `center` | point | 0,0 | Center point | @@ -385,6 +395,8 @@ Diamond gradients radiate from the center toward the four corners. ``` +[▶ View Sample](samples/diamond-gradient.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `center` | point | 0,0 | Center point | @@ -462,6 +474,8 @@ Except for solid colors, all color sources (gradients, image patterns) operate w ``` +[▶ View Sample](samples/color-source-coordinate.pagx) + - Applying `scale(2, 2)` transform to this layer: The rectangle becomes 200×200, and the gradient scales accordingly, maintaining consistent visual appearance - Directly changing Rectangle's size to 200,200: The rectangle becomes 200×200, but the gradient coordinates remain unchanged, covering only the left half of the rectangle @@ -478,6 +492,8 @@ Compositions are used for content reuse (similar to After Effects pre-comps). ``` +[▶ View Sample](samples/composition.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `width` | float | (required) | Composition width | @@ -628,6 +644,8 @@ Layer background is primarily used for: ``` +[▶ View Sample](samples/layer.pagx) + #### Child Elements Layer child elements are automatically categorized into four collections by type: @@ -721,6 +739,8 @@ Some layer styles additionally use **layer contour** or **layer background** as ``` +[▶ View Sample](samples/layer-styles.pagx) + **Common LayerStyle Attributes**: | Attribute | Type | Default | Description | @@ -799,6 +819,8 @@ Unlike layer styles (Section 4.3), which **independently render** visual effects ``` +[▶ View Sample](samples/layer-filters.pagx) + #### 4.4.1 BlurFilter | Attribute | Type | Default | Description | @@ -884,6 +906,8 @@ The `scrollRect` attribute defines the layer's visible region; content outside t ``` +[▶ View Sample](samples/scroll-rect.pagx) + #### 4.5.2 Masking Reference another layer as a mask using the `mask` attribute. @@ -899,6 +923,8 @@ Reference another layer as a mask using the `mask` attribute. ``` +[▶ View Sample](samples/masking.pagx) + **Masking Rules**: - The mask layer itself is not rendered (the `visible` attribute is ignored) - The mask layer's transforms do not affect the masked layer @@ -1250,6 +1276,8 @@ Fill draws the interior region of geometry using a specified color source. ``` +[▶ View Sample](samples/fill.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `color` | color/idref | #000000 | Color value or color source reference, default black | @@ -1292,6 +1320,8 @@ Stroke draws lines along geometry boundaries. ``` +[▶ View Sample](samples/stroke.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `color` | color/idref | #000000 | Color value or color source reference, default black | @@ -1385,6 +1415,8 @@ Trims paths to a specified start/end range. ``` +[▶ View Sample](samples/trim-path.pagx) + #### 5.4.2 RoundCorner Converts sharp corners of paths to rounded corners. @@ -1438,6 +1470,8 @@ Merges all shapes into a single shape. ``` +[▶ View Sample](samples/merge-path.pagx) + ### 5.5 Text Modifiers Text modifiers transform individual glyphs within text. @@ -1455,6 +1489,8 @@ When a text modifier is encountered, **all glyph lists** accumulated in the cont ``` +[▶ View Sample](samples/text-modifier.pagx) + #### 5.5.2 Text to Shape Conversion When text encounters a shape modifier, it is forcibly converted to shape paths: @@ -1623,6 +1659,8 @@ Arranges text along a specified path. The path can reference a PathData defined ``` +[▶ View Sample](samples/text-path.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `path` | string/idref | (required) | SVG path data or PathData resource reference "@id" | @@ -1677,6 +1715,8 @@ During rendering, an attached text typesetting module performs pre-layout, recal ``` +[▶ View Sample: Point Text](samples/text-layout-point.pagx) | [▶ View Sample: Paragraph Text](samples/text-layout-paragraph.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `position` | point | 0,0 | Layout origin | @@ -1748,6 +1788,8 @@ Rich text is achieved through multiple Text elements within a Group, each Text h ``` +[▶ View Sample](samples/rich-text.pagx) + **Note**: Each Group's Text + Fill/Stroke defines a text segment with independent styling. TextLayout treats all segments as a single unit for typography, enabling auto-wrapping and alignment. ### 5.6 Repeater @@ -1758,6 +1800,8 @@ Repeater duplicates accumulated content and rendered styles, applying progressiv ``` +[▶ View Sample](samples/repeater.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `copies` | float | 3 | Number of copies | @@ -1824,6 +1868,8 @@ When `copies` is a decimal (e.g., `3.5`), partial copies are achieved through ** ``` +[▶ View Sample](samples/repeater-text.pagx) + ### 5.7 Group Group is a VectorElement container with transform properties. @@ -1834,6 +1880,8 @@ Group is a VectorElement container with transform properties. ``` +[▶ View Sample](samples/group.pagx) + | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `anchorPoint` | point | 0,0 | Anchor point "x,y" | @@ -1892,6 +1940,8 @@ Groups create isolated scopes for geometry accumulation and rendering: ``` +[▶ View Sample](samples/group-isolation.pagx) + **Example 2 - Child Group Geometry Propagates Upward**: ```xml @@ -1905,6 +1955,8 @@ Groups create isolated scopes for geometry accumulation and rendering: ``` +[▶ View Sample](samples/group-propagation.pagx) + **Example 3 - Multiple Painters Reuse Geometry**: ```xml @@ -1912,6 +1964,8 @@ Groups create isolated scopes for geometry accumulation and rendering: ``` +[▶ View Sample](samples/multiple-painters.pagx) + #### Multiple Fills and Strokes Since painters do not clear the geometry list, the same geometry can have multiple Fills and Strokes applied consecutively. @@ -1925,6 +1979,8 @@ Since painters do not clear the geometry list, the same geometry can have multip ``` +[▶ View Sample](samples/multiple-fills.pagx) + **Example 5 - Multiple Strokes**: ```xml @@ -1933,6 +1989,8 @@ Since painters do not clear the geometry list, the same geometry can have multip ``` +[▶ View Sample](samples/multiple-strokes.pagx) + **Example 6 - Mixed Overlay**: ```xml @@ -1948,6 +2006,8 @@ Since painters do not clear the geometry list, the same geometry can have multip ``` +[▶ View Sample](samples/mixed-overlay.pagx) + **Rendering Order**: Multiple painters render in document order; those appearing earlier are below. --- @@ -2317,6 +2377,8 @@ The following example covers all major node types in PAGX, demonstrating complet ``` +[▶ View Complete Sample](samples/complete-example.pagx) + **Example Description**: This example demonstrates the complete feature set of PAGX with a modern dark theme design: diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index c9bb947056..287f53fe6c 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -244,6 +244,8 @@ PAGX 使用标准的 2D 笛卡尔坐标系: ``` +[▶ 查看示例](../samples/document-structure.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `version` | string | (必填) | 格式版本 | @@ -272,6 +274,8 @@ PAGX 使用标准的 2D 笛卡尔坐标系: ``` +[▶ 查看示例](../samples/resources.pagx) + #### 3.3.1 图片(Image) 图片资源定义可在文档中引用的位图数据。 @@ -327,6 +331,8 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 ``` +[▶ 查看示例](../samples/linear-gradient.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `startPoint` | point | (必填) | 起点 | @@ -346,6 +352,8 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 ``` +[▶ 查看示例](../samples/radial-gradient.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `center` | point | 0,0 | 中心点 | @@ -365,6 +373,8 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 ``` +[▶ 查看示例](../samples/conic-gradient.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `center` | point | 0,0 | 中心点 | @@ -385,6 +395,8 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 ``` +[▶ 查看示例](../samples/diamond-gradient.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `center` | point | 0,0 | 中心点 | @@ -465,6 +477,8 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 - 对该图层应用 `scale(2, 2)` 变换:矩形变为 200×200,渐变也随之放大,视觉效果保持一致 - 直接将 Rectangle 的 size 改为 200,200:矩形变为 200×200,但渐变坐标不变,只覆盖矩形的左半部分 +[▶ 查看示例](../samples/color-source-coordinate.pagx) + #### 3.3.4 合成(Composition) 合成用于内容复用(类似 After Effects 的 Pre-comp)。 @@ -478,6 +492,8 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 ``` +[▶ 查看示例](../samples/composition.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `width` | float | (必填) | 合成宽度 | @@ -628,6 +644,8 @@ PAGX 文档采用层级结构组织内容: ``` +[▶ 查看示例](../samples/layer.pagx) + #### 子元素 Layer 的子元素按类型自动归类为四个集合: @@ -721,6 +739,8 @@ Layer 的子元素按类型自动归类为四个集合: ``` +[▶ 查看示例](../samples/layer-styles.pagx) + **所有 LayerStyle 共有属性**: | 属性 | 类型 | 默认值 | 说明 | @@ -799,6 +819,8 @@ Layer 的子元素按类型自动归类为四个集合: ``` +[▶ 查看示例](../samples/layer-filters.pagx) + #### 4.4.1 模糊滤镜(BlurFilter) | 属性 | 类型 | 默认值 | 说明 | @@ -884,6 +906,8 @@ Layer 的子元素按类型自动归类为四个集合: ``` +[▶ 查看示例](../samples/scroll-rect.pagx) + #### 4.5.2 遮罩(Masking) 通过 `mask` 属性引用另一个图层作为遮罩。 @@ -899,6 +923,8 @@ Layer 的子元素按类型自动归类为四个集合: ``` +[▶ 查看示例](../samples/masking.pagx) + **遮罩规则**: - 遮罩图层自身不渲染(`visible` 属性被忽略) - 遮罩图层的变换不影响被遮罩图层 @@ -1250,6 +1276,8 @@ Matrix 是完整的 2D 仿射变换矩阵,六个分量 (a, b, c, d, tx, ty) ``` +[▶ 查看示例](../samples/fill.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | @@ -1292,6 +1320,8 @@ Matrix 是完整的 2D 仿射变换矩阵,六个分量 (a, b, c, d, tx, ty) ``` +[▶ 查看示例](../samples/stroke.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | @@ -1385,6 +1415,8 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: ``` +[▶ 查看示例](../samples/trim-path.pagx) + #### 5.4.2 圆角(RoundCorner) 将路径的尖角转换为圆角。 @@ -1438,6 +1470,8 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: ``` +[▶ 查看示例](../samples/merge-path.pagx) + ### 5.5 文本修改器(Text Modifiers) 文本修改器对文本中的独立字形进行变换。 @@ -1511,6 +1545,8 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: ``` +[▶ 查看示例](../samples/text-modifier.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `anchorPoint` | point | 0,0 | 锚点偏移 | @@ -1650,6 +1686,8 @@ finalColor = blend(originalColor, overrideColor, blendFactor) **闭合路径**:对于闭合路径,超出范围的字形会环绕到路径另一端。 +[▶ 查看示例](../samples/text-path.pagx) + #### 5.5.6 文本排版(TextLayout) TextLayout 是文本排版修改器,对累积的 Text 元素应用排版,会覆盖 Text 元素的原始位置(类似 TextPath 覆盖位置的行为)。支持两种模式: @@ -1748,6 +1786,8 @@ TextLayout 是文本排版修改器,对累积的 Text 元素应用排版,会 **说明**:每个 Group 内的 Text + Fill/Stroke 定义一段样式独立的文本片段,TextLayout 将所有片段作为整体进行排版,实现自动换行和对齐。 +[▶ 查看示例](../samples/rich-text.pagx) + ### 5.6 复制器(Repeater) 复制累积的内容和已渲染的样式,对每个副本应用渐进变换。Repeater 对 Path 和字形列表同时生效,且不会触发文本转形状。 @@ -1756,6 +1796,8 @@ TextLayout 是文本排版修改器,对累积的 Text 元素应用排版,会 ``` +[▶ 查看示例](../samples/repeater.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `copies` | float | 3 | 副本数 | @@ -1822,6 +1864,8 @@ alpha = lerp(startAlpha, endAlpha, t) ``` +[▶ 查看示例](../samples/repeater-text.pagx) + ### 5.7 容器(Group) Group 是带变换属性的矢量元素容器。 @@ -1832,6 +1876,8 @@ Group 是带变换属性的矢量元素容器。 ``` +[▶ 查看示例](../samples/group.pagx) + | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `anchorPoint` | point | 0,0 | 锚点 "x,y" | @@ -1890,6 +1936,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ``` +[▶ 查看示例](../samples/group-isolation.pagx) + **示例 2 - 子 Group 几何向上累积**: ```xml @@ -1903,6 +1951,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ``` +[▶ 查看示例](../samples/group-propagation.pagx) + **示例 3 - 多个绘制器复用几何**: ```xml @@ -1910,6 +1960,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ``` +[▶ 查看示例](../samples/multiple-painters.pagx) + #### 多重填充与描边 由于绘制器不清空几何列表,同一几何可连续应用多个 Fill 和 Stroke。 @@ -1923,6 +1975,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ``` +[▶ 查看示例](../samples/multiple-fills.pagx) + **示例 5 - 多重描边**: ```xml @@ -1931,6 +1985,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ``` +[▶ 查看示例](../samples/multiple-strokes.pagx) + **示例 6 - 混合叠加**: ```xml @@ -1946,6 +2002,8 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: ``` +[▶ 查看示例](../samples/mixed-overlay.pagx) + **渲染顺序**:多个绘制器按文档顺序渲染,先出现的位于下方。 --- diff --git a/pagx/spec/samples/color-source-coordinate.pagx b/pagx/spec/samples/color-source-coordinate.pagx new file mode 100644 index 0000000000..de9fe7e048 --- /dev/null +++ b/pagx/spec/samples/color-source-coordinate.pagx @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/pagx/spec/samples/complete-example.pagx b/pagx/spec/samples/complete-example.pagx new file mode 100644 index 0000000000..37779e6288 --- /dev/null +++ b/pagx/spec/samples/complete-example.pagx @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/composition.pagx b/pagx/spec/samples/composition.pagx new file mode 100644 index 0000000000..896e53408c --- /dev/null +++ b/pagx/spec/samples/composition.pagx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/pagx/spec/samples/conic-gradient.pagx b/pagx/spec/samples/conic-gradient.pagx new file mode 100644 index 0000000000..05e513e88a --- /dev/null +++ b/pagx/spec/samples/conic-gradient.pagx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pagx/spec/samples/diamond-gradient.pagx b/pagx/spec/samples/diamond-gradient.pagx new file mode 100644 index 0000000000..78440bcd8a --- /dev/null +++ b/pagx/spec/samples/diamond-gradient.pagx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pagx/spec/samples/document-structure.pagx b/pagx/spec/samples/document-structure.pagx new file mode 100644 index 0000000000..b6796f6a4f --- /dev/null +++ b/pagx/spec/samples/document-structure.pagx @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/fill.pagx b/pagx/spec/samples/fill.pagx new file mode 100644 index 0000000000..11af79355a --- /dev/null +++ b/pagx/spec/samples/fill.pagx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/group-isolation.pagx b/pagx/spec/samples/group-isolation.pagx new file mode 100644 index 0000000000..d900d1dc8c --- /dev/null +++ b/pagx/spec/samples/group-isolation.pagx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/pagx/spec/samples/group-propagation.pagx b/pagx/spec/samples/group-propagation.pagx new file mode 100644 index 0000000000..ed6141b27a --- /dev/null +++ b/pagx/spec/samples/group-propagation.pagx @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/group.pagx b/pagx/spec/samples/group.pagx new file mode 100644 index 0000000000..00ab0c5b24 --- /dev/null +++ b/pagx/spec/samples/group.pagx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pagx/spec/samples/layer-filters.pagx b/pagx/spec/samples/layer-filters.pagx new file mode 100644 index 0000000000..98d1519f44 --- /dev/null +++ b/pagx/spec/samples/layer-filters.pagx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/pagx/spec/samples/layer-styles.pagx b/pagx/spec/samples/layer-styles.pagx new file mode 100644 index 0000000000..7759963af2 --- /dev/null +++ b/pagx/spec/samples/layer-styles.pagx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/pagx/spec/samples/layer.pagx b/pagx/spec/samples/layer.pagx new file mode 100644 index 0000000000..a0782ea42b --- /dev/null +++ b/pagx/spec/samples/layer.pagx @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/pagx/spec/samples/linear-gradient.pagx b/pagx/spec/samples/linear-gradient.pagx new file mode 100644 index 0000000000..7850ecb5f5 --- /dev/null +++ b/pagx/spec/samples/linear-gradient.pagx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pagx/spec/samples/masking.pagx b/pagx/spec/samples/masking.pagx new file mode 100644 index 0000000000..b9330cc138 --- /dev/null +++ b/pagx/spec/samples/masking.pagx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pagx/spec/samples/merge-path.pagx b/pagx/spec/samples/merge-path.pagx new file mode 100644 index 0000000000..61c8563117 --- /dev/null +++ b/pagx/spec/samples/merge-path.pagx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/pagx/spec/samples/mixed-overlay.pagx b/pagx/spec/samples/mixed-overlay.pagx new file mode 100644 index 0000000000..0cda5c07fa --- /dev/null +++ b/pagx/spec/samples/mixed-overlay.pagx @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/multiple-fills.pagx b/pagx/spec/samples/multiple-fills.pagx new file mode 100644 index 0000000000..352ab433dc --- /dev/null +++ b/pagx/spec/samples/multiple-fills.pagx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/pagx/spec/samples/multiple-painters.pagx b/pagx/spec/samples/multiple-painters.pagx new file mode 100644 index 0000000000..9daff721b9 --- /dev/null +++ b/pagx/spec/samples/multiple-painters.pagx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/pagx/spec/samples/multiple-strokes.pagx b/pagx/spec/samples/multiple-strokes.pagx new file mode 100644 index 0000000000..10cacecebe --- /dev/null +++ b/pagx/spec/samples/multiple-strokes.pagx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/pagx/spec/samples/radial-gradient.pagx b/pagx/spec/samples/radial-gradient.pagx new file mode 100644 index 0000000000..78ab56b610 --- /dev/null +++ b/pagx/spec/samples/radial-gradient.pagx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pagx/spec/samples/repeater-text.pagx b/pagx/spec/samples/repeater-text.pagx new file mode 100644 index 0000000000..b459ea89d4 --- /dev/null +++ b/pagx/spec/samples/repeater-text.pagx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pagx/spec/samples/repeater.pagx b/pagx/spec/samples/repeater.pagx new file mode 100644 index 0000000000..fc401d0829 --- /dev/null +++ b/pagx/spec/samples/repeater.pagx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/pagx/spec/samples/resources.pagx b/pagx/spec/samples/resources.pagx new file mode 100644 index 0000000000..23cfcb32f1 --- /dev/null +++ b/pagx/spec/samples/resources.pagx @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/rich-text.pagx b/pagx/spec/samples/rich-text.pagx new file mode 100644 index 0000000000..02f9ed5962 --- /dev/null +++ b/pagx/spec/samples/rich-text.pagx @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/scroll-rect.pagx b/pagx/spec/samples/scroll-rect.pagx new file mode 100644 index 0000000000..a445cd7035 --- /dev/null +++ b/pagx/spec/samples/scroll-rect.pagx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/pagx/spec/samples/stroke.pagx b/pagx/spec/samples/stroke.pagx new file mode 100644 index 0000000000..89034c5eee --- /dev/null +++ b/pagx/spec/samples/stroke.pagx @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/text-layout-paragraph.pagx b/pagx/spec/samples/text-layout-paragraph.pagx new file mode 100644 index 0000000000..28834811a7 --- /dev/null +++ b/pagx/spec/samples/text-layout-paragraph.pagx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/pagx/spec/samples/text-layout-point.pagx b/pagx/spec/samples/text-layout-point.pagx new file mode 100644 index 0000000000..e68c871a64 --- /dev/null +++ b/pagx/spec/samples/text-layout-point.pagx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/pagx/spec/samples/text-modifier.pagx b/pagx/spec/samples/text-modifier.pagx new file mode 100644 index 0000000000..59344e5baf --- /dev/null +++ b/pagx/spec/samples/text-modifier.pagx @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/pagx/spec/samples/text-path.pagx b/pagx/spec/samples/text-path.pagx new file mode 100644 index 0000000000..3ded325190 --- /dev/null +++ b/pagx/spec/samples/text-path.pagx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pagx/spec/samples/trim-path.pagx b/pagx/spec/samples/trim-path.pagx new file mode 100644 index 0000000000..538f4f29ea --- /dev/null +++ b/pagx/spec/samples/trim-path.pagx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/pagx/spec/script/publish.js b/pagx/spec/script/publish.js index 1e8e05bf67..6a6cb99ae3 100755 --- a/pagx/spec/script/publish.js +++ b/pagx/spec/script/publish.js @@ -12,6 +12,7 @@ * Output structure: * ../public//index.html - English (default) * ../public//zh/index.html - Chinese + * ../public//samples/ - Sample PAGX files * * Usage: * cd pagx/spec && npm run publish @@ -30,6 +31,7 @@ const SPEC_DIR = path.dirname(SCRIPT_DIR); const PAGX_DIR = path.dirname(SPEC_DIR); const SPEC_FILE_EN = path.join(SPEC_DIR, 'pagx_spec.md'); const SPEC_FILE_ZH = path.join(SPEC_DIR, 'pagx_spec.zh_CN.md'); +const SAMPLES_DIR = path.join(SPEC_DIR, 'samples'); const PACKAGE_FILE = path.join(SPEC_DIR, 'package.json'); const SITE_DIR = path.join(PAGX_DIR, 'public'); @@ -733,6 +735,7 @@ Output structure: public/index.html - Redirect page public//index.html - English (default) public//zh/index.html - Chinese + public//samples/ - Sample PAGX files Examples: npm run publish:spec @@ -775,6 +778,10 @@ function main() { console.log('\nPublishing Chinese version...'); publishSpec(SPEC_FILE_ZH, path.join(baseOutputDir, 'zh'), 'zh', version, isDraft, '../', viewerUrlFromZh); + // Copy samples directory + console.log('\nCopying samples...'); + copySamples(baseOutputDir); + // Generate redirect index page (point to stableVersion if exists, otherwise current version) const redirectVersion = stableVersion || version; console.log('\nGenerating redirect page...'); @@ -784,6 +791,33 @@ function main() { console.log('\nDone!'); } +/** + * Copy samples directory to output. + */ +function copySamples(outputDir) { + const destDir = path.join(outputDir, 'samples'); + + if (!fs.existsSync(SAMPLES_DIR)) { + console.log(' Skipped (samples directory not found)'); + return; + } + + fs.mkdirSync(destDir, { recursive: true }); + + const files = fs.readdirSync(SAMPLES_DIR); + let count = 0; + for (const file of files) { + if (file.endsWith('.pagx')) { + const src = path.join(SAMPLES_DIR, file); + const dest = path.join(destDir, file); + fs.copyFileSync(src, dest); + count++; + } + } + + console.log(` Copied ${count} sample files to ${destDir}`); +} + /** * Generate the redirect index page at site/index.html. */ From a3d62e3d4ae3964f164654f7b4d18ac328ee0ce7 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 11:04:37 +0800 Subject: [PATCH 272/678] Rename URL parameter from url to pagx. --- pagx/viewer/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pagx/viewer/index.ts b/pagx/viewer/index.ts index 83151dd0e5..f700acfa13 100644 --- a/pagx/viewer/index.ts +++ b/pagx/viewer/index.ts @@ -590,7 +590,7 @@ async function loadPAGXFromURL(url: string) { function getPAGXUrlFromParams(): string | null { const params = new URLSearchParams(window.location.search); - return params.get('url'); + return params.get('pagx'); } function setupDragAndDrop() { From eecffefbb7d70cf46a03f1b0ff261bc7de93a03d Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 11:05:41 +0800 Subject: [PATCH 273/678] Rename URL parameter from pagx to src. --- pagx/viewer/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pagx/viewer/index.ts b/pagx/viewer/index.ts index f700acfa13..9d466e166a 100644 --- a/pagx/viewer/index.ts +++ b/pagx/viewer/index.ts @@ -590,7 +590,7 @@ async function loadPAGXFromURL(url: string) { function getPAGXUrlFromParams(): string | null { const params = new URLSearchParams(window.location.search); - return params.get('pagx'); + return params.get('src'); } function setupDragAndDrop() { From cb382c469ceee6a146d4d48681f9b3fdcdbcfa3e Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 11:06:06 +0800 Subject: [PATCH 274/678] Rename URL parameter from src to file. --- pagx/viewer/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pagx/viewer/index.ts b/pagx/viewer/index.ts index 9d466e166a..8c3563a3c8 100644 --- a/pagx/viewer/index.ts +++ b/pagx/viewer/index.ts @@ -590,7 +590,7 @@ async function loadPAGXFromURL(url: string) { function getPAGXUrlFromParams(): string | null { const params = new URLSearchParams(window.location.search); - return params.get('src'); + return params.get('file'); } function setupDragAndDrop() { From ab495a935c681d42ff971f8832b07025d0547917 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Thu, 29 Jan 2026 11:10:18 +0800 Subject: [PATCH 275/678] Add performance monitoring utility for WeChat MiniProgram demo. --- .../wx_demo/utils/performance-monitor.js | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 pagx/wechat/wx_demo/utils/performance-monitor.js diff --git a/pagx/wechat/wx_demo/utils/performance-monitor.js b/pagx/wechat/wx_demo/utils/performance-monitor.js new file mode 100644 index 0000000000..6523c4f959 --- /dev/null +++ b/pagx/wechat/wx_demo/utils/performance-monitor.js @@ -0,0 +1,290 @@ +/** + * Copyright (C) 2026 Tencent. All Rights Reserved. + * Performance monitoring utility for measuring rendering smoothness + */ + +class PerformanceMonitor { + constructor() { + this.enabled = false; + this.startTime = 0; + this.lastFrameTime = 0; + this.frameTimes = []; + this.gestureStartTime = 0; + this.gestureFrameTimes = []; + + // Statistics + this.currentFPS = 0; + this.averageFPS = 0; + this.minFPS = Infinity; + this.maxFPS = 0; + this.averageFrameTime = 0; + this.maxFrameTime = 0; + this.droppedFrames = 0; + this.totalFrames = 0; + this.validFrames = 0; // Only count frames used in calculations + + // Gesture-specific metrics + this.gestureResponseTime = 0; + this.gestureAverageFPS = 0; + this.gestureMinFPS = Infinity; + + // Config + this.targetFrameTime = 1000 / 60; // 60 FPS = ~16.67ms per frame + this.droppedFrameThreshold = this.targetFrameTime * 1.5; // 25ms + this.sampleWindow = 60; // Keep last 60 frame times for rolling average + this.minValidFrameTime = 5; // Minimum frame time to consider valid (ms) + + // Callbacks + this.onStatsUpdate = null; + } + + start() { + this.enabled = true; + this.reset(); + } + + stop() { + this.enabled = false; + } + + reset() { + this.startTime = Date.now(); + this.lastFrameTime = this.startTime; + this.frameTimes = []; + this.gestureFrameTimes = []; + this.minFPS = Infinity; + this.maxFPS = 0; + this.droppedFrames = 0; + this.totalFrames = 0; + this.validFrames = 0; + this.currentFPS = 0; + this.averageFPS = 0; + this.gestureMinFPS = Infinity; + this.gestureAverageFPS = 0; + this.gestureResponseTime = 0; + } + + recordFrame() { + if (!this.enabled) return; + + const now = Date.now(); + const frameTime = now - this.lastFrameTime; + + this.totalFrames++; + + // Always update lastFrameTime to avoid accumulating errors + this.lastFrameTime = now; + + // Skip first frame (initialization timing is unreliable) + if (this.totalFrames === 1) { + return; + } + + // Skip abnormally small intervals (< 5ms) to avoid timer precision issues + // But don't block forever - if we get 10+ frames, start accepting them + if (frameTime < this.minValidFrameTime && this.totalFrames < 10) { + return; + } + + this.validFrames++; + + // Update frame timing array + this.frameTimes.push(frameTime); + if (this.frameTimes.length > this.sampleWindow) { + this.frameTimes.shift(); + } + + // Track dropped frames (frame time > 25ms means dropped frames) + if (frameTime > this.droppedFrameThreshold) { + this.droppedFrames++; + } + + // Calculate current FPS (instantaneous) with reasonable cap + // Cap at 120 FPS (most displays are 60-120Hz) + this.currentFPS = Math.min(120, Math.round(1000 / frameTime)); + + // Track min/max FPS + if (this.currentFPS < this.minFPS) { + this.minFPS = this.currentFPS; + } + if (this.currentFPS > this.maxFPS) { + this.maxFPS = this.currentFPS; + } + + // Calculate average FPS based on valid frames only + const elapsed = now - this.startTime; + if (elapsed > 0 && this.validFrames > 0) { + this.averageFPS = Math.min(120, Math.round((this.validFrames * 1000) / elapsed)); + } + + // Calculate average and max frame time + if (this.frameTimes.length > 0) { + const sum = this.frameTimes.reduce((a, b) => a + b, 0); + this.averageFrameTime = Math.round(sum / this.frameTimes.length * 10) / 10; + this.maxFrameTime = Math.round(Math.max(...this.frameTimes) * 10) / 10; + } + + // Warn when FPS drops below 30 + if (this.currentFPS < 30 && this.validFrames > 10) { + console.warn(`[Performance] Low FPS detected: ${this.currentFPS} (frameTime: ${frameTime}ms)`); + } + + // Gesture-specific tracking + if (this.gestureStartTime > 0) { + this.gestureFrameTimes.push(frameTime); + + // Calculate gesture instantaneous FPS with cap + const gestureFPS = Math.min(120, Math.round(1000 / frameTime)); + if (gestureFPS < this.gestureMinFPS) { + this.gestureMinFPS = gestureFPS; + } + + // Calculate gesture average FPS + const gestureElapsed = now - this.gestureStartTime; + if (gestureElapsed > 0 && this.gestureFrameTimes.length > 0) { + this.gestureAverageFPS = Math.min(120, Math.round((this.gestureFrameTimes.length * 1000) / gestureElapsed)); + } + } + + // Trigger callback every 10 valid frames + if (this.onStatsUpdate && this.validFrames % 10 === 0) { + this.onStatsUpdate(this.getStats()); + } + } + + onGestureStart() { + if (!this.enabled) return; + + this.gestureStartTime = Date.now(); + this.gestureFrameTimes = []; + this.gestureMinFPS = Infinity; + this.gestureResponseTime = 0; + } + + onGestureFirstFrame() { + if (!this.enabled || this.gestureStartTime === 0) return; + + // Measure time from gesture start to first rendered frame + this.gestureResponseTime = Date.now() - this.gestureStartTime; + } + + onGestureEnd() { + if (!this.enabled) return; + + this.gestureStartTime = 0; + + // Trigger final gesture stats + if (this.onStatsUpdate) { + this.onStatsUpdate(this.getStats()); + } + } + + getStats() { + const droppedFrameRate = this.validFrames > 0 + ? Math.round((this.droppedFrames / this.validFrames) * 100) + : 0; + + return { + // Overall metrics + currentFPS: this.currentFPS, + averageFPS: this.averageFPS, + minFPS: this.minFPS === Infinity ? 0 : this.minFPS, + maxFPS: this.maxFPS, + + // Frame timing + averageFrameTime: this.averageFrameTime, + maxFrameTime: this.maxFrameTime, + targetFrameTime: this.targetFrameTime, + + // Frame drops + droppedFrames: this.droppedFrames, + totalFrames: this.totalFrames, + validFrames: this.validFrames, + droppedFrameRate: droppedFrameRate, + + // Gesture-specific + gestureResponseTime: this.gestureResponseTime, + gestureAverageFPS: this.gestureAverageFPS, + gestureMinFPS: this.gestureMinFPS === Infinity ? 0 : this.gestureMinFPS, + isGestureActive: this.gestureStartTime > 0, + + // Smoothness rating (0-100) + smoothness: this.calculateSmoothness() + }; + } + + calculateSmoothness() { + // Calculate smoothness score based on multiple factors + // 100 = perfectly smooth, 0 = very stuttery + + if (this.validFrames < 10) { + return 100; // Not enough data + } + + // Factor 1: Average FPS (40% weight) + // Perfect score at 60+ FPS, linear scale below + const fpsScore = Math.min(100, (this.averageFPS / 60) * 100); + + // Factor 2: Frame drops (30% weight) + // Perfect score at 0%, linearly decrease (2% drop = -4 points) + const dropRate = this.droppedFrames / this.validFrames; + const dropScore = Math.max(0, 100 - dropRate * 100 * 2); + + // Factor 3: Frame time variance (30% weight) + // Low variance = smooth, high variance = jittery + let varianceScore = 100; + if (this.frameTimes.length > 1) { + const stdDev = this.calculateStdDev(this.frameTimes); + // Standard deviation > 5ms indicates noticeable jitter + // Linearly penalize: 5ms stdDev = 50 points, 10ms = 0 points + varianceScore = Math.max(0, 100 - (stdDev / 10) * 100); + } + + // Weighted average + const score = fpsScore * 0.4 + dropScore * 0.3 + varianceScore * 0.3; + + return Math.round(score); + } + + calculateStdDev(values) { + if (values.length === 0) return 0; + + const mean = values.reduce((a, b) => a + b, 0) / values.length; + const squaredDiffs = values.map(value => Math.pow(value - mean, 2)); + const variance = squaredDiffs.reduce((a, b) => a + b, 0) / values.length; + return Math.sqrt(variance); + } + + getSummary() { + const stats = this.getStats(); + + let quality = 'Excellent'; + if (stats.smoothness < 90) quality = 'Good'; + if (stats.smoothness < 75) quality = 'Fair'; + if (stats.smoothness < 60) quality = 'Poor'; + if (stats.smoothness < 40) quality = 'Very Poor'; + + return { + quality: quality, + smoothness: stats.smoothness, + averageFPS: stats.averageFPS, + droppedFrameRate: stats.droppedFrameRate, + gestureAverageFPS: stats.gestureAverageFPS, + gestureResponseTime: stats.gestureResponseTime + }; + } + + logStats() { + const stats = this.getStats(); + console.log('=== Performance Statistics ==='); + console.log(`FPS: ${stats.currentFPS} (avg: ${stats.averageFPS}, min: ${stats.minFPS}, max: ${stats.maxFPS})`); + console.log(`Frame Time: ${stats.averageFrameTime}ms (max: ${stats.maxFrameTime}ms, target: ${stats.targetFrameTime}ms)`); + console.log(`Frames: ${stats.validFrames} valid / ${stats.totalFrames} total`); + console.log(`Dropped Frames: ${stats.droppedFrames}/${stats.validFrames} (${stats.droppedFrameRate}%)`); + console.log(`Gesture Response: ${stats.gestureResponseTime}ms`); + console.log(`Gesture FPS: ${stats.gestureAverageFPS} (min: ${stats.gestureMinFPS})`); + console.log(`Smoothness Score: ${stats.smoothness}/100 (${this.getSummary().quality})`); + } +} + +export { PerformanceMonitor }; From 705c2703e9f134ae7530877a9e4be9b35d256a23 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Thu, 29 Jan 2026 11:13:33 +0800 Subject: [PATCH 276/678] Refactor PAGX loading to use PAGXImporter for XML parsing. --- pagx/viewer/src/PAGXView.cpp | 15 +++++++++------ pagx/wechat/src/PAGXView.cpp | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/pagx/viewer/src/PAGXView.cpp b/pagx/viewer/src/PAGXView.cpp index 4f0b2f0119..3a863f3d69 100644 --- a/pagx/viewer/src/PAGXView.cpp +++ b/pagx/viewer/src/PAGXView.cpp @@ -19,6 +19,7 @@ #include "PAGXView.h" #include #include "GridBackground.h" +#include "pagx/PAGXImporter.h" #include "tgfx/core/Data.h" #include "tgfx/core/Typeface.h" @@ -76,15 +77,17 @@ void PAGXView::loadPAGX(const val& pagxData) { if (!data) { return; } - LayerBuilder::Options options; + // LayerBuilder::Options options; // options.fallbackTypefaces = fallbackTypefaces; - auto content = pagx::LayerBuilder::FromData(data->bytes(), data->size(), options); - if (!content.root) { + // auto content = pagx::LayerBuilder::FromData(data->bytes(), data->size(), options); + auto doc = PAGXImporter::FromXML(data->bytes(), data->size()); + auto content = pagx::LayerBuilder::Build(doc.get()); + if (!content) { return; } - contentLayer = content.root; - pagxWidth = content.width; - pagxHeight = content.height; + contentLayer = content; + pagxWidth = doc->width; + pagxHeight = doc->height; displayList.root()->removeChildren(); displayList.root()->addChild(contentLayer); applyCenteringTransform(); diff --git a/pagx/wechat/src/PAGXView.cpp b/pagx/wechat/src/PAGXView.cpp index 1007edaf2b..6dfe18a382 100644 --- a/pagx/wechat/src/PAGXView.cpp +++ b/pagx/wechat/src/PAGXView.cpp @@ -23,6 +23,7 @@ #include "tgfx/core/Data.h" #include "tgfx/core/Stream.h" #include "tgfx/core/Typeface.h" +#include "pagx/PAGXImporter.h" using namespace emscripten; @@ -74,15 +75,17 @@ void PAGXView::loadPAGX(const val& pagxData) { if (!data) { return; } - LayerBuilder::Options options; + // LayerBuilder::Options options; // options.fallbackTypefaces = fallbackTypefaces; - auto content = pagx::LayerBuilder::FromData(data->bytes(), data->size(), options); - if (!content.root) { + // auto content = pagx::LayerBuilder::FromData(data->bytes(), data->size(), options); + auto doc = PAGXImporter::FromXML(data->bytes(), data->size()); + auto content = pagx::LayerBuilder::Build(doc.get()); + if (!content) { return; } - contentLayer = content.root; - pagxWidth = content.width; - pagxHeight = content.height; + contentLayer = content; + pagxWidth = doc->width; + pagxHeight = doc->height; displayList.root()->removeChildren(); displayList.root()->addChild(contentLayer); applyCenteringTransform(); From fcfd5e9598e5d11659b544308456693ed934215b Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 29 Jan 2026 11:16:51 +0800 Subject: [PATCH 277/678] Replace error alert with inline error UI for better user experience. --- pagx/viewer/index.css | 33 +++++++++++++++++++++++++++++++++ pagx/viewer/index.html | 10 ++++++++++ pagx/viewer/index.ts | 35 +++++++++++++++++++++++++++++------ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/pagx/viewer/index.css b/pagx/viewer/index.css index b99dc9df54..a4fee5d707 100644 --- a/pagx/viewer/index.css +++ b/pagx/viewer/index.css @@ -149,6 +149,39 @@ html, body { margin: 12px 0 0 0; } +.error-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 60px 80px; +} + +.error-content.hidden { + display: none; +} + +.error-icon { + width: 64px; + height: 64px; + color: #e74c3c; + margin-bottom: 20px; +} + +.error-title { + color: #ccc; + font-size: 20px; + margin: 0 0 12px 0; +} + +.error-message { + color: #888; + font-size: 14px; + margin: 0 0 24px 0; + max-width: 400px; + text-align: center; + word-break: break-word; +} + .toolbar { position: absolute; top: 16px; diff --git a/pagx/viewer/index.html b/pagx/viewer/index.html index b21e3c50e3..3c5e51517d 100644 --- a/pagx/viewer/index.html +++ b/pagx/viewer/index.html @@ -31,6 +31,16 @@

    0%

    +
    -
    -
    - -
    - ${content} -
    -
    - - - - -`; + const vars = { + htmlLang: isEnglish ? 'en' : 'zh-CN', + title, + faviconUrl, + viewerUrl, + tocTitle: isEnglish ? 'Table of Contents' : '目录', + tocLabel: isEnglish ? 'TOC' : '目录', + viewerLabel: isEnglish ? 'Viewer' : '在线预览', + currentLang: isEnglish ? 'English' : '简体中文', + enUrl: isEnglish ? '#' : langSwitchUrl, + zhUrl: isEnglish ? langSwitchUrl : '#', + enActive: isEnglish ? ' class="active"' : '', + zhActive: isEnglish ? '' : ' class="active"', + tocHtml, + content, + }; + + return TEMPLATE.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] || ''); } /** * Publish a single spec file. - * For Chinese version, uses englishSlugs to generate English anchor IDs. */ -function publishSpec(specFile, outputDir, lang, version, showDraft, langSwitchUrl, viewerUrl, faviconUrl, englishSlugs = null) { +function publishSpec(specFile, outputDir, lang, langSwitchUrl, viewerUrl, faviconUrl, englishSlugs = null) { if (!fs.existsSync(specFile)) { console.log(` Skipped (file not found): ${specFile}`); return; } console.log(` Reading: ${specFile}`); - let mdContent = fs.readFileSync(specFile, 'utf-8'); - - // Get file last modified time - const stats = fs.statSync(specFile); - const lastModified = stats.mtime; - - // Format date based on language - const dateStr = formatDate(lastModified, lang); - - // Generate version info HTML - const isEnglish = lang === 'en'; - const lastUpdatedLabel = isEnglish ? `Last updated: ${dateStr}` : `最后更新:${dateStr}`; - - // Inject version into title and add last updated below - mdContent = mdContent.replace(/^(#\s+.+)$/m, `$1 ${version}\n\n

    ${lastUpdatedLabel}

    `); + const mdContent = fs.readFileSync(specFile, 'utf-8'); - // Extract title const titleMatch = mdContent.match(/^#\s+(.+)$/m); const title = titleMatch ? titleMatch[1] : 'PAGX Format Specification'; - // Parse headings for TOC (use English slugs for Chinese version) const headings = parseMarkdownHeadings(mdContent, englishSlugs); - - // Generate TOC HTML const tocHtml = generateTocHtml(headings); - // Convert Markdown to HTML using marked const marked = createMarkedInstance(); let htmlContent = marked.parse(mdContent); - // For Chinese version, replace heading IDs with English slugs if (englishSlugs) { htmlContent = replaceHeadingIds(htmlContent, englishSlugs); } - // Generate complete HTML document - const html = generateHtml(htmlContent, title, tocHtml, lang, showDraft, langSwitchUrl, viewerUrl, faviconUrl); + const html = generateHtml(htmlContent, title, tocHtml, lang, langSwitchUrl, viewerUrl, faviconUrl); - // Create output directory fs.mkdirSync(outputDir, { recursive: true }); - - // Write output file const outputFile = path.join(outputDir, 'index.html'); fs.writeFileSync(outputFile, html, 'utf-8'); - console.log(` Published: ${outputFile}`); } +/** + * Generate redirect page in latest folder. + */ +function generateRedirectPage(siteDir, version) { + const html = ` + + + + + PAGX Specification + + + +

    Redirecting to the latest specification...

    + +`; + + const latestDir = path.join(siteDir, 'latest'); + fs.mkdirSync(latestDir, { recursive: true }); + const outputFile = path.join(latestDir, 'index.html'); + fs.writeFileSync(outputFile, html, 'utf-8'); + console.log(` Generated: ${outputFile}`); +} + +/** + * Find all published versions in the site directory. + */ +function findPublishedVersions(siteDir) { + if (!fs.existsSync(siteDir)) return []; + + const entries = fs.readdirSync(siteDir, { withFileTypes: true }); + const versions = []; + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + // Skip non-version directories + if (['latest', 'fonts', 'wasm-mt', 'node_modules', 'viewer'].includes(entry.name)) continue; + // Version directories must match semver pattern (e.g., 1.0, 0.5, 2.1.3) + if (!/^\d+\.\d+(\.\d+)?$/.test(entry.name)) continue; + // Check if it looks like a version (contains index.html) + const indexPath = path.join(siteDir, entry.name, 'index.html'); + if (fs.existsSync(indexPath)) { + versions.push(entry.name); + } + } + + return versions; +} + +/** + * Generate version info block HTML (left border style). + * - Stable (latest): green border + * - Draft: yellow border with draft warning + * - Outdated: red border with outdated warning + */ +function generateVersionInfoHtml(thisVersion, draftVersion, stableVersion, isZh, lastUpdated) { + const langSuffix = isZh ? 'zh/' : ''; + const thisUrl = `${BASE_URL}/${thisVersion}/${langSuffix}`; + const latestUrl = `${BASE_URL}/latest/`; + + // Check version status + const isOutdated = stableVersion && thisVersion !== stableVersion && + thisVersion.localeCompare(stableVersion, undefined, { numeric: true }) < 0; + const isDraft = !isOutdated && thisVersion !== stableVersion; + + // Determine status class and text + let statusClass, statusText; + if (isOutdated) { + statusClass = 'outdated'; + statusText = isZh + ? `版本 ${thisVersion} (过时) — 当前版本不是最新版本,请查阅最新版本获取最新内容。` + : `Version ${thisVersion} (Outdated) — This is not the latest version. Please refer to the latest version for up-to-date content.`; + } else if (isDraft) { + statusClass = 'draft'; + statusText = isZh + ? `版本 ${thisVersion} (草稿) — 本规范为草稿版本,内容可能随时变更。` + : `Version ${thisVersion} (Draft) — This specification is a working draft and may change at any time.`; + } else { + statusClass = 'stable'; + statusText = isZh ? `版本 ${thisVersion} (正式)` : `Version ${thisVersion} (Stable)`; + } + + // Generate labels + const labels = isZh + ? { updated: '最后更新:', this: '当前版本:', latest: '最新版本:', draft: '草稿版本:' } + : { updated: 'Last updated:', this: 'This version:', latest: 'Latest version:', draft: "Editor's draft:" }; + + // Generate links + let linksHtml = `

    ${labels.updated} ${lastUpdated}

    +

    ${labels.this} ${thisUrl}

    +

    ${labels.latest} ${latestUrl}

    `; + + if (draftVersion !== stableVersion) { + const draftUrl = `${BASE_URL}/${draftVersion}/${langSuffix}`; + linksHtml += `\n

    ${labels.draft} ${draftUrl}

    `; + } + + return `
    +
    ${statusText}
    +${linksHtml} +
    `; +} + +/** + * Update version links in an HTML file. + * Removes old version links block and inserts new one after the h1 title. + * Preserves the original "last updated" time from the existing HTML. + */ +function updateVersionLinks(htmlFile, thisVersion, draftVersion, stableVersion, isZh) { + if (!fs.existsSync(htmlFile)) return false; + + let html = fs.readFileSync(htmlFile, 'utf-8'); + + // Extract existing "last updated" date from HTML, preserve it + // Matches: "30 January 2026" or "2026 年 1 月 30 日" + const lastUpdatedRegex = /(\d{1,2}\s+\w+\s+\d{4}|\d{4}\s*年\s*\d{1,2}\s*月\s*\d{1,2}\s*日)/; + const lastUpdatedMatch = html.match(lastUpdatedRegex); + const lastUpdated = lastUpdatedMatch ? lastUpdatedMatch[1] : formatDate(new Date(), isZh ? 'zh' : 'en'); + + // Generate version info block + const versionInfoHtml = generateVersionInfoHtml(thisVersion, draftVersion, stableVersion, isZh, lastUpdated); + + + // Update CSS: replace old version-info styles with new ones (with background colors) + const versionInfoCss = `/* Version info block */ + .version-info { + border-left: 4px solid; + padding: 12px 16px; + margin-bottom: 24px; + } + .version-info.stable { + border-color: #1e7e34; + background-color: #f0f9f1; + } + .version-info.draft { + border-color: #d4a000; + background-color: #fffbf0; + } + .version-info.outdated { + border-color: #c42c2c; + background-color: #fef6f6; + } + .version-info .version-status { + font-weight: 600; + margin-bottom: 8px; + } + .version-info.stable .version-status { color: #1e5631; } + .version-info.draft .version-status { color: #6a4c00; } + .version-info.outdated .version-status { color: #82071e; } + .version-info p { margin-bottom: 4px; font-size: 14px; color: #555; }`; + + // Replace existing version-info CSS block or insert new one + if (html.includes('/* Version info block */')) { + html = html.replace( + /\/\* Version info block \*\/[\s\S]*?\.version-info p \{[^}]+\}/, + versionInfoCss + ); + } else { + // Insert after img styles + html = html.replace( + /(img \{[^}]+\})/, + `$1\n\n ${versionInfoCss}` + ); + } + + // Restore h1 border-bottom if it was removed + html = html.replace( + /h1 \{ font-size: 2em; margin-bottom: 16px; \}/, + 'h1 { font-size: 2em; padding-bottom: 0.3em; border-bottom: 1px solid var(--border-color); margin-bottom: 16px; }' + ); + + + // Pattern to match everything from h1 closing tag to first h2 opening tag + const versionBlockRegex = /(]*>)[^<]*(<\/h1>)[\s\S]*?(]*>([^<]*)<\/h1>/); + let title = titleMatch ? titleMatch[1] : ''; + + // Remove version number from title if present + title = title.replace(/\s+\d+\.\d+(\.\d+)?$/, ''); + + // Also update the h1 id + const cleanH1Open = h1Open.replace(/id="[^"]*"/, 'id="pagx-format-specification"'); + + // Structure: title -> version info block -> content + const newContent = `${cleanH1Open}${title}${h1Close}\n${versionInfoHtml}\n${h2Open}`; + html = html.replace(versionBlockRegex, newContent); + fs.writeFileSync(htmlFile, html, 'utf-8'); + return true; + } + return false; +} + +/** + * Update version links in all published versions. + */ +function updateAllVersionLinks(siteDir, draftVersion, stableVersion) { + const versions = findPublishedVersions(siteDir); + if (versions.length === 0) return; + + console.log('\nUpdating version links in all versions...'); + + for (const ver of versions) { + const enFile = path.join(siteDir, ver, 'index.html'); + const zhFile = path.join(siteDir, ver, 'zh', 'index.html'); + + if (updateVersionLinks(enFile, ver, draftVersion, stableVersion, false)) { + console.log(` Updated: ${enFile}`); + } + if (updateVersionLinks(zhFile, ver, draftVersion, stableVersion, true)) { + console.log(` Updated: ${zhFile}`); + } + } +} + /** * Parse command line arguments. */ function parseArgs() { const args = process.argv.slice(2); - const options = { - siteDir: DEFAULT_SITE_DIR, - }; + const options = { siteDir: DEFAULT_SITE_DIR }; for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if ((arg === '-o' || arg === '--output') && args[i + 1]) { - options.siteDir = path.resolve(args[i + 1]); - i++; - } else if (arg === '--help' || arg === '-h') { + if ((args[i] === '-o' || args[i] === '--output') && args[i + 1]) { + options.siteDir = path.resolve(args[++i]); + } else if (args[i] === '-h' || args[i] === '--help') { console.log(` PAGX Specification Publisher -Usage: - npm run publish [-- -o ] +Usage: npm run publish [-- -o ] Options: -o, --output Output directory (default: ../public) -h, --help Show this help message - -Configuration (package.json): - version - Current version to publish - stableVersion - Latest stable version (empty if none) - If version != stableVersion, draft banner is shown - -Source files: - spec/pagx_spec.md - English version - spec/pagx_spec.zh_CN.md - Chinese version - -Output structure: - /index.html - Redirect page - //index.html - English (default) - //zh/index.html - Chinese `); process.exit(0); } } - return options; } @@ -808,102 +548,44 @@ Output structure: * Main function. */ function main() { - const options = parseArgs(); - const siteDir = options.siteDir; - - // Read versions from package.json + const { siteDir } = parseArgs(); const version = getVersion(); const stableVersion = getStableVersion(); - const isDraft = version !== stableVersion; console.log(`Version: ${version}`); console.log(`Stable: ${stableVersion || '(none)'}`); console.log(`Output: ${siteDir}`); - if (isDraft) { - console.log('Mode: Draft'); - } const baseOutputDir = path.join(siteDir, version); - - // Viewer URL (relative path from spec pages to viewer) - const viewerUrlFromRoot = '../viewer/'; - const viewerUrlFromZh = '../../viewer/'; - - // Favicon URL (relative path from spec pages to favicon) + const viewerUrlFromRoot = '../'; + const viewerUrlFromZh = '../../'; const faviconUrlFromRoot = '../favicon.png'; const faviconUrlFromZh = '../../favicon.png'; - // Extract English slugs for Chinese version to use the same anchor IDs let englishSlugs = null; if (fs.existsSync(SPEC_FILE_EN)) { - const enContent = fs.readFileSync(SPEC_FILE_EN, 'utf-8'); - englishSlugs = extractEnglishSlugs(enContent); + englishSlugs = extractEnglishSlugs(fs.readFileSync(SPEC_FILE_EN, 'utf-8')); console.log(`\nExtracted ${englishSlugs.length} heading slugs from English version`); } - // Publish English version (default, at root) console.log('\nPublishing English version...'); - publishSpec(SPEC_FILE_EN, baseOutputDir, 'en', version, isDraft, 'zh/', viewerUrlFromRoot, faviconUrlFromRoot); + publishSpec(SPEC_FILE_EN, baseOutputDir, 'en', 'zh/', viewerUrlFromRoot, faviconUrlFromRoot, englishSlugs); - // Publish Chinese version (under /zh/) with English anchor IDs console.log('\nPublishing Chinese version...'); - publishSpec(SPEC_FILE_ZH, path.join(baseOutputDir, 'zh'), 'zh', version, isDraft, '../', viewerUrlFromZh, faviconUrlFromZh, englishSlugs); + publishSpec(SPEC_FILE_ZH, path.join(baseOutputDir, 'zh'), 'zh', '../', viewerUrlFromZh, faviconUrlFromZh, englishSlugs); - // Generate redirect index page (point to stableVersion if exists, otherwise current version) - const redirectVersion = stableVersion || version; console.log('\nGenerating redirect page...'); - console.log(` Redirect to: ${redirectVersion}`); - generateRedirectPage(siteDir, redirectVersion); + console.log(` Redirect to: ${stableVersion || version}`); + generateRedirectPage(siteDir, stableVersion || version); + + // Update version links in all published versions + updateAllVersionLinks(siteDir, version, stableVersion); - // Copy favicon console.log('\nCopying favicon...'); - const faviconSrc = path.join(SPEC_DIR, 'favicon.png'); - const faviconDest = path.join(siteDir, 'favicon.png'); - fs.copyFileSync(faviconSrc, faviconDest); - console.log(` Copied: ${faviconDest}`); + fs.copyFileSync(path.join(SPEC_DIR, 'favicon.png'), path.join(siteDir, 'favicon.png')); + console.log(` Copied: ${path.join(siteDir, 'favicon.png')}`); console.log('\nDone!'); } -/** - * Generate the redirect index page at site/index.html. - */ -function generateRedirectPage(siteDir, version) { - const html = ` - - - - - PAGX Specification - - - -

    Redirecting to the latest specification...

    - - -`; - - fs.mkdirSync(siteDir, { recursive: true }); - const outputFile = path.join(siteDir, 'index.html'); - fs.writeFileSync(outputFile, html, 'utf-8'); - console.log(` Generated: ${outputFile}`); -} - main(); diff --git a/pagx/spec/script/template.html b/pagx/spec/script/template.html new file mode 100644 index 0000000000..9e0056ed26 --- /dev/null +++ b/pagx/spec/script/template.html @@ -0,0 +1,346 @@ + + + + + + {{title}} + + + + + +
    + + + + {{viewerLabel}} + +
    + + +
    +
    + +
    + +
    +{{content}} +
    +
    + + + + diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 394e488fa5..a4c749702b 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -141,7 +141,7 @@ static void CollectVectorGlyph(PAGXDocument* document, const tgfx::Font& font, glyphPath.transform(tgfx::Matrix::MakeScale(scale, scale)); if (builder.font == nullptr) { - builder.font = document->makeNode("vector_font"); + builder.font = document->makeNode(); builder.font->unitsPerEm = kVectorFontUnitsPerEm; } @@ -177,7 +177,7 @@ static void CollectBitmapGlyph( } builder.backingSize = static_cast(std::round(font.getSize() / scaleX)); builder.typeface = typeface; - builder.font = document->makeNode("bitmap_font"); + builder.font = document->makeNode(); builder.font->unitsPerEm = builder.backingSize; } @@ -331,6 +331,17 @@ bool FontEmbedder::embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { } } + // Assign sequential IDs to all fonts + int fontIndex = 1; + if (vectorBuilder.font != nullptr) { + vectorBuilder.font->id = "font" + std::to_string(fontIndex++); + } + for (auto& [typeface, builder] : bitmapBuilders) { + if (builder.font != nullptr) { + builder.font->id = "font" + std::to_string(fontIndex++); + } + } + // Third pass: create GlyphRuns for each Text for (const auto& [text, textBlob] : textGlyphs.textBlobs) { if (textBlob == nullptr) { diff --git a/pagx/viewer/index.css b/pagx/viewer/index.css index d74331a3d0..eb12cba18b 100644 --- a/pagx/viewer/index.css +++ b/pagx/viewer/index.css @@ -27,6 +27,10 @@ html, body { touch-action: none; } +.canvas.hidden { + display: none; +} + .canvas:active { cursor: grabbing; } @@ -65,13 +69,15 @@ html, body { flex-direction: column; align-items: center; justify-content: center; - min-height: 160px; + width: 420px; + height: 280px; padding: 60px 80px; border: 2px dashed #555; border-radius: 16px; background: rgba(255, 255, 255, 0.02); cursor: pointer; transition: all 0.3s ease; + box-sizing: border-box; } .drop-zone-content:hover { @@ -170,10 +176,11 @@ html, body { flex-direction: column; align-items: center; justify-content: center; - min-height: 160px; - padding: 60px 80px; + width: 420px; + height: 280px; border: 2px solid transparent; border-radius: 16px; + box-sizing: border-box; } .loading-content.hidden { @@ -213,11 +220,12 @@ html, body { flex-direction: column; align-items: center; justify-content: center; - min-height: 160px; - padding: 60px 80px; + width: 420px; + height: 280px; border: 2px solid transparent; border-radius: 16px; cursor: pointer; + box-sizing: border-box; } .error-content:hover .error-icon { diff --git a/pagx/viewer/index.html b/pagx/viewer/index.html index a680b8c805..3f92e46256 100644 --- a/pagx/viewer/index.html +++ b/pagx/viewer/index.html @@ -13,7 +13,7 @@
    diff --git a/pagx/viewer/index.ts b/pagx/viewer/index.ts index 40540cfcf5..d9b7bf2f7e 100644 --- a/pagx/viewer/index.ts +++ b/pagx/viewer/index.ts @@ -35,6 +35,7 @@ interface I18nStrings { invalidFile: string; spec: string; specTitle: string; + leave: string; } const i18n: Record = { @@ -53,6 +54,7 @@ const i18n: Record = { invalidFile: 'Please drop a .pagx file', spec: 'Spec', specTitle: 'PAGX Specification', + leave: 'Leave', }, zh: { dropText: '拖放 PAGX 文件到此处', @@ -69,6 +71,7 @@ const i18n: Record = { invalidFile: '请拖放 .pagx 文件', spec: 'Spec', specTitle: 'PAGX 格式规范', + leave: '离开', }, }; @@ -197,6 +200,20 @@ class GestureManager { private mouseWheelRatio = 800; private touchWheelRatio = 100; + // Drag state + private isDragging = false; + private dragStartX = 0; + private dragStartY = 0; + private dragStartOffsetX = 0; + private dragStartOffsetY = 0; + + // Touch state + private lastTouchDistance = 0; + private lastTouchCenterX = 0; + private lastTouchCenterY = 0; + private isTouchPanning = false; + private isTouchZooming = false; + public zoom = 1.0; public offsetX = 0; public offsetY = 0; @@ -315,6 +332,135 @@ class GestureManager { } } } + + // Mouse drag handlers + public onMouseDown(event: MouseEvent, canvas: HTMLElement) { + // Only handle left mouse button + if (event.button !== 0) { + return; + } + this.isDragging = true; + this.dragStartX = event.clientX; + this.dragStartY = event.clientY; + this.dragStartOffsetX = this.offsetX; + this.dragStartOffsetY = this.offsetY; + canvas.style.cursor = 'grabbing'; + } + + public onMouseMove(event: MouseEvent, viewerState: ViewerState) { + if (!this.isDragging) { + return; + } + const deltaX = (event.clientX - this.dragStartX) * window.devicePixelRatio; + const deltaY = (event.clientY - this.dragStartY) * window.devicePixelRatio; + this.offsetX = this.dragStartOffsetX + deltaX; + this.offsetY = this.dragStartOffsetY + deltaY; + viewerState.pagxView?.updateZoomScaleAndOffset(this.zoom, this.offsetX, this.offsetY); + } + + public onMouseUp(canvas: HTMLElement) { + this.isDragging = false; + canvas.style.cursor = 'grab'; + } + + // Touch handlers + private getTouchDistance(touches: TouchList): number { + if (touches.length < 2) { + return 0; + } + const dx = touches[0].clientX - touches[1].clientX; + const dy = touches[0].clientY - touches[1].clientY; + return Math.sqrt(dx * dx + dy * dy); + } + + private getTouchCenter(touches: TouchList): { x: number; y: number } { + if (touches.length < 2) { + return { x: touches[0].clientX, y: touches[0].clientY }; + } + return { + x: (touches[0].clientX + touches[1].clientX) / 2, + y: (touches[0].clientY + touches[1].clientY) / 2, + }; + } + + public onTouchStart(event: TouchEvent, canvas: HTMLElement) { + if (event.touches.length === 1) { + // Single finger pan + this.isTouchPanning = true; + this.isTouchZooming = false; + this.dragStartX = event.touches[0].clientX; + this.dragStartY = event.touches[0].clientY; + this.dragStartOffsetX = this.offsetX; + this.dragStartOffsetY = this.offsetY; + } else if (event.touches.length === 2) { + // Two finger zoom/pan + this.isTouchPanning = false; + this.isTouchZooming = true; + this.lastTouchDistance = this.getTouchDistance(event.touches); + const center = this.getTouchCenter(event.touches); + this.lastTouchCenterX = center.x; + this.lastTouchCenterY = center.y; + this.scaleStartZoom = this.zoom; + this.dragStartOffsetX = this.offsetX; + this.dragStartOffsetY = this.offsetY; + } + } + + public onTouchMove(event: TouchEvent, canvas: HTMLElement, viewerState: ViewerState) { + if (event.touches.length === 1 && this.isTouchPanning) { + // Single finger pan + const deltaX = (event.touches[0].clientX - this.dragStartX) * window.devicePixelRatio; + const deltaY = (event.touches[0].clientY - this.dragStartY) * window.devicePixelRatio; + this.offsetX = this.dragStartOffsetX + deltaX; + this.offsetY = this.dragStartOffsetY + deltaY; + viewerState.pagxView?.updateZoomScaleAndOffset(this.zoom, this.offsetX, this.offsetY); + } else if (event.touches.length === 2 && this.isTouchZooming) { + // Two finger zoom and pan + const currentDistance = this.getTouchDistance(event.touches); + const center = this.getTouchCenter(event.touches); + const rect = canvas.getBoundingClientRect(); + const pixelX = (center.x - rect.left) * window.devicePixelRatio; + const pixelY = (center.y - rect.top) * window.devicePixelRatio; + + // Calculate zoom + if (this.lastTouchDistance > 0) { + const scale = currentDistance / this.lastTouchDistance; + const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.zoom * scale)); + + // Zoom around pinch center + this.offsetX = (this.offsetX - pixelX) * (newZoom / this.zoom) + pixelX; + this.offsetY = (this.offsetY - pixelY) * (newZoom / this.zoom) + pixelY; + this.zoom = newZoom; + } + + // Also handle pan during pinch + const centerDeltaX = (center.x - this.lastTouchCenterX) * window.devicePixelRatio; + const centerDeltaY = (center.y - this.lastTouchCenterY) * window.devicePixelRatio; + this.offsetX += centerDeltaX; + this.offsetY += centerDeltaY; + + this.lastTouchDistance = currentDistance; + this.lastTouchCenterX = center.x; + this.lastTouchCenterY = center.y; + + viewerState.pagxView?.updateZoomScaleAndOffset(this.zoom, this.offsetX, this.offsetY); + } + } + + public onTouchEnd(event: TouchEvent, canvas: HTMLElement) { + if (event.touches.length === 0) { + this.isTouchPanning = false; + this.isTouchZooming = false; + } else if (event.touches.length === 1) { + // Switched from pinch to single finger + this.isTouchPanning = true; + this.isTouchZooming = false; + this.dragStartX = event.touches[0].clientX; + this.dragStartY = event.touches[0].clientY; + this.dragStartOffsetX = this.offsetX; + this.dragStartOffsetY = this.offsetY; + } + } } const viewerState = new ViewerState(); @@ -531,11 +677,46 @@ function setupVisibilityListeners() { } function bindCanvasEvents(canvas: HTMLElement) { + // Set initial cursor style + canvas.style.cursor = 'grab'; + + // Wheel events for scroll and zoom canvas.addEventListener('wheel', (e: WheelEvent) => { e.preventDefault(); gestureManager.onWheel(e, canvas, viewerState); }, { passive: false }); + // Mouse drag events + canvas.addEventListener('mousedown', (e: MouseEvent) => { + e.preventDefault(); + gestureManager.onMouseDown(e, canvas); + }); + canvas.addEventListener('mousemove', (e: MouseEvent) => { + gestureManager.onMouseMove(e, viewerState); + }); + canvas.addEventListener('mouseup', () => { + gestureManager.onMouseUp(canvas); + }); + canvas.addEventListener('mouseleave', () => { + gestureManager.onMouseUp(canvas); + }); + + // Touch events for mobile + canvas.addEventListener('touchstart', (e: TouchEvent) => { + e.preventDefault(); + gestureManager.onTouchStart(e, canvas); + }, { passive: false }); + canvas.addEventListener('touchmove', (e: TouchEvent) => { + e.preventDefault(); + gestureManager.onTouchMove(e, canvas, viewerState); + }, { passive: false }); + canvas.addEventListener('touchend', (e: TouchEvent) => { + gestureManager.onTouchEnd(e, canvas); + }); + canvas.addEventListener('touchcancel', (e: TouchEvent) => { + gestureManager.onTouchEnd(e, canvas); + }); + // Prevent browser pinch-to-zoom on Safari canvas.addEventListener('gesturestart', (e: Event) => { e.preventDefault(); @@ -596,10 +777,28 @@ function hideDropZone(): void { } } +const DEFAULT_TITLE = 'PAGX Viewer'; + +function goHome(): void { + const toolbar = document.getElementById('toolbar') as HTMLDivElement; + const specBtn = document.getElementById('spec-btn') as HTMLAnchorElement; + const canvas = document.getElementById('pagx-canvas') as HTMLCanvasElement; + + if (viewerState.pagxView) { + viewerState.pagxView.loadPAGX(new Uint8Array(0)); + gestureManager.resetTransform(viewerState); + } + canvas.classList.add('hidden'); + toolbar.classList.add('hidden'); + specBtn.classList.remove('hidden'); + document.title = DEFAULT_TITLE; + showDropZoneUI(); +} + async function loadPAGXData(data: Uint8Array, name: string) { - const fileName = document.getElementById('file-name') as HTMLSpanElement; const specBtn = document.getElementById('spec-btn') as HTMLAnchorElement; const toolbar = document.getElementById('toolbar') as HTMLDivElement; + const canvas = document.getElementById('pagx-canvas') as HTMLCanvasElement; if (!viewerState.pagxView) { throw new Error('PAGXView not initialized'); @@ -610,9 +809,10 @@ async function loadPAGXData(data: Uint8Array, name: string) { gestureManager.resetTransform(viewerState); updateSize(); hideDropZone(); + canvas.classList.remove('hidden'); toolbar.classList.remove('hidden'); specBtn.classList.add('hidden'); - fileName.textContent = name; + document.title = 'PAGX Viewer - ' + name; } async function loadPAGXFile(file: File) { @@ -713,6 +913,7 @@ function setupDragAndDrop() { const errorContent = document.getElementById('error-content') as HTMLDivElement; const container = document.getElementById('container') as HTMLDivElement; const fileInput = document.getElementById('file-input') as HTMLInputElement; + const leaveBtn = document.getElementById('leave-btn') as HTMLButtonElement; const openBtn = document.getElementById('open-btn') as HTMLButtonElement; const resetBtn = document.getElementById('reset-btn') as HTMLButtonElement; @@ -759,6 +960,10 @@ function setupDragAndDrop() { fileInput.click(); }); + leaveBtn.addEventListener('click', () => { + goHome(); + }); + openBtn.addEventListener('click', () => { fileInput.click(); }); @@ -814,6 +1019,7 @@ function applyI18n(): void { const errorTitle = document.querySelector('.error-title'); const openBtn = document.getElementById('open-btn'); const resetBtn = document.getElementById('reset-btn'); + const leaveBtn = document.getElementById('leave-btn'); if (dropText) dropText.textContent = strings.dropText; if (dropSubtext) dropSubtext.textContent = strings.dropSubtext; @@ -821,13 +1027,12 @@ function applyI18n(): void { if (errorTitle) errorTitle.textContent = strings.errorTitle; if (openBtn) openBtn.title = strings.openFile; if (resetBtn) resetBtn.title = strings.resetView; + if (leaveBtn) leaveBtn.title = strings.leave; const specBtn = document.getElementById('spec-btn'); const specBtnText = document.getElementById('spec-btn-text'); - const toolbarSpecBtn = document.getElementById('toolbar-spec-btn'); if (specBtn) specBtn.title = strings.specTitle; if (specBtnText) specBtnText.textContent = strings.spec; - if (toolbarSpecBtn) toolbarSpecBtn.title = strings.specTitle; } if (typeof window !== 'undefined') { diff --git a/pagx/viewer/script/publish.cjs b/pagx/viewer/script/publish.cjs index 069acbdde7..4610299a33 100644 --- a/pagx/viewer/script/publish.cjs +++ b/pagx/viewer/script/publish.cjs @@ -30,7 +30,7 @@ const VIEWER_DIR = path.dirname(SCRIPT_DIR); const PAGX_DIR = path.dirname(VIEWER_DIR); const LIBPAG_DIR = path.dirname(PAGX_DIR); const RESOURCES_FONT_DIR = path.join(LIBPAG_DIR, 'resources', 'font'); -const DEFAULT_OUTPUT_DIR = path.join(PAGX_DIR, 'public', 'viewer'); +const DEFAULT_OUTPUT_DIR = path.join(PAGX_DIR, 'public'); /** * Parse command line arguments. diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 16ae926e70..7aa12d0097 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -1257,6 +1257,62 @@ PAG_TEST(PAGXTest, TextShaperMultipleText) { device->unlock(); } +/** + * Test case: Text shaping with emoji (mixed vector and bitmap fonts) + */ +PAG_TEST(PAGXTest, TextShaperEmoji) { + std::string svgPath = ProjectPath::Absolute("resources/apitest/SVG/emoji.svg"); + + auto doc = pagx::SVGImporter::Parse(svgPath); + ASSERT_TRUE(doc != nullptr); + + int canvasWidth = static_cast(doc->width); + int canvasHeight = static_cast(doc->height); + + auto device = DevicePool::Make(); + ASSERT_TRUE(device != nullptr); + auto context = device->lockContext(); + ASSERT_TRUE(context != nullptr); + + auto typefaces = GetFallbackTypefaces(); + + // Typeset text and embed fonts + pagx::Typesetter typesetter; + typesetter.setFallbackTypefaces(typefaces); + auto textGlyphs = typesetter.createTextGlyphs(doc.get()); + EXPECT_FALSE(textGlyphs.empty()); + pagx::FontEmbedder().embed(doc.get(), textGlyphs); + + // Render typeset document + auto originalLayer = pagx::LayerBuilder::Build(doc.get(), &typesetter); + ASSERT_TRUE(originalLayer != nullptr); + + auto originalSurface = Surface::Make(context, canvasWidth, canvasHeight); + DisplayList originalDL; + originalDL.root()->addChild(originalLayer); + originalDL.render(originalSurface.get(), false); + + // Export and reload + std::string xml = pagx::PAGXExporter::ToXML(*doc); + std::string pagxPath = SavePAGXFile(xml, "PAGXTest/emoji_preshaped.pagx"); + + auto reloadedDoc = pagx::PAGXImporter::FromFile(pagxPath); + ASSERT_TRUE(reloadedDoc != nullptr); + auto reloadedLayer = pagx::LayerBuilder::Build(reloadedDoc.get()); + ASSERT_TRUE(reloadedLayer != nullptr); + + // Render pre-shaped + auto preshapedSurface = Surface::Make(context, canvasWidth, canvasHeight); + DisplayList preshapedDL; + preshapedDL.root()->addChild(reloadedLayer); + preshapedDL.render(preshapedSurface.get(), false); + + EXPECT_TRUE(Baseline::Compare(originalSurface, "PAGXTest/TextShaperEmoji_Original")); + EXPECT_TRUE(Baseline::Compare(preshapedSurface, "PAGXTest/TextShaperEmoji_PreShaped")); + + device->unlock(); +} + /** * Test case: Complete PAGX example from specification document. * This tests the full rendering capabilities of PAGX including: From 86cedd885fef12bce51befe8c5dee992616012bd Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 30 Jan 2026 22:40:22 +0800 Subject: [PATCH 337/678] Update PAGX sample files with modern color scheme and improved visual design. --- pagx/spec/samples/3.2_document_structure.pagx | 22 ++-- .../3.3.3_color_source_coordinates.pagx | 19 +++- pagx/spec/samples/3.3.3_conic_gradient.pagx | 25 +++-- pagx/spec/samples/3.3.3_diamond_gradient.pagx | 22 +++- pagx/spec/samples/3.3.3_image_pattern.pagx | 39 ++++--- pagx/spec/samples/3.3.3_linear_gradient.pagx | 20 +++- pagx/spec/samples/3.3.3_radial_gradient.pagx | 19 +++- pagx/spec/samples/3.3.4_composition.pagx | 29 +++-- pagx/spec/samples/3.3_resources.pagx | 23 ++-- pagx/spec/samples/4.2_layer.pagx | 24 ++-- pagx/spec/samples/4.3_layer_styles.pagx | 23 +++- pagx/spec/samples/4.4_layer_filters.pagx | 22 ++-- pagx/spec/samples/4.5.1_scroll_rect.pagx | 26 +++-- pagx/spec/samples/4.5.2_masking.pagx | 22 ++-- pagx/spec/samples/5.2.1_rectangle.pagx | 53 +++++++++ pagx/spec/samples/5.2.2_ellipse.pagx | 53 +++++++++ pagx/spec/samples/5.2.3_polystar.pagx | 53 +++++++++ pagx/spec/samples/5.2.4_path.pagx | 47 ++++++++ pagx/spec/samples/5.2.5_text.pagx | 33 ++++++ pagx/spec/samples/5.3.1_fill.pagx | 29 +++-- pagx/spec/samples/5.3.2_stroke.pagx | 30 +++-- pagx/spec/samples/5.4.1_trim_path.pagx | 40 ++++--- pagx/spec/samples/5.4.2_round_corner.pagx | 55 +++++++++ pagx/spec/samples/5.4.3_merge_path.pagx | 22 ++-- pagx/spec/samples/5.5.1_text_modifier.pagx | 38 ++++--- pagx/spec/samples/5.5.2_text_to_shape.pagx | 24 ++++ pagx/spec/samples/5.5.5_text_path.pagx | 36 ++++-- pagx/spec/samples/5.5.6_text_layout.pagx | 36 ++++-- pagx/spec/samples/5.5.7_rich_text.pagx | 43 ++++--- pagx/spec/samples/5.6_repeater.pagx | 19 +++- pagx/spec/samples/5.7_group.pagx | 20 +++- pagx/spec/samples/5.7_group_isolation.pagx | 18 ++- pagx/spec/samples/5.7_group_propagation.pagx | 18 ++- pagx/spec/samples/5.7_mixed_overlay.pagx | 20 +++- pagx/spec/samples/5.7_multiple_fills.pagx | 20 +++- pagx/spec/samples/5.7_multiple_painters.pagx | 20 +++- pagx/spec/samples/5.7_multiple_strokes.pagx | 23 +++- pagx/spec/samples/B.1_complete_example.pagx | 106 +++++++++--------- 38 files changed, 898 insertions(+), 293 deletions(-) create mode 100644 pagx/spec/samples/5.2.1_rectangle.pagx create mode 100644 pagx/spec/samples/5.2.2_ellipse.pagx create mode 100644 pagx/spec/samples/5.2.3_polystar.pagx create mode 100644 pagx/spec/samples/5.2.4_path.pagx create mode 100644 pagx/spec/samples/5.2.5_text.pagx create mode 100644 pagx/spec/samples/5.4.2_round_corner.pagx create mode 100644 pagx/spec/samples/5.5.2_text_to_shape.pagx diff --git a/pagx/spec/samples/3.2_document_structure.pagx b/pagx/spec/samples/3.2_document_structure.pagx index a2939c8421..d1c79713d6 100644 --- a/pagx/spec/samples/3.2_document_structure.pagx +++ b/pagx/spec/samples/3.2_document_structure.pagx @@ -1,17 +1,25 @@ - + + - - + + + + + + + + - + - - - + + + + diff --git a/pagx/spec/samples/3.3.3_color_source_coordinates.pagx b/pagx/spec/samples/3.3.3_color_source_coordinates.pagx index fd2ae1937d..3da89d6471 100644 --- a/pagx/spec/samples/3.3.3_color_source_coordinates.pagx +++ b/pagx/spec/samples/3.3.3_color_source_coordinates.pagx @@ -1,15 +1,22 @@ - + + - + + + + + + + - - - - + + + + diff --git a/pagx/spec/samples/3.3.3_conic_gradient.pagx b/pagx/spec/samples/3.3.3_conic_gradient.pagx index 06b37a8525..9957216814 100644 --- a/pagx/spec/samples/3.3.3_conic_gradient.pagx +++ b/pagx/spec/samples/3.3.3_conic_gradient.pagx @@ -1,15 +1,24 @@ - + + + - + + + + + + - - - - - - + + + + + + + + diff --git a/pagx/spec/samples/3.3.3_diamond_gradient.pagx b/pagx/spec/samples/3.3.3_diamond_gradient.pagx index 6f3e09628c..f517a4047a 100644 --- a/pagx/spec/samples/3.3.3_diamond_gradient.pagx +++ b/pagx/spec/samples/3.3.3_diamond_gradient.pagx @@ -1,13 +1,23 @@ - + + + - + + + + + + - - - - + + + + + + + diff --git a/pagx/spec/samples/3.3.3_image_pattern.pagx b/pagx/spec/samples/3.3.3_image_pattern.pagx index 84e1a09091..3811602b42 100644 --- a/pagx/spec/samples/3.3.3_image_pattern.pagx +++ b/pagx/spec/samples/3.3.3_image_pattern.pagx @@ -1,40 +1,47 @@ - - + + + + + + + + + - + - + - + - + - + - + - + - + - + - - - + + + - + - + diff --git a/pagx/spec/samples/3.3.3_linear_gradient.pagx b/pagx/spec/samples/3.3.3_linear_gradient.pagx index cf204376fc..8d086bbab4 100644 --- a/pagx/spec/samples/3.3.3_linear_gradient.pagx +++ b/pagx/spec/samples/3.3.3_linear_gradient.pagx @@ -1,13 +1,21 @@ - + + + - + + + + + + - - - - + + + + + diff --git a/pagx/spec/samples/3.3.3_radial_gradient.pagx b/pagx/spec/samples/3.3.3_radial_gradient.pagx index e148df40eb..9313ab427c 100644 --- a/pagx/spec/samples/3.3.3_radial_gradient.pagx +++ b/pagx/spec/samples/3.3.3_radial_gradient.pagx @@ -1,13 +1,22 @@ - + + + - + + + + + + - + - - + + + + diff --git a/pagx/spec/samples/3.3.4_composition.pagx b/pagx/spec/samples/3.3.4_composition.pagx index 387f808922..2e14814fe9 100644 --- a/pagx/spec/samples/3.3.4_composition.pagx +++ b/pagx/spec/samples/3.3.4_composition.pagx @@ -1,18 +1,31 @@ - - + + + + + + + + + + - + - + - - - + + + - + + + + + + diff --git a/pagx/spec/samples/3.3_resources.pagx b/pagx/spec/samples/3.3_resources.pagx index 6a1cd3efdb..47be9c66b7 100644 --- a/pagx/spec/samples/3.3_resources.pagx +++ b/pagx/spec/samples/3.3_resources.pagx @@ -1,16 +1,23 @@ - + + - - + + + + + + + + - - - - - + + + + + diff --git a/pagx/spec/samples/4.2_layer.pagx b/pagx/spec/samples/4.2_layer.pagx index 535a4d067a..ab9e6871e9 100644 --- a/pagx/spec/samples/4.2_layer.pagx +++ b/pagx/spec/samples/4.2_layer.pagx @@ -1,18 +1,26 @@ - - - + + + + + + + + + - - - + + + - + + - + + diff --git a/pagx/spec/samples/4.3_layer_styles.pagx b/pagx/spec/samples/4.3_layer_styles.pagx index 5d834d4c93..89cf8d89fb 100644 --- a/pagx/spec/samples/4.3_layer_styles.pagx +++ b/pagx/spec/samples/4.3_layer_styles.pagx @@ -1,10 +1,21 @@ - - - - - - + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/4.4_layer_filters.pagx b/pagx/spec/samples/4.4_layer_filters.pagx index 38468df9ec..c1195ff671 100644 --- a/pagx/spec/samples/4.4_layer_filters.pagx +++ b/pagx/spec/samples/4.4_layer_filters.pagx @@ -1,15 +1,21 @@ - - - + + + + + + + + + - - - + + + - - + + diff --git a/pagx/spec/samples/4.5.1_scroll_rect.pagx b/pagx/spec/samples/4.5.1_scroll_rect.pagx index b9f66df947..7d99409d72 100644 --- a/pagx/spec/samples/4.5.1_scroll_rect.pagx +++ b/pagx/spec/samples/4.5.1_scroll_rect.pagx @@ -1,19 +1,25 @@ - - + + - - + + - - - + + + - - - + + + + + + + + + diff --git a/pagx/spec/samples/4.5.2_masking.pagx b/pagx/spec/samples/4.5.2_masking.pagx index 97c05752f1..9e381f197d 100644 --- a/pagx/spec/samples/4.5.2_masking.pagx +++ b/pagx/spec/samples/4.5.2_masking.pagx @@ -1,17 +1,25 @@ - + + + + + + + - + + - + - - - - + + + + + diff --git a/pagx/spec/samples/5.2.1_rectangle.pagx b/pagx/spec/samples/5.2.1_rectangle.pagx new file mode 100644 index 0000000000..32d4ce3f3f --- /dev/null +++ b/pagx/spec/samples/5.2.1_rectangle.pagx @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/5.2.2_ellipse.pagx b/pagx/spec/samples/5.2.2_ellipse.pagx new file mode 100644 index 0000000000..67cee8a5e0 --- /dev/null +++ b/pagx/spec/samples/5.2.2_ellipse.pagx @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/5.2.3_polystar.pagx b/pagx/spec/samples/5.2.3_polystar.pagx new file mode 100644 index 0000000000..487517dae8 --- /dev/null +++ b/pagx/spec/samples/5.2.3_polystar.pagx @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/5.2.4_path.pagx b/pagx/spec/samples/5.2.4_path.pagx new file mode 100644 index 0000000000..7ac1e36fca --- /dev/null +++ b/pagx/spec/samples/5.2.4_path.pagx @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/5.2.5_text.pagx b/pagx/spec/samples/5.2.5_text.pagx new file mode 100644 index 0000000000..138fb4c511 --- /dev/null +++ b/pagx/spec/samples/5.2.5_text.pagx @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/5.3.1_fill.pagx b/pagx/spec/samples/5.3.1_fill.pagx index 439de26d38..11b0cb9651 100644 --- a/pagx/spec/samples/5.3.1_fill.pagx +++ b/pagx/spec/samples/5.3.1_fill.pagx @@ -1,29 +1,38 @@ - + + + + + + - - + + + - + - - - + + + + - + - + - + + + diff --git a/pagx/spec/samples/5.3.2_stroke.pagx b/pagx/spec/samples/5.3.2_stroke.pagx index 960da1ee61..b0fcbf1701 100644 --- a/pagx/spec/samples/5.3.2_stroke.pagx +++ b/pagx/spec/samples/5.3.2_stroke.pagx @@ -1,23 +1,29 @@ - - + + - - + + + + + + + - - + + - + - - - - - + + + + + + diff --git a/pagx/spec/samples/5.4.1_trim_path.pagx b/pagx/spec/samples/5.4.1_trim_path.pagx index 7fc90dd7d6..3f74297e69 100644 --- a/pagx/spec/samples/5.4.1_trim_path.pagx +++ b/pagx/spec/samples/5.4.1_trim_path.pagx @@ -1,25 +1,37 @@ - + + - - - + + + - - - - + + + - - - + + + + + + + + + - - - + + + + + + + + + diff --git a/pagx/spec/samples/5.4.2_round_corner.pagx b/pagx/spec/samples/5.4.2_round_corner.pagx new file mode 100644 index 0000000000..7e17389ba9 --- /dev/null +++ b/pagx/spec/samples/5.4.2_round_corner.pagx @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/5.4.3_merge_path.pagx b/pagx/spec/samples/5.4.3_merge_path.pagx index 787a1fa3b7..665986c951 100644 --- a/pagx/spec/samples/5.4.3_merge_path.pagx +++ b/pagx/spec/samples/5.4.3_merge_path.pagx @@ -1,15 +1,23 @@ - - + + + - - + + + + + + + - - - + + + + + diff --git a/pagx/spec/samples/5.5.1_text_modifier.pagx b/pagx/spec/samples/5.5.1_text_modifier.pagx index 8db993e5f1..3fbae8fe0f 100644 --- a/pagx/spec/samples/5.5.1_text_modifier.pagx +++ b/pagx/spec/samples/5.5.1_text_modifier.pagx @@ -1,25 +1,37 @@ - - - + + + - - + + - + - - + + + - + + + + + + - + - - + + + - + + + + + + diff --git a/pagx/spec/samples/5.5.2_text_to_shape.pagx b/pagx/spec/samples/5.5.2_text_to_shape.pagx new file mode 100644 index 0000000000..a0d5bdf165 --- /dev/null +++ b/pagx/spec/samples/5.5.2_text_to_shape.pagx @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/5.5.5_text_path.pagx b/pagx/spec/samples/5.5.5_text_path.pagx index ac5c86ecd7..829a884678 100644 --- a/pagx/spec/samples/5.5.5_text_path.pagx +++ b/pagx/spec/samples/5.5.5_text_path.pagx @@ -1,15 +1,33 @@ - - - + + + - - + + - + - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/5.5.6_text_layout.pagx b/pagx/spec/samples/5.5.6_text_layout.pagx index 5d42552777..72cb2b0d50 100644 --- a/pagx/spec/samples/5.5.6_text_layout.pagx +++ b/pagx/spec/samples/5.5.6_text_layout.pagx @@ -1,22 +1,38 @@ - + + + + + + - - + + + + + + + + - + - - + + + + - - + + + + - - + + + diff --git a/pagx/spec/samples/5.5.7_rich_text.pagx b/pagx/spec/samples/5.5.7_rich_text.pagx index 404b79e49b..16fa6acd36 100644 --- a/pagx/spec/samples/5.5.7_rich_text.pagx +++ b/pagx/spec/samples/5.5.7_rich_text.pagx @@ -1,27 +1,44 @@ - + + + + + + - - + + + + + + + + - + - - + + + + - - + + + - + - - + + + + - - + + + diff --git a/pagx/spec/samples/5.6_repeater.pagx b/pagx/spec/samples/5.6_repeater.pagx index c5d171f518..23f743c874 100644 --- a/pagx/spec/samples/5.6_repeater.pagx +++ b/pagx/spec/samples/5.6_repeater.pagx @@ -1,11 +1,22 @@ - + + + + + + + - - - + + + + + + + + diff --git a/pagx/spec/samples/5.7_group.pagx b/pagx/spec/samples/5.7_group.pagx index f48a761e24..f1fbe2e21a 100644 --- a/pagx/spec/samples/5.7_group.pagx +++ b/pagx/spec/samples/5.7_group.pagx @@ -1,15 +1,23 @@ - + + - - + + + + + + + - - - + + + + + diff --git a/pagx/spec/samples/5.7_group_isolation.pagx b/pagx/spec/samples/5.7_group_isolation.pagx index 04e8aaf349..52f35ede93 100644 --- a/pagx/spec/samples/5.7_group_isolation.pagx +++ b/pagx/spec/samples/5.7_group_isolation.pagx @@ -1,12 +1,18 @@ - + + - - - + + + + + + + + - - + + diff --git a/pagx/spec/samples/5.7_group_propagation.pagx b/pagx/spec/samples/5.7_group_propagation.pagx index 5e7dd8a243..1fcba6d943 100644 --- a/pagx/spec/samples/5.7_group_propagation.pagx +++ b/pagx/spec/samples/5.7_group_propagation.pagx @@ -1,16 +1,22 @@ - + + + + + + + - - + + - - + + - + diff --git a/pagx/spec/samples/5.7_mixed_overlay.pagx b/pagx/spec/samples/5.7_mixed_overlay.pagx index 73d029c9d3..2f18eb93d1 100644 --- a/pagx/spec/samples/5.7_mixed_overlay.pagx +++ b/pagx/spec/samples/5.7_mixed_overlay.pagx @@ -1,14 +1,24 @@ - + + - + + + + + + - + + - + + - + + diff --git a/pagx/spec/samples/5.7_multiple_fills.pagx b/pagx/spec/samples/5.7_multiple_fills.pagx index 3bedc8e1c2..4c731ae2cf 100644 --- a/pagx/spec/samples/5.7_multiple_fills.pagx +++ b/pagx/spec/samples/5.7_multiple_fills.pagx @@ -1,15 +1,23 @@ - + + - + + + + + + + - - - + + + - + + diff --git a/pagx/spec/samples/5.7_multiple_painters.pagx b/pagx/spec/samples/5.7_multiple_painters.pagx index 2527086d7a..2669f39b9b 100644 --- a/pagx/spec/samples/5.7_multiple_painters.pagx +++ b/pagx/spec/samples/5.7_multiple_painters.pagx @@ -1,9 +1,21 @@ - + + - - - + + + + + + + + + + + + + + diff --git a/pagx/spec/samples/5.7_multiple_strokes.pagx b/pagx/spec/samples/5.7_multiple_strokes.pagx index bbf6db150d..60072e96c8 100644 --- a/pagx/spec/samples/5.7_multiple_strokes.pagx +++ b/pagx/spec/samples/5.7_multiple_strokes.pagx @@ -1,14 +1,25 @@ - + + - - + + + + + + - + - + - + + + + + + + diff --git a/pagx/spec/samples/B.1_complete_example.pagx b/pagx/spec/samples/B.1_complete_example.pagx index 121b16387d..7fe02a3863 100644 --- a/pagx/spec/samples/B.1_complete_example.pagx +++ b/pagx/spec/samples/B.1_complete_example.pagx @@ -9,14 +9,14 @@ - + - + - - + + @@ -28,7 +28,7 @@ - + @@ -36,61 +36,61 @@ - - + + - + - + - - + + - + - - + + - + - + - - + + - + - + - - + + - + @@ -153,7 +153,7 @@ - + @@ -167,7 +167,7 @@ - + @@ -176,8 +176,8 @@ - - + + @@ -187,11 +187,11 @@ - + - + @@ -200,14 +200,14 @@ - + - - + + @@ -226,41 +226,41 @@ - - - + + + - - - - + + + + - - - + + + - - + + - - + + - + - - - + + + - + @@ -270,8 +270,8 @@ - - + + From 54603bd85b5f6d248ba3a9b282c133e6c519fd39 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 30 Jan 2026 23:47:24 +0800 Subject: [PATCH 338/678] Fix PAGX sample geometry calculations for diamond gradient radius and path shape layouts. --- pagx/spec/samples/3.3.3_diamond_gradient.pagx | 6 ++--- pagx/spec/samples/3.3.3_image_pattern.pagx | 6 +++-- pagx/spec/samples/5.2.4_path.pagx | 24 +++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/pagx/spec/samples/3.3.3_diamond_gradient.pagx b/pagx/spec/samples/3.3.3_diamond_gradient.pagx index f517a4047a..61b6ad32e6 100644 --- a/pagx/spec/samples/3.3.3_diamond_gradient.pagx +++ b/pagx/spec/samples/3.3.3_diamond_gradient.pagx @@ -6,12 +6,12 @@ - + + - - + diff --git a/pagx/spec/samples/3.3.3_image_pattern.pagx b/pagx/spec/samples/3.3.3_image_pattern.pagx index 3811602b42..6391dd65c6 100644 --- a/pagx/spec/samples/3.3.3_image_pattern.pagx +++ b/pagx/spec/samples/3.3.3_image_pattern.pagx @@ -8,11 +8,13 @@ - + + + - + diff --git a/pagx/spec/samples/5.2.4_path.pagx b/pagx/spec/samples/5.2.4_path.pagx index 7ac1e36fca..199a8439ad 100644 --- a/pagx/spec/samples/5.2.4_path.pagx +++ b/pagx/spec/samples/5.2.4_path.pagx @@ -7,40 +7,40 @@ - + - + - + - + - + - + - + - + - + - + - + - + From 18744d6fb3669f23c417674355d1c33f66343893 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 30 Jan 2026 23:56:58 +0800 Subject: [PATCH 339/678] Update tgfx --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index f96fa52f81..386cc4b438 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "dfe3e2f26cfcbbdee43c9a9d6ef5fb11e5a6d16b", + "commit": "503a5f559321aafc9ccdc94249967a07827dbcd5", "dir": "third_party/tgfx" }, { From e48647a3edb7a2055cca6c6f02eb8c125fed9a38 Mon Sep 17 00:00:00 2001 From: Dom Date: Sat, 31 Jan 2026 00:00:29 +0800 Subject: [PATCH 340/678] Add unknown node error reporting and fix sample files to comply with PAGX spec. --- pagx/spec/samples/3.3_resources.pagx | 8 +- pagx/spec/samples/5.5.2_text_to_shape.pagx | 24 -- pagx/spec/samples/5.5.5_text_path.pagx | 22 +- pagx/spec/samples/5.5.7_rich_text.pagx | 42 ++-- pagx/spec/samples/5.7_group.pagx | 2 +- pagx/spec/samples/B.1_complete_example.pagx | 246 ++++++++++---------- pagx/src/PAGXImporter.cpp | 18 +- 7 files changed, 173 insertions(+), 189 deletions(-) delete mode 100644 pagx/spec/samples/5.5.2_text_to_shape.pagx diff --git a/pagx/spec/samples/3.3_resources.pagx b/pagx/spec/samples/3.3_resources.pagx index 47be9c66b7..f8181d026c 100644 --- a/pagx/spec/samples/3.3_resources.pagx +++ b/pagx/spec/samples/3.3_resources.pagx @@ -6,14 +6,18 @@ - + + + + + + - diff --git a/pagx/spec/samples/5.5.2_text_to_shape.pagx b/pagx/spec/samples/5.5.2_text_to_shape.pagx deleted file mode 100644 index a0d5bdf165..0000000000 --- a/pagx/spec/samples/5.5.2_text_to_shape.pagx +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pagx/spec/samples/5.5.5_text_path.pagx b/pagx/spec/samples/5.5.5_text_path.pagx index 829a884678..3636ce92b5 100644 --- a/pagx/spec/samples/5.5.5_text_path.pagx +++ b/pagx/spec/samples/5.5.5_text_path.pagx @@ -1,33 +1,27 @@ - + - + - - + + - + - + - - + + - - - - - - diff --git a/pagx/spec/samples/5.5.7_rich_text.pagx b/pagx/spec/samples/5.5.7_rich_text.pagx index 16fa6acd36..6a132ec774 100644 --- a/pagx/spec/samples/5.5.7_rich_text.pagx +++ b/pagx/spec/samples/5.5.7_rich_text.pagx @@ -1,5 +1,5 @@ - + @@ -17,28 +17,28 @@ - + - - - + + + + + + + + + - + - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/pagx/spec/samples/5.7_group.pagx b/pagx/spec/samples/5.7_group.pagx index f1fbe2e21a..bc665d6b56 100644 --- a/pagx/spec/samples/5.7_group.pagx +++ b/pagx/spec/samples/5.7_group.pagx @@ -17,7 +17,7 @@ - + diff --git a/pagx/spec/samples/B.1_complete_example.pagx b/pagx/spec/samples/B.1_complete_example.pagx index 7fe02a3863..0bc3081d3c 100644 --- a/pagx/spec/samples/B.1_complete_example.pagx +++ b/pagx/spec/samples/B.1_complete_example.pagx @@ -32,66 +32,64 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -125,7 +123,7 @@ - + @@ -141,74 +139,70 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 6e4906efec..3fdbf69c4f 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -416,7 +416,12 @@ static void parseResources(const XMLNode* node, PAGXDocument* doc) { continue; } // Try to parse as a color source (which is also a Node) - parseColorSource(child.get(), doc); + auto colorSource = parseColorSource(child.get(), doc); + if (colorSource) { + continue; + } + // Unknown resource type - report error. + tgfx::PrintError("PAGXImporter: Unknown element '%s' in Resources.\n", child->tag.c_str()); } } @@ -524,7 +529,10 @@ static Layer* parseLayer(const XMLNode* node, PAGXDocument* doc) { auto filter = parseLayerFilter(child.get(), doc); if (filter) { layer->filters.push_back(filter); + continue; } + // Unknown node type - report error. + tgfx::PrintError("PAGXImporter: Unknown element '%s' in Layer.\n", child->tag.c_str()); } return layer; @@ -535,6 +543,8 @@ static void parseContents(const XMLNode* node, Layer* layer, PAGXDocument* doc) auto element = parseElement(child.get(), doc); if (element) { layer->contents.push_back(element); + } else { + tgfx::PrintError("PAGXImporter: Unknown element '%s' in contents.\n", child->tag.c_str()); } } } @@ -544,6 +554,8 @@ static void parseStyles(const XMLNode* node, Layer* layer, PAGXDocument* doc) { auto style = parseLayerStyle(child.get(), doc); if (style) { layer->styles.push_back(style); + } else { + tgfx::PrintError("PAGXImporter: Unknown element '%s' in styles.\n", child->tag.c_str()); } } } @@ -553,6 +565,8 @@ static void parseFilters(const XMLNode* node, Layer* layer, PAGXDocument* doc) { auto filter = parseLayerFilter(child.get(), doc); if (filter) { layer->filters.push_back(filter); + } else { + tgfx::PrintError("PAGXImporter: Unknown element '%s' in filters.\n", child->tag.c_str()); } } } @@ -1003,6 +1017,8 @@ static Group* parseGroup(const XMLNode* node, PAGXDocument* doc) { auto element = parseElement(child.get(), doc); if (element) { group->elements.push_back(element); + } else { + tgfx::PrintError("PAGXImporter: Unknown element '%s' in Group.\n", child->tag.c_str()); } } From 966a51d7790574a44af453eea5aee68a84cc0ce6 Mon Sep 17 00:00:00 2001 From: Dom Date: Sat, 31 Jan 2026 00:02:32 +0800 Subject: [PATCH 341/678] Revert 5.5.5_text_path and 5.5.7_rich_text to working versions. --- pagx/spec/samples/5.5.5_text_path.pagx | 22 +++++++++----- pagx/spec/samples/5.5.7_rich_text.pagx | 42 +++++++++++++------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/pagx/spec/samples/5.5.5_text_path.pagx b/pagx/spec/samples/5.5.5_text_path.pagx index 3636ce92b5..829a884678 100644 --- a/pagx/spec/samples/5.5.5_text_path.pagx +++ b/pagx/spec/samples/5.5.5_text_path.pagx @@ -1,27 +1,33 @@ - + - + - - + + - + - + - - + + + + + + + + diff --git a/pagx/spec/samples/5.5.7_rich_text.pagx b/pagx/spec/samples/5.5.7_rich_text.pagx index 6a132ec774..16fa6acd36 100644 --- a/pagx/spec/samples/5.5.7_rich_text.pagx +++ b/pagx/spec/samples/5.5.7_rich_text.pagx @@ -1,5 +1,5 @@ - + @@ -17,28 +17,28 @@ - + - - - - - - - - - + + + - + - - - - - - - - - + + + + + + + + + + + + + + + From 8e82e6622ed54267fa6ad5d1f62a8be0dbf6f4e3 Mon Sep 17 00:00:00 2001 From: Dom Date: Sat, 31 Jan 2026 21:09:00 +0800 Subject: [PATCH 342/678] Simplify node category table in Appendix A from 12 to 5 categories. --- pagx/spec/pagx_spec.md | 16 ++++++---------- pagx/spec/pagx_spec.zh_CN.md | 16 ++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 1127b6a1f8..c31af34df2 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -2134,18 +2134,14 @@ This appendix describes node categorization and nesting rules. | Category | Nodes | |----------|-------| -| **Document Root** | `pagx` | -| **Resources** | `Resources`, `Image`, `PathData`, `Font`, `Glyph`, `SolidColor`, `LinearGradient`, `RadialGradient`, `ConicGradient`, `DiamondGradient`, `ColorStop`, `ImagePattern`, `Composition` | -| **Layer** | `Layer` | +| **Containers** | `pagx`, `Resources`, `Layer`, `Group` | +| **Resources** | `Image`, `PathData`, `Composition`, `Font`, `Glyph` | +| **Color Sources** | `SolidColor`, `LinearGradient`, `RadialGradient`, `ConicGradient`, `DiamondGradient`, `ImagePattern`, `ColorStop` | | **Layer Styles** | `DropShadowStyle`, `InnerShadowStyle`, `BackgroundBlurStyle` | -| **Filters** | `BlurFilter`, `DropShadowFilter`, `InnerShadowFilter`, `BlendFilter`, `ColorMatrixFilter` | -| **Geometry Elements** | `Rectangle`, `Ellipse`, `Polystar`, `Path`, `Text` | -| **Pre-layout Data** | `GlyphRun` (Text child element) | +| **Layer Filters** | `BlurFilter`, `DropShadowFilter`, `InnerShadowFilter`, `BlendFilter`, `ColorMatrixFilter` | +| **Geometry Elements** | `Rectangle`, `Ellipse`, `Polystar`, `Path`, `Text`, `GlyphRun` | +| **Modifiers** | `TrimPath`, `RoundCorner`, `MergePath`, `TextModifier`, `RangeSelector`, `TextPath`, `TextLayout`, `Repeater` | | **Painters** | `Fill`, `Stroke` | -| **Shape Modifiers** | `TrimPath`, `RoundCorner`, `MergePath` | -| **Text Modifiers** | `TextModifier`, `TextPath`, `TextLayout` | -| **Text Selectors** | `RangeSelector` (TextModifier child element) | -| **Other** | `Repeater`, `Group` | ### A.2 Document Containment diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index e9a5751af0..48a75bf7b3 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -2132,18 +2132,14 @@ Group 创建独立的作用域,用于隔离几何累积和渲染: | 分类 | 节点 | |------|------| -| **文档根** | `pagx` | -| **资源** | `Resources`, `Image`, `PathData`, `Font`, `Glyph`, `SolidColor`, `LinearGradient`, `RadialGradient`, `ConicGradient`, `DiamondGradient`, `ColorStop`, `ImagePattern`, `Composition` | -| **图层** | `Layer` | +| **容器** | `pagx`, `Resources`, `Layer`, `Group` | +| **资源** | `Image`, `PathData`, `Composition`, `Font`, `Glyph` | +| **颜色源** | `SolidColor`, `LinearGradient`, `RadialGradient`, `ConicGradient`, `DiamondGradient`, `ImagePattern`, `ColorStop` | | **图层样式** | `DropShadowStyle`, `InnerShadowStyle`, `BackgroundBlurStyle` | -| **滤镜** | `BlurFilter`, `DropShadowFilter`, `InnerShadowFilter`, `BlendFilter`, `ColorMatrixFilter` | -| **几何元素** | `Rectangle`, `Ellipse`, `Polystar`, `Path`, `Text` | -| **预排版数据** | `GlyphRun`(Text 子元素) | +| **图层滤镜** | `BlurFilter`, `DropShadowFilter`, `InnerShadowFilter`, `BlendFilter`, `ColorMatrixFilter` | +| **几何元素** | `Rectangle`, `Ellipse`, `Polystar`, `Path`, `Text`, `GlyphRun` | +| **修改器** | `TrimPath`, `RoundCorner`, `MergePath`, `TextModifier`, `RangeSelector`, `TextPath`, `TextLayout`, `Repeater` | | **绘制器** | `Fill`, `Stroke` | -| **形状修改器** | `TrimPath`, `RoundCorner`, `MergePath` | -| **文本修改器** | `TextModifier`, `TextPath`, `TextLayout` | -| **文本选择器** | `RangeSelector`(TextModifier 子元素) | -| **其他** | `Repeater`, `Group` | ### A.2 文档包含关系 From 162e0848c8832993f31f53881a2d6d6b45f452b5 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Tue, 3 Feb 2026 20:26:29 +0800 Subject: [PATCH 343/678] Optimize zoom gesture handling with accumulation-based direction detection. --- pagx/wechat/src/PAGXView.cpp | 82 +++++++++++++++------- pagx/wechat/src/PAGXView.h | 23 ++++--- pagx/wechat/src/binding.cpp | 4 +- pagx/wechat/ts/pagx-view.ts | 130 +++++++++++++++++++++++++++-------- pagx/wechat/ts/types.ts | 2 - 5 files changed, 172 insertions(+), 69 deletions(-) diff --git a/pagx/wechat/src/PAGXView.cpp b/pagx/wechat/src/PAGXView.cpp index 9043f50650..4cfaca0bfb 100644 --- a/pagx/wechat/src/PAGXView.cpp +++ b/pagx/wechat/src/PAGXView.cpp @@ -69,8 +69,6 @@ PAGXView::PAGXView(std::shared_ptr device, int width, int height) displayList.setMaxTilesRefinedPerFrame(currentMaxTilesRefinedPerFrame); } -static std::vector> fallbackTypefaces; - void PAGXView::loadPAGX(const val& pagxData) { auto data = GetDataFromEmscripten(pagxData); if (!data) { @@ -128,9 +126,24 @@ void PAGXView::applyCenteringTransform() { void PAGXView::updateZoomScaleAndOffset(float zoom, float offsetX, float offsetY) { bool zoomChanged = (std::abs(zoom - lastZoom) > 0.001f); - if (zoomChanged && !isZooming) { - isZooming = true; - updateAdaptiveTileRefinement(); + if (zoomChanged) { + if (!isZooming) { + isZooming = true; + zoomStartValue = lastZoom; + accumulatedZoomChange = 0.0f; + updateAdaptiveTileRefinement(); + } + + // Accumulate zoom change to determine overall direction + float currentChange = zoom - lastZoom; + accumulatedZoomChange += currentChange; + + // Determine direction based on accumulated change (ignoring noise < 0.01) + if (std::abs(accumulatedZoomChange) > 0.01f) { + isZoomingIn = (accumulatedZoomChange > 0.0f); + } + + lastZoomUpdateTimestampMs = emscripten_get_now(); } displayList.setZoomScale(zoom); @@ -144,7 +157,15 @@ void PAGXView::onZoomEnd() { } isZooming = false; - updateAdaptiveTileRefinement(); + + if (!enablePerformanceAdaptation) { + return; + } + + currentMaxTilesRefinedPerFrame = 1; + displayList.setMaxTilesRefinedPerFrame(currentMaxTilesRefinedPerFrame); + + tryUpgradeTimestampMs = emscripten_get_now() + 200.0; } bool PAGXView::draw() { @@ -168,7 +189,6 @@ bool PAGXView::draw() { return false; } } - double frameStartMs = emscripten_get_now(); auto canvas = surface->getCanvas(); @@ -189,8 +209,30 @@ bool PAGXView::draw() { updatePerformanceState(frameDurationMs); - if (!isZooming) { - updateAdaptiveTileRefinement(); + if (isZooming && lastZoomUpdateTimestampMs > 0.0) { + double currentTimeoutMs = isZoomingIn ? zoomInEndTimeoutMs : zoomOutEndTimeoutMs; + double timeSinceLastUpdate = frameStartMs - lastZoomUpdateTimestampMs; + + if (timeSinceLastUpdate >= currentTimeoutMs) { + onZoomEnd(); + } + } + + if (!isZooming && enablePerformanceAdaptation) { + if (tryUpgradeTimestampMs > 0.0) { + if (frameStartMs >= tryUpgradeTimestampMs) { + if (!lastFrameSlow) { + int targetCount = calculateTargetTileRefinement(lastZoom); + currentMaxTilesRefinedPerFrame = targetCount; + displayList.setMaxTilesRefinedPerFrame(targetCount); + tryUpgradeTimestampMs = 0.0; + } else { + tryUpgradeTimestampMs = frameStartMs + upgradeRetryDelayMs; + } + } + } else { + updateAdaptiveTileRefinement(); + } } return true; @@ -230,13 +272,15 @@ void PAGXView::updatePerformanceState(double frameDurationMs) { if (lastFrameSlow && !frameHistory.empty()) { double avgTime = frameHistoryTotalTime / static_cast(frameHistory.size()); - if (avgTime <= slowFrameThresholdMs) { + size_t minFrames = isZooming ? minRecoveryFramesZoomEnd : minRecoveryFramesStatic; + if (avgTime <= slowFrameThresholdMs && frameHistory.size() >= minFrames) { lastFrameSlow = false; } } } int PAGXView::calculateTargetTileRefinement(float zoom) const { + // Performance-first strategy: disable tile refinement during zooming to reduce stutter if (isZooming) { return 0; } @@ -248,9 +292,9 @@ int PAGXView::calculateTargetTileRefinement(float zoom) const { if (zoom < 1.0f) { int count = static_cast(zoom / 0.33f) + 1; return std::clamp(count, 1, 3); - } else { - return 3; } + + return 3; } void PAGXView::updateAdaptiveTileRefinement() { @@ -269,8 +313,9 @@ void PAGXView::updateAdaptiveTileRefinement() { void PAGXView::setPerformanceAdaptationEnabled(bool enabled) { enablePerformanceAdaptation = enabled; if (!enabled) { - displayList.setMaxTilesRefinedPerFrame(3); currentMaxTilesRefinedPerFrame = 3; + displayList.setMaxTilesRefinedPerFrame(currentMaxTilesRefinedPerFrame); + tryUpgradeTimestampMs = 0.0; } } @@ -282,15 +327,4 @@ void PAGXView::setRecoveryWindow(double windowMs) { recoveryWindowMs = windowMs; } -bool PAGXView::isLastFrameSlow() const { - return lastFrameSlow; -} - -double PAGXView::getAverageFrameTime() const { - if (frameHistory.empty()) { - return 0.0; - } - return frameHistoryTotalTime / static_cast(frameHistory.size()); -} - } // namespace pagx diff --git a/pagx/wechat/src/PAGXView.h b/pagx/wechat/src/PAGXView.h index 96c317a86b..ca21cfd6d9 100644 --- a/pagx/wechat/src/PAGXView.h +++ b/pagx/wechat/src/PAGXView.h @@ -57,6 +57,7 @@ class PAGXView { /** * Notifies that zoom gesture has ended. This will restore tile refinement. + * Note: This is called automatically by internal detection, no need to call manually. */ void onZoomEnd(); @@ -98,16 +99,6 @@ class PAGXView { */ void setRecoveryWindow(double windowMs); - /** - * Check if last frame was slow. - */ - bool isLastFrameSlow() const; - - /** - * Get average frame time in the recovery window. - */ - double getAverageFrameTime() const; - private: void applyCenteringTransform(); @@ -148,11 +139,21 @@ class PAGXView { bool enablePerformanceAdaptation = true; double slowFrameThresholdMs = 50.0; double recoveryWindowMs = 3000.0; + double zoomInEndTimeoutMs = 300.0; // Timeout for zoom in (faster refinement) + double zoomOutEndTimeoutMs = 800.0; // Timeout for zoom out (slower refinement) + double upgradeRetryDelayMs = 300.0; // Delay before retrying upgrade when performance is still slow + size_t minRecoveryFramesStatic = 20; // Minimum frames to confirm recovery in static state + size_t minRecoveryFramesZoomEnd = 10; // Minimum frames to confirm recovery after zoom ends // State tracking float lastZoom = 1.0f; + float zoomStartValue = 1.0f; // Zoom value at gesture start + float accumulatedZoomChange = 0.0f; // Accumulated zoom change during gesture bool isZooming = false; - int currentMaxTilesRefinedPerFrame = 3; + bool isZoomingIn = false; + int currentMaxTilesRefinedPerFrame = 1; + double tryUpgradeTimestampMs = 0.0; + double lastZoomUpdateTimestampMs = 0.0; }; } // namespace pagx diff --git a/pagx/wechat/src/binding.cpp b/pagx/wechat/src/binding.cpp index 1a7b08325b..0ce8930c37 100644 --- a/pagx/wechat/src/binding.cpp +++ b/pagx/wechat/src/binding.cpp @@ -39,7 +39,5 @@ EMSCRIPTEN_BINDINGS(PAGXView) { .function("contentHeight", &PAGXView::contentHeight) .function("setPerformanceAdaptationEnabled", &PAGXView::setPerformanceAdaptationEnabled) .function("setSlowFrameThreshold", &PAGXView::setSlowFrameThreshold) - .function("setRecoveryWindow", &PAGXView::setRecoveryWindow) - .function("isLastFrameSlow", &PAGXView::isLastFrameSlow) - .function("getAverageFrameTime", &PAGXView::getAverageFrameTime); + .function("setRecoveryWindow", &PAGXView::setRecoveryWindow); } diff --git a/pagx/wechat/ts/pagx-view.ts b/pagx/wechat/ts/pagx-view.ts index 956e21f30c..063cd21163 100644 --- a/pagx/wechat/ts/pagx-view.ts +++ b/pagx/wechat/ts/pagx-view.ts @@ -25,14 +25,20 @@ declare const wx: wx; export interface PAGXViewOptions { /** - * Use style to scale canvas. default false. - * When target canvas is offscreen canvas, useScale is false. + * Auto start rendering loop. default false. + * When true, the view will automatically start rendering after initialization. */ - useScale?: boolean; + autoRender?: boolean; /** - * Render first frame when view init. default true. + * Custom render callback. Called before each frame is drawn. + * Can be used for performance monitoring or custom rendering logic. */ - firstFrame?: boolean; + onBeforeRender?: () => void; + /** + * Custom render callback. Called after each frame is drawn. + * Can be used for performance monitoring or custom rendering logic. + */ + onAfterRender?: () => void; } /** @@ -54,9 +60,6 @@ export class View { const view = new View(module, canvas); view.pagViewOptions = { ...view.pagViewOptions, ...options }; - // Reset canvas size if needed - view.resetSize(view.pagViewOptions.useScale); - // Create RenderCanvas view.renderCanvas = RenderCanvas.from(module, canvas); view.renderCanvas.retain(); @@ -80,6 +83,10 @@ export class View { throw new Error('Failed to create PAGXViewWechat'); } + if (view.pagViewOptions.autoRender) { + view.startRendering(); + } + return view; } @@ -89,10 +96,11 @@ export class View { private backendContext: BackendContext | null = null; private canvas: WxCanvas | null = null; private pagViewOptions: PAGXViewOptions = { - useScale: false, - firstFrame: true, + autoRender: false, }; private isDestroyed = false; + private isRendering = false; + private animationFrameId: number = 0; private constructor(module: PAGX, canvas: WxCanvas) { this.module = module; @@ -150,6 +158,9 @@ export class View { /** * Notify that zoom gesture has ended (for adaptive tile refinement). + * Note: This is now handled automatically by PAGXView internal detection. + * No need to call manually unless you want to force trigger. + * @deprecated Automatic detection is enabled, manual call is optional. */ public onZoomEnd(): void { this.checkDestroyed(); @@ -157,34 +168,65 @@ export class View { } /** - * Draw current frame. + * Get content width. */ - public draw(): void { + public contentWidth(): number { this.checkDestroyed(); + return this.nativeView!.contentWidth(); + } - if (!this.backendContext) { + /** + * Get content height. + */ + public contentHeight(): number { + this.checkDestroyed(); + return this.nativeView!.contentHeight(); + } + + /** + * Start the rendering loop. + * This will continuously call draw() using requestAnimationFrame. + */ + public startRendering(): void { + this.checkDestroyed(); + if (this.isRendering) { return; } + this.isRendering = true; + this.renderLoop(); + } - this.backendContext.makeCurrent(this.module); - this.nativeView!.draw(); - this.backendContext.clearCurrent(this.module); + /** + * Stop the rendering loop. + */ + public stopRendering(): void { + if (!this.isRendering) { + return; + } + this.isRendering = false; + if (this.animationFrameId) { + if (this.canvas && (this.canvas as any).cancelAnimationFrame) { + (this.canvas as any).cancelAnimationFrame(this.animationFrameId); + } else { + clearTimeout(this.animationFrameId); + } + this.animationFrameId = 0; + } } /** - * Get content width. + * Check if the view is currently rendering. */ - public contentWidth(): number { - this.checkDestroyed(); - return this.nativeView!.contentWidth(); + public isCurrentlyRendering(): boolean { + return this.isRendering; } /** - * Get content height. + * Set render callback functions for performance monitoring. */ - public contentHeight(): number { - this.checkDestroyed(); - return this.nativeView!.contentHeight(); + public setRenderCallbacks(onBeforeRender?: () => void, onAfterRender?: () => void): void { + this.pagViewOptions.onBeforeRender = onBeforeRender; + this.pagViewOptions.onAfterRender = onAfterRender; } /** @@ -195,6 +237,8 @@ export class View { return; } + this.stopRendering(); + if (this.nativeView) { if (this.backendContext) { this.backendContext.makeCurrent(this.module); @@ -216,15 +260,11 @@ export class View { this.isDestroyed = true; } - private resetSize(useScale = false): void { + private resetSize(): void { if (!this.canvas) { throw new Error('Canvas element is not found!'); } - if (!useScale) { - return; - } - // Calculate display size for WeChat MiniProgram const displayWidth = (this.canvas as any).displayWidth || this.canvas.width; const displayHeight = (this.canvas as any).displayHeight || this.canvas.height; @@ -239,4 +279,36 @@ export class View { throw new Error('PAGXView has been destroyed'); } } + + private renderLoop(): void { + if (!this.isRendering || this.isDestroyed) { + return; + } + + if (!this.backendContext) { + return; + } + + if (this.pagViewOptions.onBeforeRender) { + this.pagViewOptions.onBeforeRender(); + } + + this.backendContext.makeCurrent(this.module); + this.nativeView!.draw(); + this.backendContext.clearCurrent(this.module); + + if (this.pagViewOptions.onAfterRender) { + this.pagViewOptions.onAfterRender(); + } + + if (this.canvas && (this.canvas as any).requestAnimationFrame) { + this.animationFrameId = (this.canvas as any).requestAnimationFrame(() => { + this.renderLoop(); + }); + } else { + this.animationFrameId = setTimeout(() => { + this.renderLoop(); + }, 16) as any; + } + } } diff --git a/pagx/wechat/ts/types.ts b/pagx/wechat/ts/types.ts index af42199c58..c3fb29fb86 100644 --- a/pagx/wechat/ts/types.ts +++ b/pagx/wechat/ts/types.ts @@ -13,8 +13,6 @@ export interface PAGXViewNative { setPerformanceAdaptationEnabled: (enabled: boolean) => void; setSlowFrameThreshold: (thresholdMs: number) => void; setRecoveryWindow: (windowMs: number) => void; - isLastFrameSlow: () => boolean; - getAverageFrameTime: () => number; delete: () => void; } From 3a11c633d92252acec2642417513a999ce76b6cb Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Tue, 3 Feb 2026 20:27:18 +0800 Subject: [PATCH 344/678] Configure TypeScript declaration generation and remove manual zoom event bindings. --- pagx/wechat/.gitignore | 1 + pagx/wechat/package.json | 2 +- pagx/wechat/tsconfig.type.json | 17 ++--- pagx/wechat/wx_demo/pages/viewer/viewer.js | 89 +++++----------------- 4 files changed, 30 insertions(+), 79 deletions(-) diff --git a/pagx/wechat/.gitignore b/pagx/wechat/.gitignore index 4807e5b198..974aef2484 100644 --- a/pagx/wechat/.gitignore +++ b/pagx/wechat/.gitignore @@ -10,3 +10,4 @@ script/.build.lock .*.md5 package-lock.json ts/wasm +types/ \ No newline at end of file diff --git a/pagx/wechat/package.json b/pagx/wechat/package.json index 3d967d1661..da5fadac3d 100644 --- a/pagx/wechat/package.json +++ b/pagx/wechat/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "clean": "rimraf --glob .pagx-viewer.wasm.md5", - "build:wechat:js": "rollup -c ./script/rollup.wx.js && node script/copy-files.js", + "build:wechat:js": "rollup -c ./script/rollup.wx.js && node script/copy-files.js && tsc -p ./tsconfig.type.json ", "build:wechat": "npm run clean && node script/cmake.wx.js -a wasm && brotli -f ./ts/wasm/pagx-viewer.wasm && npm run build:wechat:js", "server": "node server.js" }, diff --git a/pagx/wechat/tsconfig.type.json b/pagx/wechat/tsconfig.type.json index 7f170191c5..747a142c30 100644 --- a/pagx/wechat/tsconfig.type.json +++ b/pagx/wechat/tsconfig.type.json @@ -2,21 +2,20 @@ "compilerOptions": { "lib": ["ES5", "ES6", "DOM", "DOM.Iterable"], "target": "ES2020", - "module": "ES2020", - "allowJs": true, - "sourceMap": true, - "esModuleInterop": true, - "moduleResolution": "Node", + "declaration": true, + "declarationDir": "./types", + "emitDeclarationOnly": true, + "removeComments": false, + "skipLibCheck": true, "experimentalDecorators": true, - "strict": true, - "outDir": "./wasm-mt", + "moduleResolution": "Node", "baseUrl": ".", "resolveJsonModule": true, "paths": { "@tgfx/*": [ "../../third_party/tgfx/web/src/*" - ], + ] } }, - "include": ["*.ts", "ts/**/*.ts"] + "include": ["ts/pagx.ts"] } diff --git a/pagx/wechat/wx_demo/pages/viewer/viewer.js b/pagx/wechat/wx_demo/pages/viewer/viewer.js index bb1f295f25..f053dda973 100644 --- a/pagx/wechat/wx_demo/pages/viewer/viewer.js +++ b/pagx/wechat/wx_demo/pages/viewer/viewer.js @@ -53,7 +53,6 @@ Page({ View: null, module: null, canvas: null, - animationFrameId: 0, gestureManager: null, perfMonitor: null, dpr: 2, @@ -67,10 +66,6 @@ Page({ // Create gesture manager this.gestureManager = new WXGestureManager(); - // Bind zoom event listeners - this.gestureManager.on('zoomStart', this.onZoomStart.bind(this)); - this.gestureManager.on('zoomEnd', this.onZoomEnd.bind(this)); - // Create performance monitor this.perfMonitor = new PerformanceMonitor(); this.perfMonitor.onStatsUpdate = (stats) => { @@ -104,7 +99,20 @@ Page({ // Apply initial state to ensure C++ side is synchronized this.applyGestureState(initState); - this.startRendering(); + // Setup performance monitoring callbacks + this.View.setRenderCallbacks(null, () => { + if (this.perfMonitor && this.perfMonitor.enabled) { + this.perfMonitor.recordFrame(); + + // Mark first frame after gesture start + if (this.gestureJustStarted) { + this.perfMonitor.onGestureFirstFrame(); + this.gestureJustStarted = false; + } + } + }); + + this.View.startRendering(); this.setData({ loading: false }); } catch (error) { console.error('Initialization failed:', error); @@ -117,8 +125,6 @@ Page({ }, onUnload() { - this.stopRendering(); - if (this.gestureManager) { this.gestureManager.destroy(); this.gestureManager = null; @@ -163,14 +169,14 @@ Page({ // Set canvas physical pixel size based on display size and device pixel ratio // This ensures sharp rendering on high-DPI displays - const dpr = wx.getSystemInfoSync().pixelRatio || 2; - this.canvas.width = Math.floor(rect.width * dpr); - this.canvas.height = Math.floor(rect.height * dpr); + this.canvas.width = Math.floor(rect.width * this.dpr); + this.canvas.height = Math.floor(rect.height * this.dpr); // Create View this.View = await this.module.View.init(this.module, this.canvas, { useScale: false, - firstFrame: false + firstFrame: false, + autoRender: false }); if (!this.View) { @@ -291,49 +297,10 @@ Page({ } // Always update C++ side immediately for smooth rendering - try { + try {// 0,0 1,1 this.View.updateZoomScaleAndOffset(state.zoom, state.offsetX, state.offsetY); } catch (error) { - return; - } - }, - - startRendering() { - const render = () => { - if (!this.View) return; - - this.View.draw(); - - // Record frame for performance monitoring - if (this.perfMonitor && this.perfMonitor.enabled) { - this.perfMonitor.recordFrame(); - - // Mark first frame after gesture start - if (this.gestureJustStarted) { - this.perfMonitor.onGestureFirstFrame(); - this.gestureJustStarted = false; - } - } - - // Use Canvas.requestAnimationFrame in WeChat Miniprogram - if (this.canvas && this.canvas.requestAnimationFrame) { - this.animationFrameId = this.canvas.requestAnimationFrame(render); - } else { - // Fallback to setTimeout if canvas not ready - this.animationFrameId = setTimeout(render, 16); - } - }; - render(); - }, - - stopRendering() { - if (this.animationFrameId) { - if (this.canvas && this.canvas.cancelAnimationFrame) { - this.canvas.cancelAnimationFrame(this.animationFrameId); - } else { - clearTimeout(this.animationFrameId); - } - this.animationFrameId = 0; + // Silently ignore errors } }, @@ -372,22 +339,6 @@ Page({ } }, - // Zoom event handlers - onZoomStart(state) { - this.applyGestureState(state); - }, - - onZoomEnd(state) { - if (!this.View) return; - - try { - // Notify C++ that zoom gesture ended - this.View.onZoomEnd(); - } catch (error) { - // Silently ignore errors - } - }, - // Reset Button onReset() { // Prevent multiple rapid clicks From 02a4da30756724bcc2477a061e42b721ac057b51 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 18:14:13 +0800 Subject: [PATCH 345/678] Add advance attribute to embedded font glyphs and support Default positioning mode for GlyphRun. --- pagx/include/pagx/nodes/Font.h | 6 ++++++ pagx/spec/pagx_spec.md | 12 +++++++----- pagx/spec/pagx_spec.zh_CN.md | 14 ++++++++------ pagx/src/PAGXExporter.cpp | 1 + pagx/src/PAGXImporter.cpp | 1 + pagx/src/tgfx/FontEmbedder.cpp | 8 ++++++++ pagx/src/tgfx/Typesetter.cpp | 11 ++++++++--- test/src/PAGXTest.cpp | 4 ++-- 8 files changed, 41 insertions(+), 16 deletions(-) diff --git a/pagx/include/pagx/nodes/Font.h b/pagx/include/pagx/nodes/Font.h index e35034435a..c6ea85cc8a 100644 --- a/pagx/include/pagx/nodes/Font.h +++ b/pagx/include/pagx/nodes/Font.h @@ -47,6 +47,12 @@ class Glyph : public Node { */ Point offset = {}; + /** + * Horizontal advance width in design space (unitsPerEm coordinates). This value determines the + * spacing to the next glyph when using Default positioning mode. + */ + float advance = 0.0f; + NodeType nodeType() const override { return NodeType::Glyph; } diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index c31af34df2..dbe921af16 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -576,14 +576,14 @@ Font defines embedded font resources containing subsetted glyph data (vector out ```xml - - + + - - + + ``` @@ -603,6 +603,7 @@ Glyph defines rendering data for a single glyph. Either `path` or `image` must b | Attribute | Type | Default | Description | |-----------|------|---------|-------------| +| `advance` | float | (required) | Horizontal advance width in design space coordinates (unitsPerEm units). Determines spacing to next glyph in Default positioning mode | | `path` | string | - | SVG path data (vector outline) | | `image` | string | - | Image data (base64 data URI) or external file path | | `offset` | point | 0,0 | Glyph offset in design space coordinates (typically used for bitmap glyphs) | @@ -1262,7 +1263,7 @@ GlyphRun defines pre-layout data for a group of glyphs, each GlyphRun independen 2. Has `xforms` → RSXform mode: Each glyph has rotation+scale+translation (path text) 3. Has `positions` → Point mode: Each glyph has independent (x,y) position (multi-line/complex layout) 4. Has `xPositions` → Horizontal mode: Each glyph has x coordinate, sharing `y` value (single-line horizontal text) -5. Only `glyphs` → Not supported; position data must be provided +5. None of the above → Default mode: Automatically calculate positions from font's `advance` attribute (most compact format) **RSXform**: RSXform is a compressed rotation+scale matrix with four components (scos, ssin, tx, ty): @@ -2551,6 +2552,7 @@ Child elements: `Glyph`* | Attribute | Type | Default | |-----------|------|---------| +| `advance` | float | (required) | | `path` | string | - | | `image` | string | - | | `offset` | point | 0,0 | diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index 48a75bf7b3..e0009b265a 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -576,14 +576,14 @@ Font 定义嵌入字体资源,包含子集化的字形数据(矢量轮廓或 ```xml - - + + - - + + ``` @@ -606,12 +606,13 @@ Glyph 定义单个字形的渲染数据。`path` 和 `image` 二选一必填, | `path` | string | - | SVG 路径数据(矢量轮廓) | | `image` | string | - | 图片数据(base64 数据 URI)或外部文件路径 | | `offset` | point | 0,0 | 字形偏移量,设计空间坐标(通常用于位图字形) | +| `advance` | float | (必填) | 水平步进宽度,设计空间坐标。决定 Default 定位模式下字形间距 | **字形类型**: - **矢量字形**:指定 `path` 属性,使用 SVG 路径语法描述轮廓 - **位图字形**:指定 `image` 属性,用于 Emoji 等彩色字形,可通过 `offset` 调整位置 -**坐标系说明**:字形路径和偏移均使用设计空间坐标。渲染时根据 GlyphRun 的 `fontSize` 和 Font 的 `unitsPerEm` 计算缩放比例:`scale = fontSize / unitsPerEm`。 +**坐标系说明**:字形路径、偏移和步进均使用设计空间坐标。渲染时根据 GlyphRun 的 `fontSize` 和 Font 的 `unitsPerEm` 计算缩放比例:`scale = fontSize / unitsPerEm`。 ### 3.4 文档层级结构 @@ -1262,7 +1263,7 @@ GlyphRun 定义一组字形的预排版数据,每个 GlyphRun 独立引用一 2. 有 `xforms` → RSXform 模式:每个字形有旋转+缩放+平移(路径文本) 3. 有 `positions` → Point 模式:每个字形有独立 (x,y) 位置(多行/复杂布局) 4. 有 `xPositions` → Horizontal 模式:每个字形有 x 坐标,共享 `y` 值(单行水平文本) -5. 仅 `glyphs` → 不支持,必须提供位置数据 +5. 以上都没有 → Default 模式:根据字体的 `advance` 属性自动计算位置(最简洁格式) **RSXform 说明**: RSXform 是压缩的旋转+缩放矩阵,四个分量 (scos, ssin, tx, ty) 表示: @@ -2554,6 +2555,7 @@ Layer / Group | `path` | string | - | | `image` | string | - | | `offset` | point | 0,0 | +| `advance` | float | (必填) | #### SolidColor diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index d740ba678c..779c823e5f 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -1010,6 +1010,7 @@ static void writeResource(XMLBuilder& xml, const Node* node, const Options& opti if (glyph->offset.x != 0 || glyph->offset.y != 0) { xml.addAttribute("offset", pointToString(glyph->offset)); } + xml.addRequiredAttribute("advance", glyph->advance); xml.closeElementSelfClosing(); } xml.closeElement(); diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 3fdbf69c4f..210ad48d69 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -1283,6 +1283,7 @@ static Glyph* parseGlyph(const XMLNode* node, PAGXDocument* doc) { if (!offsetStr.empty()) { glyph->offset = parsePoint(offsetStr); } + glyph->advance = getFloatAttribute(node, "advance", 0); return glyph; } diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index a4c749702b..4c6cef7657 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -149,6 +149,10 @@ static void CollectVectorGlyph(PAGXDocument* document, const tgfx::Font& font, glyph->path = document->makeNode(); *glyph->path = PathDataFromSVGString(PathToSVGString(glyphPath)); + // Get advance in font size space and scale to unitsPerEm space + float advance = font.getAdvance(glyphID); + glyph->advance = advance * scale; + builder.font->glyphs.push_back(glyph); builder.glyphMapping[key] = static_cast(builder.font->glyphs.size()); } @@ -216,6 +220,10 @@ static void CollectBitmapGlyph( glyph->offset.x = imageMatrix.getTranslateX() / fontSize * backingSize; glyph->offset.y = imageMatrix.getTranslateY() / fontSize * backingSize; + // Get advance in font size space and scale to unitsPerEm (backingSize) space + float advance = font.getAdvance(glyphID); + glyph->advance = advance / fontSize * backingSize; + builder.font->glyphs.push_back(glyph); builder.glyphMapping[key] = static_cast(builder.font->glyphs.size()); } diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index db3cd6a497..178f6a78ac 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -324,7 +324,7 @@ class TypesetterContext { tgfx::Font font(typeface, fontSizeForTypeface); size_t count = run->glyphs.size(); - // Determine positioning mode + // Determine positioning mode (priority: matrices > xforms > positions > xPositions > Default) if (!run->matrices.empty() && run->matrices.size() >= count) { auto& buffer = builder.allocRunMatrix(font, count); memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); @@ -357,6 +357,11 @@ class TypesetterContext { auto& buffer = builder.allocRunPosH(font, count, run->y); memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); memcpy(buffer.positions, run->xPositions.data(), count * sizeof(float)); + } else { + // Default mode: use font's advance values to position glyphs + auto& buffer = builder.allocRun(font, count, 0, run->y); + memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); + // No positions to fill - tgfx will compute from font advances } } @@ -394,7 +399,7 @@ class TypesetterContext { if (glyph->offset.x != 0 || glyph->offset.y != 0) { path.transform(tgfx::Matrix::MakeTrans(glyph->offset.x, glyph->offset.y)); } - builder.addGlyph(path); + builder.addGlyph(path, glyph->advance); } } typeface = builder.detach(); @@ -423,7 +428,7 @@ class TypesetterContext { } if (codec) { - builder.addGlyph(codec, ToTGFXPoint(glyph->offset)); + builder.addGlyph(codec, ToTGFXPoint(glyph->offset), glyph->advance); } } } diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 7aa12d0097..e26810c1fc 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -847,12 +847,12 @@ PAG_TEST(PAGXTest, PathTypefaceBasic) { tgfx::PathTypefaceBuilder builder; auto path1 = tgfx::SVGPathParser::FromSVGString("M0 0 L40 0 L40 50 L0 50 Z"); ASSERT_TRUE(path1 != nullptr); - auto glyphId1 = builder.addGlyph(*path1); + auto glyphId1 = builder.addGlyph(*path1, 50); EXPECT_EQ(glyphId1, 1); auto path2 = tgfx::SVGPathParser::FromSVGString("M0 0 L20 50 L40 0 Z"); ASSERT_TRUE(path2 != nullptr); - auto glyphId2 = builder.addGlyph(*path2); + auto glyphId2 = builder.addGlyph(*path2, 50); EXPECT_EQ(glyphId2, 2); auto typeface = builder.detach(); From 95a58c254f816be9e4707028c1091da12a0ccc56 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Thu, 5 Feb 2026 19:18:53 +0800 Subject: [PATCH 346/678] Optimize zoom performance by disabling subtree cache when zooming in. --- DEPS | 2 +- pagx/wechat/CMakeLists.txt | 1 + pagx/wechat/src/PAGXView.cpp | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 386cc4b438..143e8ba7da 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "503a5f559321aafc9ccdc94249967a07827dbcd5", + "commit": "def33a37bdddc28b5395ab9c6a2c572d440bcc60", "dir": "third_party/tgfx" }, { diff --git a/pagx/wechat/CMakeLists.txt b/pagx/wechat/CMakeLists.txt index 2669c5d779..d3170be051 100644 --- a/pagx/wechat/CMakeLists.txt +++ b/pagx/wechat/CMakeLists.txt @@ -22,6 +22,7 @@ if (NOT TARGET tgfx) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) set(TGFX_BUILD_SVG ON CACHE BOOL "" FORCE) set(TGFX_BUILD_LAYERS ON CACHE BOOL "" FORCE) + set(TGFX_USE_PNG_DECODE ON) add_subdirectory(${TGFX_DIR} ${CMAKE_CURRENT_BINARY_DIR}/tgfx) endif () diff --git a/pagx/wechat/src/PAGXView.cpp b/pagx/wechat/src/PAGXView.cpp index 4cfaca0bfb..6fdf1343ac 100644 --- a/pagx/wechat/src/PAGXView.cpp +++ b/pagx/wechat/src/PAGXView.cpp @@ -125,6 +125,11 @@ void PAGXView::applyCenteringTransform() { void PAGXView::updateZoomScaleAndOffset(float zoom, float offsetX, float offsetY) { bool zoomChanged = (std::abs(zoom - lastZoom) > 0.001f); + if (zoom <= 1.0f) { + displayList.setSubtreeCacheMaxSize(1024); + } else { + displayList.setSubtreeCacheMaxSize(0); + } if (zoomChanged) { if (!isZooming) { From 962cea2ac2bfe9462b38a5ee87d60e544a2fe219 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 19:34:26 +0800 Subject: [PATCH 347/678] Update tgfx to support advance parameter in custom typeface and add code review guidelines. --- .codebuddy/commands/cr.md | 3 ++- .codebuddy/rules/Code.md | 1 + DEPS | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.codebuddy/commands/cr.md b/.codebuddy/commands/cr.md index 201a946207..51d1421a19 100644 --- a/.codebuddy/commands/cr.md +++ b/.codebuddy/commands/cr.md @@ -231,4 +231,5 @@ echo "已清理临时审查环境" 7. **接口变更**:公开接口变更需关注必要性和兼容性 8. **测试覆盖**:变更是否有对应测试,边界情况是否覆盖 9. **潜在风险**:是否引入回归风险或影响其他模块 -10. **整体设计**:结合关联代码评估修改后的整体合理性,必要时建议扩大修改范围 +10. **模块架构**:模块职责是否清晰,依赖方向是否合理(如核心模块不应反向依赖平台特定实现) +11. **整体设计**:结合关联代码评估修改后的整体合理性,必要时建议扩大修改范围 diff --git a/.codebuddy/rules/Code.md b/.codebuddy/rules/Code.md index 9387f351be..40e92dcafe 100644 --- a/.codebuddy/rules/Code.md +++ b/.codebuddy/rules/Code.md @@ -12,6 +12,7 @@ alwaysApply: true - 重构时审查关联代码合理性,顺带清理冗余,不考虑向后兼容 - 版权声明里的年份对新增的文件要使用当前年份(如 `Copyright (C) 2026 Tencent`),已有文件保持原年份不变 - 驼峰命名法:大写开头(类静态方法、全局函数/变量)、小写开头(成员方法/变量、局部变量)、全大写下划线(静态常量) +- 枚举值和常量不加 `k` 前缀,直接使用大写开头的驼峰命名(如 `GlyphPositioning::Default`) - 变量命名避免缩写,简短且语义明确 - 变量声明时一律赋初始值(即使是 `={}`),智能指针初始值使用 nullptr - 避免 lambda 表达式,改用显式方法或函数 diff --git a/DEPS b/DEPS index 386cc4b438..1693fa9196 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "503a5f559321aafc9ccdc94249967a07827dbcd5", + "commit": "39198e37397209d25fd6cfedb790e7feabc02227", "dir": "third_party/tgfx" }, { From dd41aa4cc844f746037575f4756f5d2c20688a23 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 19:50:09 +0800 Subject: [PATCH 348/678] Optimize GlyphRun export to use Default mode when positions match advance layout and improve spec docs. --- pagx/spec/pagx_spec.md | 6 ++-- pagx/spec/pagx_spec.zh_CN.md | 6 ++-- pagx/src/tgfx/FontEmbedder.cpp | 50 ++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index dbe921af16..5a38b1e2f1 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -603,7 +603,7 @@ Glyph defines rendering data for a single glyph. Either `path` or `image` must b | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `advance` | float | (required) | Horizontal advance width in design space coordinates (unitsPerEm units). Determines spacing to next glyph in Default positioning mode | +| `advance` | float | (required) | Horizontal advance width in design space coordinates (unitsPerEm units) | | `path` | string | - | SVG path data (vector outline) | | `image` | string | - | Image data (base64 data URI) or external file path | | `offset` | point | 0,0 | Glyph offset in design space coordinates (typically used for bitmap glyphs) | @@ -1263,7 +1263,7 @@ GlyphRun defines pre-layout data for a group of glyphs, each GlyphRun independen 2. Has `xforms` → RSXform mode: Each glyph has rotation+scale+translation (path text) 3. Has `positions` → Point mode: Each glyph has independent (x,y) position (multi-line/complex layout) 4. Has `xPositions` → Horizontal mode: Each glyph has x coordinate, sharing `y` value (single-line horizontal text) -5. None of the above → Default mode: Automatically calculate positions from font's `advance` attribute (most compact format) +5. None of the above → Default mode: Automatically calculate horizontal layout positions from each Glyph's `advance` attribute in Font. First glyph is at x=0, subsequent glyphs are positioned by accumulating the previous glyph's advance (most compact format) **RSXform**: RSXform is a compressed rotation+scale matrix with four components (scos, ssin, tx, ty): @@ -1653,7 +1653,7 @@ Applies transforms and style overrides to glyphs within selected ranges. TextMod | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `anchorPoint` | point | 0,0 | Anchor point offset | +| `anchorPoint` | point | 0,0 | Anchor point offset, relative to glyph's default anchor position. Each glyph's default anchor is at `(advance × 0.5, 0)`, the horizontal center at baseline | | `position` | point | 0,0 | Position offset | | `rotation` | float | 0 | Rotation | | `scale` | point | 1,1 | Scale | diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index e0009b265a..17e676d985 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -606,7 +606,7 @@ Glyph 定义单个字形的渲染数据。`path` 和 `image` 二选一必填, | `path` | string | - | SVG 路径数据(矢量轮廓) | | `image` | string | - | 图片数据(base64 数据 URI)或外部文件路径 | | `offset` | point | 0,0 | 字形偏移量,设计空间坐标(通常用于位图字形) | -| `advance` | float | (必填) | 水平步进宽度,设计空间坐标。决定 Default 定位模式下字形间距 | +| `advance` | float | (必填) | 水平步进宽度,设计空间坐标 | **字形类型**: - **矢量字形**:指定 `path` 属性,使用 SVG 路径语法描述轮廓 @@ -1263,7 +1263,7 @@ GlyphRun 定义一组字形的预排版数据,每个 GlyphRun 独立引用一 2. 有 `xforms` → RSXform 模式:每个字形有旋转+缩放+平移(路径文本) 3. 有 `positions` → Point 模式:每个字形有独立 (x,y) 位置(多行/复杂布局) 4. 有 `xPositions` → Horizontal 模式:每个字形有 x 坐标,共享 `y` 值(单行水平文本) -5. 以上都没有 → Default 模式:根据字体的 `advance` 属性自动计算位置(最简洁格式) +5. 以上都没有 → Default 模式:根据 Font 字体中每个 Glyph 的 `advance` 属性自动计算水平排版位置,首个字形位于 x=0,后续字形依次累加前一字形的 advance(最简洁格式) **RSXform 说明**: RSXform 是压缩的旋转+缩放矩阵,四个分量 (scos, ssin, tx, ty) 表示: @@ -1651,7 +1651,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `anchorPoint` | point | 0,0 | 锚点偏移 | +| `anchorPoint` | point | 0,0 | 锚点偏移,相对于字形默认锚点位置。每个字形的默认锚点位于 `(advance × 0.5, 0)`,即字形水平中心的基线位置 | | `position` | point | 0,0 | 位置偏移 | | `rotation` | float | 0 | 旋转 | | `scale` | point | 1,1 | 缩放 | diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 4c6cef7657..5f17d12b5a 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -233,6 +233,47 @@ static bool IsVectorGlyph(const tgfx::Font& font, tgfx::GlyphID glyphID) { return font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); } +static constexpr float kPositionTolerance = 0.5f; + +static bool CanUseDefaultMode(const tgfx::GlyphRun& run, const std::vector& indices, + Font* font, + const std::unordered_map& map, + float fontSize, float* outOffsetY) { + if (run.positioning != tgfx::GlyphPositioning::Horizontal && + run.positioning != tgfx::GlyphPositioning::Default) { + return false; + } + if (run.positioning == tgfx::GlyphPositioning::Default) { + *outOffsetY = run.offsetY; + return true; + } + if (indices.empty()) { + return false; + } + float scale = fontSize / static_cast(font->unitsPerEm); + float expectedX = 0.0f; + auto* typeface = run.font.getTypeface().get(); + for (size_t i : indices) { + float actualX = run.positions[i]; + if (std::abs(actualX - expectedX) > kPositionTolerance) { + return false; + } + GlyphKey key = {typeface, run.glyphs[i]}; + auto it = map.find(key); + if (it == map.end() || it->second == 0) { + return false; + } + tgfx::GlyphID mappedID = it->second; + if (mappedID > font->glyphs.size()) { + return false; + } + float advance = font->glyphs[mappedID - 1]->advance * scale; + expectedX += advance; + } + *outOffsetY = run.offsetY; + return true; +} + static GlyphRun* CreateGlyphRunForIndices( PAGXDocument* document, const tgfx::GlyphRun& run, const std::vector& indices, Font* font, @@ -252,6 +293,13 @@ static GlyphRun* CreateGlyphRunForIndices( } } + // Try to use Default mode if positions match advance-based layout + float offsetY = 0.0f; + if (CanUseDefaultMode(run, indices, font, glyphMapping, fontSize, &offsetY)) { + glyphRun->y = offsetY; + return glyphRun; + } + switch (run.positioning) { case tgfx::GlyphPositioning::Horizontal: { glyphRun->y = run.offsetY; @@ -293,6 +341,8 @@ static GlyphRun* CreateGlyphRunForIndices( } break; } + default: + break; } return glyphRun; From d294014321c018c5a5a4e2878be417e68346a43f Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 20:04:46 +0800 Subject: [PATCH 349/678] Remove k prefix from constants and use FloatNearlyEqual for position comparison in FontEmbedder. --- pagx/src/tgfx/FontEmbedder.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 5f17d12b5a..078899c772 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -32,7 +32,12 @@ namespace pagx { -static constexpr int kVectorFontUnitsPerEm = 1000; +static constexpr int VectorFontUnitsPerEm = 1000; +static constexpr float FloatNearlyZero = 1.0f / (1 << 12); + +static bool FloatNearlyEqual(float x, float y) { + return std::abs(x - y) <= FloatNearlyZero; +} static std::string PathToSVGString(const tgfx::Path& path) { std::string result = {}; @@ -137,12 +142,12 @@ static void CollectVectorGlyph(PAGXDocument* document, const tgfx::Font& font, return; } - float scale = static_cast(kVectorFontUnitsPerEm) / font.getSize(); + float scale = static_cast(VectorFontUnitsPerEm) / font.getSize(); glyphPath.transform(tgfx::Matrix::MakeScale(scale, scale)); if (builder.font == nullptr) { builder.font = document->makeNode(); - builder.font->unitsPerEm = kVectorFontUnitsPerEm; + builder.font->unitsPerEm = VectorFontUnitsPerEm; } auto glyph = document->makeNode(); @@ -233,8 +238,6 @@ static bool IsVectorGlyph(const tgfx::Font& font, tgfx::GlyphID glyphID) { return font.getPath(glyphID, &glyphPath) && !glyphPath.isEmpty(); } -static constexpr float kPositionTolerance = 0.5f; - static bool CanUseDefaultMode(const tgfx::GlyphRun& run, const std::vector& indices, Font* font, const std::unordered_map& map, @@ -255,7 +258,7 @@ static bool CanUseDefaultMode(const tgfx::GlyphRun& run, const std::vector kPositionTolerance) { + if (!FloatNearlyEqual(actualX, expectedX)) { return false; } GlyphKey key = {typeface, run.glyphs[i]}; From 581d7791fc9ca148a9f0bee708e51e7407ceddd1 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 20:08:27 +0800 Subject: [PATCH 350/678] Support Default positioning mode for Typesetter generated TextBlobs when positions match advance layout. --- pagx/src/tgfx/Typesetter.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index 178f6a78ac..218eb8e9ea 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -34,6 +34,12 @@ namespace pagx { +static constexpr float FloatNearlyZero = 1.0f / (1 << 12); + +static bool FloatNearlyEqual(float x, float y) { + return std::abs(x - y) <= FloatNearlyZero; +} + void Typesetter::registerTypeface(std::shared_ptr typeface) { if (typeface == nullptr) { return; @@ -120,6 +126,7 @@ class TypesetterContext { tgfx::Font font = {}; std::vector glyphIDs = {}; std::vector xPositions = {}; + bool canUseDefaultMode = true; }; // Shaped text information for a single Text element. @@ -290,9 +297,15 @@ class TypesetterContext { continue; } - auto& buffer = builder.allocRunPosH(run.font, run.glyphIDs.size(), 0); - memcpy(buffer.glyphs, run.glyphIDs.data(), run.glyphIDs.size() * sizeof(tgfx::GlyphID)); - memcpy(buffer.positions, run.xPositions.data(), run.xPositions.size() * sizeof(float)); + if (run.canUseDefaultMode) { + // Default mode: use font's advance values to position glyphs + auto& buffer = builder.allocRun(run.font, run.glyphIDs.size(), 0, 0); + memcpy(buffer.glyphs, run.glyphIDs.data(), run.glyphIDs.size() * sizeof(tgfx::GlyphID)); + } else { + auto& buffer = builder.allocRunPosH(run.font, run.glyphIDs.size(), 0); + memcpy(buffer.glyphs, run.glyphIDs.data(), run.glyphIDs.size() * sizeof(tgfx::GlyphID)); + memcpy(buffer.positions, run.xPositions.data(), run.xPositions.size() * sizeof(float)); + } } auto textBlob = builder.build(); @@ -450,10 +463,12 @@ class TypesetterContext { tgfx::Font primaryFont(primaryTypeface, text->fontSize); float currentX = 0; const std::string& content = text->text; + bool hasLetterSpacing = !FloatNearlyEqual(text->letterSpacing, 0.0f); // Current run being built ShapedGlyphRun* currentRun = nullptr; std::shared_ptr currentTypeface = nullptr; + float runStartX = 0; size_t i = 0; while (i < content.size()) { @@ -528,6 +543,9 @@ class TypesetterContext { currentRun = &info.runs.back(); currentRun->font = glyphFont; currentTypeface = glyphTypeface; + runStartX = currentX; + // Can use Default mode only if run starts at 0 and no letterSpacing + currentRun->canUseDefaultMode = FloatNearlyEqual(runStartX, 0.0f) && !hasLetterSpacing; } currentRun->xPositions.push_back(currentX); From d30bfc8d31817931d0480d1a4c8980382cdc38b7 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 20:18:37 +0800 Subject: [PATCH 351/678] Add x attribute to GlyphRun for Default mode starting position. --- pagx/include/pagx/nodes/GlyphRun.h | 7 ++++++- pagx/spec/pagx_spec.md | 6 ++++-- pagx/spec/pagx_spec.zh_CN.md | 6 ++++-- pagx/src/PAGXExporter.cpp | 4 ++++ pagx/src/PAGXImporter.cpp | 1 + pagx/src/tgfx/FontEmbedder.cpp | 14 +++++++++++--- pagx/src/tgfx/Typesetter.cpp | 12 +++++++----- 7 files changed, 37 insertions(+), 13 deletions(-) diff --git a/pagx/include/pagx/nodes/GlyphRun.h b/pagx/include/pagx/nodes/GlyphRun.h index fd2ee70c32..44b5f4085a 100644 --- a/pagx/include/pagx/nodes/GlyphRun.h +++ b/pagx/include/pagx/nodes/GlyphRun.h @@ -65,7 +65,12 @@ class GlyphRun : public Node { std::vector glyphs = {}; /** - * Shared y coordinate for Horizontal positioning mode. The default value is 0. + * Starting x coordinate for Default positioning mode. The default value is 0. + */ + float x = 0.0f; + + /** + * Shared y coordinate for Default and Horizontal positioning modes. The default value is 0. */ float y = 0.0f; diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 5a38b1e2f1..2ec41d86a4 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -1252,7 +1252,8 @@ GlyphRun defines pre-layout data for a group of glyphs, each GlyphRun independen | `font` | idref | (required) | Font resource reference `@id` | | `fontSize` | float | 12 | Rendering font size. Actual scale = `fontSize / font.unitsPerEm` | | `glyphs` | string | (required) | GlyphID sequence, comma-separated (0 means missing glyph) | -| `y` | float | 0 | Shared y coordinate (Horizontal mode only) | +| `x` | float | 0 | Starting x coordinate (Default mode only) | +| `y` | float | 0 | Shared y coordinate (Default and Horizontal modes) | | `xPositions` | string | - | x coordinate sequence, comma-separated (Horizontal mode) | | `positions` | string | - | (x,y) coordinate sequence, semicolon-separated (Point mode) | | `xforms` | string | - | RSXform sequence (scos,ssin,tx,ty), semicolon-separated (RSXform mode) | @@ -1263,7 +1264,7 @@ GlyphRun defines pre-layout data for a group of glyphs, each GlyphRun independen 2. Has `xforms` → RSXform mode: Each glyph has rotation+scale+translation (path text) 3. Has `positions` → Point mode: Each glyph has independent (x,y) position (multi-line/complex layout) 4. Has `xPositions` → Horizontal mode: Each glyph has x coordinate, sharing `y` value (single-line horizontal text) -5. None of the above → Default mode: Automatically calculate horizontal layout positions from each Glyph's `advance` attribute in Font. First glyph is at x=0, subsequent glyphs are positioned by accumulating the previous glyph's advance (most compact format) +5. None of the above → Default mode: First glyph starts at `(x, y)`, subsequent glyphs are positioned by accumulating each Glyph's `advance` attribute in Font (most compact format) **RSXform**: RSXform is a compressed rotation+scale matrix with four components (scos, ssin, tx, ty): @@ -2788,6 +2789,7 @@ Child elements: `CDATA` text, `GlyphRun`* | `font` | idref | (required) | | `fontSize` | float | 12 | | `glyphs` | string | (required) | +| `x` | float | 0 | | `y` | float | 0 | | `xPositions` | string | - | | `positions` | string | - | diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index 17e676d985..bb09d5b7b5 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -1252,7 +1252,8 @@ GlyphRun 定义一组字形的预排版数据,每个 GlyphRun 独立引用一 | `font` | idref | (必填) | 引用 Font 资源 `@id` | | `fontSize` | float | 12 | 渲染字号。实际缩放比例 = `fontSize / font.unitsPerEm` | | `glyphs` | string | (必填) | GlyphID 序列,逗号分隔(0 表示缺失字形) | -| `y` | float | 0 | 共享 y 坐标(仅 Horizontal 模式) | +| `x` | float | 0 | 起始 x 坐标(仅 Default 模式) | +| `y` | float | 0 | 共享 y 坐标(Default 和 Horizontal 模式) | | `xPositions` | string | - | x 坐标序列,逗号分隔(Horizontal 模式) | | `positions` | string | - | (x,y) 坐标序列,分号分隔(Point 模式) | | `xforms` | string | - | RSXform 序列 (scos,ssin,tx,ty),分号分隔(RSXform 模式) | @@ -1263,7 +1264,7 @@ GlyphRun 定义一组字形的预排版数据,每个 GlyphRun 独立引用一 2. 有 `xforms` → RSXform 模式:每个字形有旋转+缩放+平移(路径文本) 3. 有 `positions` → Point 模式:每个字形有独立 (x,y) 位置(多行/复杂布局) 4. 有 `xPositions` → Horizontal 模式:每个字形有 x 坐标,共享 `y` 值(单行水平文本) -5. 以上都没有 → Default 模式:根据 Font 字体中每个 Glyph 的 `advance` 属性自动计算水平排版位置,首个字形位于 x=0,后续字形依次累加前一字形的 advance(最简洁格式) +5. 以上都没有 → Default 模式:首个字形起始于 `(x, y)`,后续字形依次累加 Font 中每个 Glyph 的 `advance` 属性(最简洁格式) **RSXform 说明**: RSXform 是压缩的旋转+缩放矩阵,四个分量 (scos, ssin, tx, ty) 表示: @@ -2788,6 +2789,7 @@ Layer / Group | `font` | idref | (必填) | | `fontSize` | float | 12 | | `glyphs` | string | (必填) | +| `x` | float | 0 | | `y` | float | 0 | | `xPositions` | string | - | | `positions` | string | - | diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index 779c823e5f..aa52973323 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -535,6 +535,10 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, const Optio // Horizontal mode xml.addAttribute("y", run->y); xml.addRequiredAttribute("xPositions", floatListToString(run->xPositions)); + } else { + // Default mode: use x/y as starting point + xml.addAttribute("x", run->x); + xml.addAttribute("y", run->y); } xml.closeElementSelfClosing(); diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 210ad48d69..42fe585879 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -1302,6 +1302,7 @@ static GlyphRun* parseGlyphRun(const XMLNode* node, PAGXDocument* doc) { } } run->fontSize = getFloatAttribute(node, "fontSize", 12); + run->x = getFloatAttribute(node, "x", 0); run->y = getFloatAttribute(node, "y", 0); // Parse glyphs only if font is valid diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 078899c772..37eeb17e17 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -241,12 +241,15 @@ static bool IsVectorGlyph(const tgfx::Font& font, tgfx::GlyphID glyphID) { static bool CanUseDefaultMode(const tgfx::GlyphRun& run, const std::vector& indices, Font* font, const std::unordered_map& map, - float fontSize, float* outOffsetY) { + float fontSize, float* outOffsetX, float* outOffsetY) { if (run.positioning != tgfx::GlyphPositioning::Horizontal && run.positioning != tgfx::GlyphPositioning::Default) { return false; } if (run.positioning == tgfx::GlyphPositioning::Default) { + // Default mode doesn't have explicit positions, so we can't extract startX from positions. + // This case is already Default mode, just pass through. + *outOffsetX = 0; *outOffsetY = run.offsetY; return true; } @@ -254,7 +257,9 @@ static bool CanUseDefaultMode(const tgfx::GlyphRun& run, const std::vector(font->unitsPerEm); - float expectedX = 0.0f; + // Use first glyph's x position as the starting point + float startX = run.positions[indices[0]]; + float expectedX = startX; auto* typeface = run.font.getTypeface().get(); for (size_t i : indices) { float actualX = run.positions[i]; @@ -273,6 +278,7 @@ static bool CanUseDefaultMode(const tgfx::GlyphRun& run, const std::vectorglyphs[mappedID - 1]->advance * scale; expectedX += advance; } + *outOffsetX = startX; *outOffsetY = run.offsetY; return true; } @@ -297,8 +303,10 @@ static GlyphRun* CreateGlyphRunForIndices( } // Try to use Default mode if positions match advance-based layout + float offsetX = 0.0f; float offsetY = 0.0f; - if (CanUseDefaultMode(run, indices, font, glyphMapping, fontSize, &offsetY)) { + if (CanUseDefaultMode(run, indices, font, glyphMapping, fontSize, &offsetX, &offsetY)) { + glyphRun->x = offsetX; glyphRun->y = offsetY; return glyphRun; } diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index 218eb8e9ea..3b6583274e 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -126,6 +126,7 @@ class TypesetterContext { tgfx::Font font = {}; std::vector glyphIDs = {}; std::vector xPositions = {}; + float startX = 0; bool canUseDefaultMode = true; }; @@ -298,8 +299,8 @@ class TypesetterContext { } if (run.canUseDefaultMode) { - // Default mode: use font's advance values to position glyphs - auto& buffer = builder.allocRun(run.font, run.glyphIDs.size(), 0, 0); + // Default mode: use font's advance values to position glyphs, starting at run.startX + auto& buffer = builder.allocRun(run.font, run.glyphIDs.size(), run.startX, 0); memcpy(buffer.glyphs, run.glyphIDs.data(), run.glyphIDs.size() * sizeof(tgfx::GlyphID)); } else { auto& buffer = builder.allocRunPosH(run.font, run.glyphIDs.size(), 0); @@ -372,7 +373,7 @@ class TypesetterContext { memcpy(buffer.positions, run->xPositions.data(), count * sizeof(float)); } else { // Default mode: use font's advance values to position glyphs - auto& buffer = builder.allocRun(font, count, 0, run->y); + auto& buffer = builder.allocRun(font, count, run->x, run->y); memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); // No positions to fill - tgfx will compute from font advances } @@ -544,8 +545,9 @@ class TypesetterContext { currentRun->font = glyphFont; currentTypeface = glyphTypeface; runStartX = currentX; - // Can use Default mode only if run starts at 0 and no letterSpacing - currentRun->canUseDefaultMode = FloatNearlyEqual(runStartX, 0.0f) && !hasLetterSpacing; + currentRun->startX = runStartX; + // Can use Default mode if no letterSpacing (positions follow advance values) + currentRun->canUseDefaultMode = !hasLetterSpacing; } currentRun->xPositions.push_back(currentX); From dc3e4a9d311a4aba1264a68c5abc9c6a96b21915 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 20:25:14 +0800 Subject: [PATCH 352/678] Extract FloatNearlyEqual to MathUtil.h for code reuse. --- pagx/src/MathUtil.h | 31 +++++++++++++++++++++++++++++++ pagx/src/tgfx/FontEmbedder.cpp | 7 +------ pagx/src/tgfx/Typesetter.cpp | 8 +------- 3 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 pagx/src/MathUtil.h diff --git a/pagx/src/MathUtil.h b/pagx/src/MathUtil.h new file mode 100644 index 0000000000..110b1a2bb2 --- /dev/null +++ b/pagx/src/MathUtil.h @@ -0,0 +1,31 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace pagx { + +static constexpr float FloatNearlyZero = 1.0f / (1 << 12); + +inline bool FloatNearlyEqual(float x, float y) { + return std::abs(x - y) <= FloatNearlyZero; +} + +} // namespace pagx diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 37eeb17e17..56eaea4306 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -18,8 +18,8 @@ #include "pagx/FontEmbedder.h" #include -#include #include +#include "MathUtil.h" #include "SVGPathParser.h" #include "pagx/nodes/Font.h" #include "pagx/nodes/Image.h" @@ -33,11 +33,6 @@ namespace pagx { static constexpr int VectorFontUnitsPerEm = 1000; -static constexpr float FloatNearlyZero = 1.0f / (1 << 12); - -static bool FloatNearlyEqual(float x, float y) { - return std::abs(x - y) <= FloatNearlyZero; -} static std::string PathToSVGString(const tgfx::Path& path) { std::string result = {}; diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index 3b6583274e..a4419cc452 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -17,8 +17,8 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "pagx/Typesetter.h" -#include #include "Base64.h" +#include "MathUtil.h" #include "SVGPathParser.h" #include "pagx/nodes/Composition.h" #include "pagx/nodes/Font.h" @@ -34,12 +34,6 @@ namespace pagx { -static constexpr float FloatNearlyZero = 1.0f / (1 << 12); - -static bool FloatNearlyEqual(float x, float y) { - return std::abs(x - y) <= FloatNearlyZero; -} - void Typesetter::registerTypeface(std::shared_ptr typeface) { if (typeface == nullptr) { return; From 76484b015fc6a541e673534d77658e218942753e Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 21:19:27 +0800 Subject: [PATCH 353/678] Update PAGX spec type naming: Use UpperCamelCase for defined types (Point, Rect, etc.) while keeping basic types (int, float, etc.) lowercase --- pagx/spec/pagx_spec.md | 168 +++++++++++++++++------------------ pagx/spec/pagx_spec.zh_CN.md | 168 +++++++++++++++++------------------ 2 files changed, 168 insertions(+), 168 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 2ec41d86a4..de9e6b3638 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -323,7 +323,7 @@ Color sources define colors that can be used for fills and strokes, supporting t | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `color` | color | (required) | Color value | +| `color` | Color | (required) | Color value | ##### LinearGradient @@ -346,9 +346,9 @@ Linear gradients interpolate along the direction from start point to end point. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `startPoint` | point | (required) | Start point | -| `endPoint` | point | (required) | End point | -| `matrix` | string | identity matrix | Transform matrix | +| `startPoint` | Point | (required) | Start point | +| `endPoint` | Point | (required) | End point | +| `matrix` | Matrix | identity matrix | Transform matrix | **Calculation**: For a point P, its color is determined by the projection position of P onto the line connecting start and end points. @@ -373,9 +373,9 @@ Radial gradients radiate outward from the center. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `center` | point | 0,0 | Center point | +| `center` | Point | 0,0 | Center point | | `radius` | float | (required) | Gradient radius | -| `matrix` | string | identity matrix | Transform matrix | +| `matrix` | Matrix | identity matrix | Transform matrix | **Calculation**: For a point P, its color is determined by `distance(P, center) / radius`. @@ -402,10 +402,10 @@ Conic gradients (also known as sweep gradients) interpolate along the circumfere | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `center` | point | 0,0 | Center point | +| `center` | Point | 0,0 | Center point | | `startAngle` | float | 0 | Start angle | | `endAngle` | float | 360 | End angle | -| `matrix` | string | identity matrix | Transform matrix | +| `matrix` | Matrix | identity matrix | Transform matrix | **Calculation**: For a point P, its color is determined by the ratio of `atan2(P.y - center.y, P.x - center.x)` within the `[startAngle, endAngle]` range. @@ -430,9 +430,9 @@ Diamond gradients radiate from the center toward the four corners. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `center` | point | 0,0 | Center point | +| `center` | Point | 0,0 | Center point | | `radius` | float | (required) | Gradient radius | -| `matrix` | string | identity matrix | Transform matrix | +| `matrix` | Matrix | identity matrix | Transform matrix | **Calculation**: For a point P, its color is determined by the Manhattan distance `(|P.x - center.x| + |P.y - center.y|) / radius`. @@ -445,7 +445,7 @@ Diamond gradients radiate from the center toward the four corners. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `offset` | float | (required) | Position 0.0~1.0 | -| `color` | color | (required) | Stop color | +| `color` | Color | (required) | Stop color | **Common Gradient Rules**: @@ -471,7 +471,7 @@ Image patterns use an image as a color source. | `tileModeY` | TileMode | clamp | Y-direction tile mode | | `filterMode` | FilterMode | linear | Texture filter mode | | `mipmapMode` | MipmapMode | linear | Mipmap mode | -| `matrix` | string | identity matrix | Transform matrix | +| `matrix` | Matrix | identity matrix | Transform matrix | **TileMode**: `clamp`, `repeat`, `mirror`, `decal` @@ -606,7 +606,7 @@ Glyph defines rendering data for a single glyph. Either `path` or `image` must b | `advance` | float | (required) | Horizontal advance width in design space coordinates (unitsPerEm units) | | `path` | string | - | SVG path data (vector outline) | | `image` | string | - | Image data (base64 data URI) or external file path | -| `offset` | point | 0,0 | Glyph offset in design space coordinates (typically used for bitmap glyphs) | +| `offset` | Point | 0,0 | Glyph offset in design space coordinates (typically used for bitmap glyphs) | **Glyph Types**: - **Vector glyph**: Specifies the `path` attribute using SVG path syntax to describe the outline @@ -740,14 +740,14 @@ Layer child elements are automatically categorized into four collections by type | `blendMode` | BlendMode | normal | Blend mode | | `x` | float | 0 | X position | | `y` | float | 0 | Y position | -| `matrix` | string | identity matrix | 2D transform "a,b,c,d,tx,ty" | -| `matrix3D` | string | - | 3D transform (16 values, column-major) | +| `matrix` | Matrix | identity matrix | 2D transform "a,b,c,d,tx,ty" | +| `matrix3D` | Matrix | - | 3D transform (16 values, column-major) | | `preserve3D` | bool | false | Preserve 3D transform | | `antiAlias` | bool | true | Edge anti-aliasing | | `groupOpacity` | bool | false | Group opacity | | `passThroughBackground` | bool | true | Whether to pass background through to child layers | | `excludeChildEffectsInLayerStyle` | bool | false | Whether layer styles exclude child layer effects | -| `scrollRect` | string | - | Scroll clipping region "x,y,w,h" | +| `scrollRect` | Rect | - | Scroll clipping region "x,y,w,h" | | `mask` | idref | - | Mask layer reference "@id" | | `maskType` | MaskType | alpha | Mask type | | `composition` | idref | - | Composition reference "@id" | @@ -828,7 +828,7 @@ Draws a drop shadow **below** the layer. Computes shadow shape based on opaque l | `offsetY` | float | 0 | Y offset | | `blurX` | float | 0 | X blur radius | | `blurY` | float | 0 | Y blur radius | -| `color` | color | #000000 | Shadow color | +| `color` | Color | #000000 | Shadow color | | `showBehindLayer` | bool | true | Whether shadow shows behind layer | **Rendering Steps**: @@ -866,7 +866,7 @@ Draws an inner shadow **above** the layer, appearing inside the layer content. C | `offsetY` | float | 0 | Y offset | | `blurX` | float | 0 | X blur radius | | `blurY` | float | 0 | Y blur radius | -| `color` | color | #000000 | Shadow color | +| `color` | Color | #000000 | Shadow color | **Rendering Steps**: 1. Get opaque layer content and offset by `(offsetX, offsetY)` @@ -910,7 +910,7 @@ Generates shadow effect based on filter input. Unlike DropShadowStyle, the filte | `offsetY` | float | 0 | Y offset | | `blurX` | float | 0 | X blur radius | | `blurY` | float | 0 | Y blur radius | -| `color` | color | #000000 | Shadow color | +| `color` | Color | #000000 | Shadow color | | `shadowOnly` | bool | false | Show shadow only | **Rendering Steps**: @@ -929,7 +929,7 @@ Draws shadow inside the filter input. | `offsetY` | float | 0 | Y offset | | `blurX` | float | 0 | X blur radius | | `blurY` | float | 0 | Y blur radius | -| `color` | color | #000000 | Shadow color | +| `color` | Color | #000000 | Shadow color | | `shadowOnly` | bool | false | Show shadow only | **Rendering Steps**: @@ -944,7 +944,7 @@ Overlays a specified color onto the layer using a specified blend mode. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `color` | color | (required) | Blend color | +| `color` | Color | (required) | Blend color | | `blendMode` | BlendMode | normal | Blend mode (see Section 4.1) | #### 4.4.5 ColorMatrixFilter @@ -953,7 +953,7 @@ Transforms colors using a 4×5 color matrix. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `matrix` | string | (required) | 4x5 color matrix (20 comma-separated floats) | +| `matrix` | Matrix | (required) | 4x5 color matrix (20 comma-separated floats) | **Matrix Format** (20 values, row-major): ``` @@ -1085,8 +1085,8 @@ Rectangles are defined from center point with uniform corner rounding support. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `center` | point | 0,0 | Center point | -| `size` | size | 100,100 | Dimensions "width,height" | +| `center` | Point | 0,0 | Center point | +| `size` | Size | 100,100 | Dimensions "width,height" | | `roundness` | float | 0 | Corner radius | | `reversed` | bool | false | Reverse path direction | @@ -1114,8 +1114,8 @@ Ellipses are defined from center point. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `center` | point | 0,0 | Center point | -| `size` | size | 100,100 | Dimensions "width,height" | +| `center` | Point | 0,0 | Center point | +| `size` | Size | 100,100 | Dimensions "width,height" | | `reversed` | bool | false | Reverse path direction | **Calculation Rules**: @@ -1138,7 +1138,7 @@ Supports both regular polygon and star modes. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `center` | point | 0,0 | Center point | +| `center` | Point | 0,0 | Center point | | `type` | PolystarType | star | Type (see below) | | `pointCount` | float | 5 | Number of points (supports decimals) | | `outerRadius` | float | 100 | Outer radius | @@ -1209,7 +1209,7 @@ Text elements provide geometric shapes for text content. Unlike shape elements t | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `text` | string | "" | Text content | -| `position` | point | 0,0 | Text start position, y is baseline (may be overridden by TextLayout) | +| `position` | Point | 0,0 | Text start position, y is baseline (may be overridden by TextLayout) | | `fontFamily` | string | system default | Font family | | `fontStyle` | string | "Regular" | Font variant (Regular, Bold, Italic, Bold Italic, etc.) | | `fontSize` | float | 12 | Font size | @@ -1363,7 +1363,7 @@ Fill draws the interior region of geometry using a specified color source. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `color` | color/idref | #000000 | Color value or color source reference, default black | +| `color` | Color/idref | #000000 | Color value or color source reference, default black | | `alpha` | float | 1 | Opacity 0~1 | | `blendMode` | BlendMode | normal | Blend mode (see Section 4.1) | | `fillRule` | FillRule | winding | Fill rule (see below) | @@ -1415,7 +1415,7 @@ Stroke draws lines along geometry boundaries. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `color` | color/idref | #000000 | Color value or color source reference, default black | +| `color` | Color/idref | #000000 | Color value or color source reference, default black | | `width` | float | 1 | Stroke width | | `alpha` | float | 1 | Opacity 0~1 | | `blendMode` | BlendMode | normal | Blend mode (see Section 4.1) | @@ -1654,15 +1654,15 @@ Applies transforms and style overrides to glyphs within selected ranges. TextMod | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `anchorPoint` | point | 0,0 | Anchor point offset, relative to glyph's default anchor position. Each glyph's default anchor is at `(advance × 0.5, 0)`, the horizontal center at baseline | -| `position` | point | 0,0 | Position offset | +| `anchorPoint` | Point | 0,0 | Anchor point offset, relative to glyph's default anchor position. Each glyph's default anchor is at `(advance × 0.5, 0)`, the horizontal center at baseline | +| `position` | Point | 0,0 | Position offset | | `rotation` | float | 0 | Rotation | -| `scale` | point | 1,1 | Scale | +| `scale` | Point | 1,1 | Scale | | `skew` | float | 0 | Skew | | `skewAxis` | float | 0 | Skew axis | | `alpha` | float | 1 | Opacity | -| `fillColor` | color | - | Fill color override | -| `strokeColor` | color | - | Stroke color override | +| `fillColor` | Color | - | Fill color override | +| `strokeColor` | Color | - | Stroke color override | | `strokeWidth` | float | - | Stroke width override | **Selector Calculation**: @@ -1823,7 +1823,7 @@ During rendering, an attached text typesetting module performs pre-layout, recal | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `position` | point | 0,0 | Layout origin | +| `position` | Point | 0,0 | Layout origin | | `width` | float | auto | Layout width (auto-wraps when specified) | | `height` | float | auto | Layout height (enables vertical alignment when specified) | | `textAlign` | TextAlign | start | Horizontal alignment | @@ -1899,10 +1899,10 @@ Repeater duplicates accumulated content and rendered styles, applying progressiv | `copies` | float | 3 | Number of copies | | `offset` | float | 0 | Start offset | | `order` | RepeaterOrder | belowOriginal | Stacking order | -| `anchorPoint` | point | 0,0 | Anchor point | -| `position` | point | 100,100 | Position offset per copy | +| `anchorPoint` | Point | 0,0 | Anchor point | +| `position` | Point | 100,100 | Position offset per copy | | `rotation` | float | 0 | Rotation per copy | -| `scale` | point | 1,1 | Scale per copy | +| `scale` | Point | 1,1 | Scale per copy | | `startAlpha` | float | 1 | First copy opacity | | `endAlpha` | float | 1 | Last copy opacity | @@ -1982,10 +1982,10 @@ Group is a VectorElement container with transform properties. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `anchorPoint` | point | 0,0 | Anchor point "x,y" | -| `position` | point | 0,0 | Position "x,y" | +| `anchorPoint` | Point | 0,0 | Anchor point "x,y" | +| `position` | Point | 0,0 | Position "x,y" | | `rotation` | float | 0 | Rotation angle | -| `scale` | point | 1,1 | Scale "sx,sy" | +| `scale` | Point | 1,1 | Scale "sx,sy" | | `skew` | float | 0 | Skew amount | | `skewAxis` | float | 0 | Skew axis angle | | `alpha` | float | 1 | Opacity 0~1 | @@ -2556,21 +2556,21 @@ Child elements: `Glyph`* | `advance` | float | (required) | | `path` | string | - | | `image` | string | - | -| `offset` | point | 0,0 | +| `offset` | Point | 0,0 | #### SolidColor | Attribute | Type | Default | |-----------|------|---------| -| `color` | color | (required) | +| `color` | Color | (required) | #### LinearGradient | Attribute | Type | Default | |-----------|------|---------| -| `startPoint` | point | (required) | -| `endPoint` | point | (required) | -| `matrix` | string | identity matrix | +| `startPoint` | Point | (required) | +| `endPoint` | Point | (required) | +| `matrix` | Matrix | identity matrix | Child elements: `ColorStop`+ @@ -2578,9 +2578,9 @@ Child elements: `ColorStop`+ | Attribute | Type | Default | |-----------|------|---------| -| `center` | point | 0,0 | +| `center` | Point | 0,0 | | `radius` | float | (required) | -| `matrix` | string | identity matrix | +| `matrix` | Matrix | identity matrix | Child elements: `ColorStop`+ @@ -2588,10 +2588,10 @@ Child elements: `ColorStop`+ | Attribute | Type | Default | |-----------|------|---------| -| `center` | point | 0,0 | +| `center` | Point | 0,0 | | `startAngle` | float | 0 | | `endAngle` | float | 360 | -| `matrix` | string | identity matrix | +| `matrix` | Matrix | identity matrix | Child elements: `ColorStop`+ @@ -2599,9 +2599,9 @@ Child elements: `ColorStop`+ | Attribute | Type | Default | |-----------|------|---------| -| `center` | point | 0,0 | +| `center` | Point | 0,0 | | `radius` | float | (required) | -| `matrix` | string | identity matrix | +| `matrix` | Matrix | identity matrix | Child elements: `ColorStop`+ @@ -2610,7 +2610,7 @@ Child elements: `ColorStop`+ | Attribute | Type | Default | |-----------|------|---------| | `offset` | float | (required) | -| `color` | color | (required) | +| `color` | Color | (required) | #### ImagePattern @@ -2621,7 +2621,7 @@ Child elements: `ColorStop`+ | `tileModeY` | TileMode | clamp | | `filterMode` | FilterMode | linear | | `mipmapMode` | MipmapMode | linear | -| `matrix` | string | identity matrix | +| `matrix` | Matrix | identity matrix | ### C.3 Layer Node @@ -2635,14 +2635,14 @@ Child elements: `ColorStop`+ | `blendMode` | BlendMode | normal | | `x` | float | 0 | | `y` | float | 0 | -| `matrix` | string | identity matrix | -| `matrix3D` | string | - | +| `matrix` | Matrix | identity matrix | +| `matrix3D` | Matrix | - | | `preserve3D` | bool | false | | `antiAlias` | bool | true | | `groupOpacity` | bool | false | | `passThroughBackground` | bool | true | | `excludeChildEffectsInLayerStyle` | bool | false | -| `scrollRect` | string | - | +| `scrollRect` | Rect | - | | `mask` | idref | - | | `maskType` | MaskType | alpha | | `composition` | idref | - | @@ -2659,7 +2659,7 @@ Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* (autom | `offsetY` | float | 0 | | `blurX` | float | 0 | | `blurY` | float | 0 | -| `color` | color | #000000 | +| `color` | Color | #000000 | | `showBehindLayer` | bool | true | | `blendMode` | BlendMode | normal | @@ -2671,7 +2671,7 @@ Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* (autom | `offsetY` | float | 0 | | `blurX` | float | 0 | | `blurY` | float | 0 | -| `color` | color | #000000 | +| `color` | Color | #000000 | | `blendMode` | BlendMode | normal | #### BackgroundBlurStyle @@ -2701,7 +2701,7 @@ Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* (autom | `offsetY` | float | 0 | | `blurX` | float | 0 | | `blurY` | float | 0 | -| `color` | color | #000000 | +| `color` | Color | #000000 | | `shadowOnly` | bool | false | #### InnerShadowFilter @@ -2712,21 +2712,21 @@ Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* (autom | `offsetY` | float | 0 | | `blurX` | float | 0 | | `blurY` | float | 0 | -| `color` | color | #000000 | +| `color` | Color | #000000 | | `shadowOnly` | bool | false | #### BlendFilter | Attribute | Type | Default | |-----------|------|---------| -| `color` | color | (required) | +| `color` | Color | (required) | | `blendMode` | BlendMode | normal | #### ColorMatrixFilter | Attribute | Type | Default | |-----------|------|---------| -| `matrix` | string | (required) | +| `matrix` | Matrix | (required) | ### C.6 Geometry Element Nodes @@ -2734,8 +2734,8 @@ Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* (autom | Attribute | Type | Default | |-----------|------|---------| -| `center` | point | 0,0 | -| `size` | size | 100,100 | +| `center` | Point | 0,0 | +| `size` | Size | 100,100 | | `roundness` | float | 0 | | `reversed` | bool | false | @@ -2743,15 +2743,15 @@ Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* (autom | Attribute | Type | Default | |-----------|------|---------| -| `center` | point | 0,0 | -| `size` | size | 100,100 | +| `center` | Point | 0,0 | +| `size` | Size | 100,100 | | `reversed` | bool | false | #### Polystar | Attribute | Type | Default | |-----------|------|---------| -| `center` | point | 0,0 | +| `center` | Point | 0,0 | | `type` | PolystarType | star | | `pointCount` | float | 5 | | `outerRadius` | float | 100 | @@ -2773,7 +2773,7 @@ Child elements: `VectorElement`*, `LayerStyle`*, `LayerFilter`*, `Layer`* (autom | Attribute | Type | Default | |-----------|------|---------| | `text` | string | "" | -| `position` | point | 0,0 | +| `position` | Point | 0,0 | | `fontFamily` | string | system default | | `fontStyle` | string | "Regular" | | `fontSize` | float | 12 | @@ -2802,7 +2802,7 @@ Child elements: `CDATA` text, `GlyphRun`* | Attribute | Type | Default | |-----------|------|---------| -| `color` | color/idref | #000000 | +| `color` | Color/idref | #000000 | | `alpha` | float | 1 | | `blendMode` | BlendMode | normal | | `fillRule` | FillRule | winding | @@ -2812,7 +2812,7 @@ Child elements: `CDATA` text, `GlyphRun`* | Attribute | Type | Default | |-----------|------|---------| -| `color` | color/idref | #000000 | +| `color` | Color/idref | #000000 | | `width` | float | 1 | | `alpha` | float | 1 | | `blendMode` | BlendMode | normal | @@ -2853,15 +2853,15 @@ Child elements: `CDATA` text, `GlyphRun`* | Attribute | Type | Default | |-----------|------|---------| -| `anchorPoint` | point | 0,0 | -| `position` | point | 0,0 | +| `anchorPoint` | Point | 0,0 | +| `position` | Point | 0,0 | | `rotation` | float | 0 | -| `scale` | point | 1,1 | +| `scale` | Point | 1,1 | | `skew` | float | 0 | | `skewAxis` | float | 0 | | `alpha` | float | 1 | -| `fillColor` | color | - | -| `strokeColor` | color | - | +| `fillColor` | Color | - | +| `strokeColor` | Color | - | | `strokeWidth` | float | - | Child elements: `RangeSelector`* @@ -2897,7 +2897,7 @@ Child elements: `RangeSelector`* | Attribute | Type | Default | |-----------|------|---------| -| `position` | point | 0,0 | +| `position` | Point | 0,0 | | `width` | float | auto | | `height` | float | auto | | `textAlign` | TextAlign | start | @@ -2914,10 +2914,10 @@ Child elements: `RangeSelector`* | `copies` | float | 3 | | `offset` | float | 0 | | `order` | RepeaterOrder | belowOriginal | -| `anchorPoint` | point | 0,0 | -| `position` | point | 100,100 | +| `anchorPoint` | Point | 0,0 | +| `position` | Point | 100,100 | | `rotation` | float | 0 | -| `scale` | point | 1,1 | +| `scale` | Point | 1,1 | | `startAlpha` | float | 1 | | `endAlpha` | float | 1 | @@ -2925,10 +2925,10 @@ Child elements: `RangeSelector`* | Attribute | Type | Default | |-----------|------|---------| -| `anchorPoint` | point | 0,0 | -| `position` | point | 0,0 | +| `anchorPoint` | Point | 0,0 | +| `position` | Point | 0,0 | | `rotation` | float | 0 | -| `scale` | point | 1,1 | +| `scale` | Point | 1,1 | | `skew` | float | 0 | | `skewAxis` | float | 0 | | `alpha` | float | 1 | diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index bb09d5b7b5..7ab6c599aa 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -323,7 +323,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color | (必填) | 颜色值 | +| `color` | Color | (必填) | 颜色值 | ##### 线性渐变(LinearGradient) @@ -346,9 +346,9 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `startPoint` | point | (必填) | 起点 | -| `endPoint` | point | (必填) | 终点 | -| `matrix` | string | 单位矩阵 | 变换矩阵 | +| `startPoint` | Point | (必填) | 起点 | +| `endPoint` | Point | (必填) | 终点 | +| `matrix` | Matrix | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由 P 在起点-终点连线上的投影位置决定。 @@ -373,9 +373,9 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `center` | point | 0,0 | 中心点 | +| `center` | Point | 0,0 | 中心点 | | `radius` | float | (必填) | 渐变半径 | -| `matrix` | string | 单位矩阵 | 变换矩阵 | +| `matrix` | Matrix | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由 `distance(P, center) / radius` 决定。 @@ -402,10 +402,10 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `center` | point | 0,0 | 中心点 | +| `center` | Point | 0,0 | 中心点 | | `startAngle` | float | 0 | 起始角度 | | `endAngle` | float | 360 | 结束角度 | -| `matrix` | string | 单位矩阵 | 变换矩阵 | +| `matrix` | Matrix | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由 `atan2(P.y - center.y, P.x - center.x)` 在 `[startAngle, endAngle]` 范围内的比例决定。 @@ -430,9 +430,9 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `center` | point | 0,0 | 中心点 | +| `center` | Point | 0,0 | 中心点 | | `radius` | float | (必填) | 渐变半径 | -| `matrix` | string | 单位矩阵 | 变换矩阵 | +| `matrix` | Matrix | 单位矩阵 | 变换矩阵 | **计算**:对于点 P,其颜色由曼哈顿距离 `(|P.x - center.x| + |P.y - center.y|) / radius` 决定。 @@ -445,7 +445,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `offset` | float | (必填) | 位置 0.0~1.0 | -| `color` | color | (必填) | 色标颜色 | +| `color` | Color | (必填) | 色标颜色 | **渐变通用规则**: @@ -471,7 +471,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 | `tileModeY` | TileMode | clamp | Y 方向平铺模式 | | `filterMode` | FilterMode | linear | 纹理滤镜模式 | | `mipmapMode` | MipmapMode | linear | 多级渐远纹理模式 | -| `matrix` | string | 单位矩阵 | 变换矩阵 | +| `matrix` | Matrix | 单位矩阵 | 变换矩阵 | **TileMode(平铺模式)**:`clamp`(钳制)、`repeat`(重复)、`mirror`(镜像)、`decal`(贴花) @@ -605,7 +605,7 @@ Glyph 定义单个字形的渲染数据。`path` 和 `image` 二选一必填, |------|------|--------|------| | `path` | string | - | SVG 路径数据(矢量轮廓) | | `image` | string | - | 图片数据(base64 数据 URI)或外部文件路径 | -| `offset` | point | 0,0 | 字形偏移量,设计空间坐标(通常用于位图字形) | +| `offset` | Point | 0,0 | 字形偏移量,设计空间坐标(通常用于位图字形) | | `advance` | float | (必填) | 水平步进宽度,设计空间坐标 | **字形类型**: @@ -740,14 +740,14 @@ Layer 的子元素按类型自动归类为四个集合: | `blendMode` | BlendMode | normal | 混合模式 | | `x` | float | 0 | X 位置 | | `y` | float | 0 | Y 位置 | -| `matrix` | string | 单位矩阵 | 2D 变换 "a,b,c,d,tx,ty" | -| `matrix3D` | string | - | 3D 变换(16 个值,列优先) | +| `matrix` | Matrix | 单位矩阵 | 2D 变换 "a,b,c,d,tx,ty" | +| `matrix3D` | Matrix | - | 3D 变换(16 个值,列优先) | | `preserve3D` | bool | false | 保持 3D 变换 | | `antiAlias` | bool | true | 边缘抗锯齿 | | `groupOpacity` | bool | false | 组透明度 | | `passThroughBackground` | bool | true | 是否允许背景透传给子图层 | | `excludeChildEffectsInLayerStyle` | bool | false | 图层样式是否排除子图层效果 | -| `scrollRect` | string | - | 滚动裁剪区域 "x,y,w,h" | +| `scrollRect` | Rect | - | 滚动裁剪区域 "x,y,w,h" | | `mask` | idref | - | 遮罩图层引用 "@id" | | `maskType` | MaskType | alpha | 遮罩类型 | | `composition` | idref | - | 合成引用 "@id" | @@ -828,7 +828,7 @@ Layer 的子元素按类型自动归类为四个集合: | `offsetY` | float | 0 | Y 偏移 | | `blurX` | float | 0 | X 模糊半径 | | `blurY` | float | 0 | Y 模糊半径 | -| `color` | color | #000000 | 阴影颜色 | +| `color` | Color | #000000 | 阴影颜色 | | `showBehindLayer` | bool | true | 图层后面是否显示阴影 | **渲染步骤**: @@ -866,7 +866,7 @@ Layer 的子元素按类型自动归类为四个集合: | `offsetY` | float | 0 | Y 偏移 | | `blurX` | float | 0 | X 模糊半径 | | `blurY` | float | 0 | Y 模糊半径 | -| `color` | color | #000000 | 阴影颜色 | +| `color` | Color | #000000 | 阴影颜色 | **渲染步骤**: 1. 获取不透明图层内容并偏移 `(offsetX, offsetY)` @@ -910,7 +910,7 @@ Layer 的子元素按类型自动归类为四个集合: | `offsetY` | float | 0 | Y 偏移 | | `blurX` | float | 0 | X 模糊半径 | | `blurY` | float | 0 | Y 模糊半径 | -| `color` | color | #000000 | 阴影颜色 | +| `color` | Color | #000000 | 阴影颜色 | | `shadowOnly` | bool | false | 仅显示阴影 | **渲染步骤**: @@ -929,7 +929,7 @@ Layer 的子元素按类型自动归类为四个集合: | `offsetY` | float | 0 | Y 偏移 | | `blurX` | float | 0 | X 模糊半径 | | `blurY` | float | 0 | Y 模糊半径 | -| `color` | color | #000000 | 阴影颜色 | +| `color` | Color | #000000 | 阴影颜色 | | `shadowOnly` | bool | false | 仅显示阴影 | **渲染步骤**: @@ -944,7 +944,7 @@ Layer 的子元素按类型自动归类为四个集合: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color | (必填) | 混合颜色 | +| `color` | Color | (必填) | 混合颜色 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | #### 4.4.5 颜色矩阵滤镜(ColorMatrixFilter) @@ -953,7 +953,7 @@ Layer 的子元素按类型自动归类为四个集合: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `matrix` | string | (必填) | 4x5 颜色矩阵(20 个逗号分隔的浮点数) | +| `matrix` | Matrix | (必填) | 4x5 颜色矩阵(20 个逗号分隔的浮点数) | **矩阵格式**(20 个值,行优先): ``` @@ -1085,8 +1085,8 @@ VectorElement 按**文档顺序**依次处理,文档中靠前的元素先处 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `center` | point | 0,0 | 中心点 | -| `size` | size | 100,100 | 尺寸 "width,height" | +| `center` | Point | 0,0 | 中心点 | +| `size` | Size | 100,100 | 尺寸 "width,height" | | `roundness` | float | 0 | 圆角半径 | | `reversed` | bool | false | 反转路径方向 | @@ -1114,8 +1114,8 @@ rect.bottom = center.y + size.height / 2 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `center` | point | 0,0 | 中心点 | -| `size` | size | 100,100 | 尺寸 "width,height" | +| `center` | Point | 0,0 | 中心点 | +| `size` | Size | 100,100 | 尺寸 "width,height" | | `reversed` | bool | false | 反转路径方向 | **计算规则**: @@ -1138,7 +1138,7 @@ boundingRect.bottom = center.y + size.height / 2 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `center` | point | 0,0 | 中心点 | +| `center` | Point | 0,0 | 中心点 | | `type` | PolystarType | star | 类型(见下方) | | `pointCount` | float | 5 | 顶点数(支持小数) | | `outerRadius` | float | 100 | 外半径 | @@ -1209,7 +1209,7 @@ y = center.y + outerRadius * sin(angle) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `text` | string | "" | 文本内容 | -| `position` | point | 0,0 | 文本起点位置,y 为基线(可被 TextLayout 覆盖) | +| `position` | Point | 0,0 | 文本起点位置,y 为基线(可被 TextLayout 覆盖) | | `fontFamily` | string | 系统默认 | 字体族 | | `fontStyle` | string | "Regular" | 字体变体(Regular, Bold, Italic, Bold Italic 等) | | `fontSize` | float | 12 | 字号 | @@ -1363,7 +1363,7 @@ Matrix 是完整的 2D 仿射变换矩阵,六个分量 (a, b, c, d, tx, ty) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | +| `color` | Color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | | `fillRule` | FillRule | winding | 填充规则(见下方) | @@ -1415,7 +1415,7 @@ Matrix 是完整的 2D 仿射变换矩阵,六个分量 (a, b, c, d, tx, ty) | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `color` | color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | +| `color` | Color/idref | #000000 | 颜色值或颜色源引用,默认黑色 | | `width` | float | 1 | 描边宽度 | | `alpha` | float | 1 | 透明度 0~1 | | `blendMode` | BlendMode | normal | 混合模式(见 4.1 节) | @@ -1652,15 +1652,15 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `anchorPoint` | point | 0,0 | 锚点偏移,相对于字形默认锚点位置。每个字形的默认锚点位于 `(advance × 0.5, 0)`,即字形水平中心的基线位置 | -| `position` | point | 0,0 | 位置偏移 | +| `anchorPoint` | Point | 0,0 | 锚点偏移,相对于字形默认锚点位置。每个字形的默认锚点位于 `(advance × 0.5, 0)`,即字形水平中心的基线位置 | +| `position` | Point | 0,0 | 位置偏移 | | `rotation` | float | 0 | 旋转 | -| `scale` | point | 1,1 | 缩放 | +| `scale` | Point | 1,1 | 缩放 | | `skew` | float | 0 | 倾斜 | | `skewAxis` | float | 0 | 倾斜轴 | | `alpha` | float | 1 | 透明度 | -| `fillColor` | color | - | 填充颜色覆盖 | -| `strokeColor` | color | - | 描边颜色覆盖 | +| `fillColor` | Color | - | 填充颜色覆盖 | +| `strokeColor` | Color | - | 描边颜色覆盖 | | `strokeWidth` | float | - | 描边宽度覆盖 | **选择器计算**: @@ -1821,7 +1821,7 @@ TextLayout 是文本排版修改器,对累积的 Text 元素应用排版,会 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `position` | point | 0,0 | 排版原点 | +| `position` | Point | 0,0 | 排版原点 | | `width` | float | auto | 排版宽度(有值则自动换行) | | `height` | float | auto | 排版高度(有值则启用垂直对齐) | | `textAlign` | TextAlign | start | 水平对齐(见下方) | @@ -1897,10 +1897,10 @@ TextLayout 是文本排版修改器,对累积的 Text 元素应用排版,会 | `copies` | float | 3 | 副本数 | | `offset` | float | 0 | 起始偏移 | | `order` | RepeaterOrder | belowOriginal | 堆叠顺序(见下方) | -| `anchorPoint` | point | 0,0 | 锚点 | -| `position` | point | 100,100 | 每个副本的位置偏移 | +| `anchorPoint` | Point | 0,0 | 锚点 | +| `position` | Point | 100,100 | 每个副本的位置偏移 | | `rotation` | float | 0 | 每个副本的旋转 | -| `scale` | point | 1,1 | 每个副本的缩放 | +| `scale` | Point | 1,1 | 每个副本的缩放 | | `startAlpha` | float | 1 | 首个副本透明度 | | `endAlpha` | float | 1 | 末个副本透明度 | @@ -1980,10 +1980,10 @@ Group 是带变换属性的矢量元素容器。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `anchorPoint` | point | 0,0 | 锚点 "x,y" | -| `position` | point | 0,0 | 位置 "x,y" | +| `anchorPoint` | Point | 0,0 | 锚点 "x,y" | +| `position` | Point | 0,0 | 位置 "x,y" | | `rotation` | float | 0 | 旋转角度 | -| `scale` | point | 1,1 | 缩放 "sx,sy" | +| `scale` | Point | 1,1 | 缩放 "sx,sy" | | `skew` | float | 0 | 倾斜量 | | `skewAxis` | float | 0 | 倾斜轴角度 | | `alpha` | float | 1 | 透明度 0~1 | @@ -2555,22 +2555,22 @@ Layer / Group |------|------|--------| | `path` | string | - | | `image` | string | - | -| `offset` | point | 0,0 | +| `offset` | Point | 0,0 | | `advance` | float | (必填) | #### SolidColor | 属性 | 类型 | 默认值 | |------|------|--------| -| `color` | color | (必填) | +| `color` | Color | (必填) | #### LinearGradient | 属性 | 类型 | 默认值 | |------|------|--------| -| `startPoint` | point | (必填) | -| `endPoint` | point | (必填) | -| `matrix` | string | 单位矩阵 | +| `startPoint` | Point | (必填) | +| `endPoint` | Point | (必填) | +| `matrix` | Matrix | 单位矩阵 | 子元素:`ColorStop`+ @@ -2578,9 +2578,9 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `center` | point | 0,0 | +| `center` | Point | 0,0 | | `radius` | float | (必填) | -| `matrix` | string | 单位矩阵 | +| `matrix` | Matrix | 单位矩阵 | 子元素:`ColorStop`+ @@ -2588,10 +2588,10 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `center` | point | 0,0 | +| `center` | Point | 0,0 | | `startAngle` | float | 0 | | `endAngle` | float | 360 | -| `matrix` | string | 单位矩阵 | +| `matrix` | Matrix | 单位矩阵 | 子元素:`ColorStop`+ @@ -2599,9 +2599,9 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `center` | point | 0,0 | +| `center` | Point | 0,0 | | `radius` | float | (必填) | -| `matrix` | string | 单位矩阵 | +| `matrix` | Matrix | 单位矩阵 | 子元素:`ColorStop`+ @@ -2610,7 +2610,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| | `offset` | float | (必填) | -| `color` | color | (必填) | +| `color` | Color | (必填) | #### ImagePattern @@ -2621,7 +2621,7 @@ Layer / Group | `tileModeY` | TileMode | clamp | | `filterMode` | FilterMode | linear | | `mipmapMode` | MipmapMode | linear | -| `matrix` | string | 单位矩阵 | +| `matrix` | Matrix | 单位矩阵 | ### C.3 图层节点 @@ -2635,14 +2635,14 @@ Layer / Group | `blendMode` | BlendMode | normal | | `x` | float | 0 | | `y` | float | 0 | -| `matrix` | string | 单位矩阵 | -| `matrix3D` | string | - | +| `matrix` | Matrix | 单位矩阵 | +| `matrix3D` | Matrix | - | | `preserve3D` | bool | false | | `antiAlias` | bool | true | | `groupOpacity` | bool | false | | `passThroughBackground` | bool | true | | `excludeChildEffectsInLayerStyle` | bool | false | -| `scrollRect` | string | - | +| `scrollRect` | Rect | - | | `mask` | idref | - | | `maskType` | MaskType | alpha | | `composition` | idref | - | @@ -2659,7 +2659,7 @@ Layer / Group | `offsetY` | float | 0 | | `blurX` | float | 0 | | `blurY` | float | 0 | -| `color` | color | #000000 | +| `color` | Color | #000000 | | `showBehindLayer` | bool | true | | `blendMode` | BlendMode | normal | @@ -2671,7 +2671,7 @@ Layer / Group | `offsetY` | float | 0 | | `blurX` | float | 0 | | `blurY` | float | 0 | -| `color` | color | #000000 | +| `color` | Color | #000000 | | `blendMode` | BlendMode | normal | #### BackgroundBlurStyle @@ -2701,7 +2701,7 @@ Layer / Group | `offsetY` | float | 0 | | `blurX` | float | 0 | | `blurY` | float | 0 | -| `color` | color | #000000 | +| `color` | Color | #000000 | | `shadowOnly` | bool | false | #### InnerShadowFilter @@ -2712,21 +2712,21 @@ Layer / Group | `offsetY` | float | 0 | | `blurX` | float | 0 | | `blurY` | float | 0 | -| `color` | color | #000000 | +| `color` | Color | #000000 | | `shadowOnly` | bool | false | #### BlendFilter | 属性 | 类型 | 默认值 | |------|------|--------| -| `color` | color | (必填) | +| `color` | Color | (必填) | | `blendMode` | BlendMode | normal | #### ColorMatrixFilter | 属性 | 类型 | 默认值 | |------|------|--------| -| `matrix` | string | (必填) | +| `matrix` | Matrix | (必填) | ### C.6 几何元素节点 @@ -2734,8 +2734,8 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `center` | point | 0,0 | -| `size` | size | 100,100 | +| `center` | Point | 0,0 | +| `size` | Size | 100,100 | | `roundness` | float | 0 | | `reversed` | bool | false | @@ -2743,15 +2743,15 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `center` | point | 0,0 | -| `size` | size | 100,100 | +| `center` | Point | 0,0 | +| `size` | Size | 100,100 | | `reversed` | bool | false | #### Polystar | 属性 | 类型 | 默认值 | |------|------|--------| -| `center` | point | 0,0 | +| `center` | Point | 0,0 | | `type` | PolystarType | star | | `pointCount` | float | 5 | | `outerRadius` | float | 100 | @@ -2773,7 +2773,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| | `text` | string | "" | -| `position` | point | 0,0 | +| `position` | Point | 0,0 | | `fontFamily` | string | 系统默认 | | `fontStyle` | string | "Regular" | | `fontSize` | float | 12 | @@ -2802,7 +2802,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `color` | color/idref | #000000 | +| `color` | Color/idref | #000000 | | `alpha` | float | 1 | | `blendMode` | BlendMode | normal | | `fillRule` | FillRule | winding | @@ -2812,7 +2812,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `color` | color/idref | #000000 | +| `color` | Color/idref | #000000 | | `width` | float | 1 | | `alpha` | float | 1 | | `blendMode` | BlendMode | normal | @@ -2853,15 +2853,15 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `anchorPoint` | point | 0,0 | -| `position` | point | 0,0 | +| `anchorPoint` | Point | 0,0 | +| `position` | Point | 0,0 | | `rotation` | float | 0 | -| `scale` | point | 1,1 | +| `scale` | Point | 1,1 | | `skew` | float | 0 | | `skewAxis` | float | 0 | | `alpha` | float | 1 | -| `fillColor` | color | - | -| `strokeColor` | color | - | +| `fillColor` | Color | - | +| `strokeColor` | Color | - | | `strokeWidth` | float | - | 子元素:`RangeSelector`* @@ -2897,7 +2897,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `position` | point | 0,0 | +| `position` | Point | 0,0 | | `width` | float | auto | | `height` | float | auto | | `textAlign` | TextAlign | start | @@ -2914,10 +2914,10 @@ Layer / Group | `copies` | float | 3 | | `offset` | float | 0 | | `order` | RepeaterOrder | belowOriginal | -| `anchorPoint` | point | 0,0 | -| `position` | point | 100,100 | +| `anchorPoint` | Point | 0,0 | +| `position` | Point | 100,100 | | `rotation` | float | 0 | -| `scale` | point | 1,1 | +| `scale` | Point | 1,1 | | `startAlpha` | float | 1 | | `endAlpha` | float | 1 | @@ -2925,10 +2925,10 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `anchorPoint` | point | 0,0 | -| `position` | point | 0,0 | +| `anchorPoint` | Point | 0,0 | +| `position` | Point | 0,0 | | `rotation` | float | 0 | -| `scale` | point | 1,1 | +| `scale` | Point | 1,1 | | `skew` | float | 0 | | `skewAxis` | float | 0 | | `alpha` | float | 1 | From c8b53473722ad4ccac21234c9b682c96c17bf1da Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 21:58:12 +0800 Subject: [PATCH 354/678] Simplify anchorPoint attribute to anchor in PAGX spec and samples. --- pagx/spec/pagx_spec.md | 32 ++++++++++----------- pagx/spec/pagx_spec.zh_CN.md | 32 ++++++++++----------- pagx/spec/samples/5.7_group.pagx | 2 +- pagx/spec/samples/B.1_complete_example.pagx | 2 +- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index de9e6b3638..6129c4f3bb 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -1646,7 +1646,7 @@ Text Element Shape Modifier Subsequent Modifiers Applies transforms and style overrides to glyphs within selected ranges. TextModifier may contain multiple RangeSelector child elements. ```xml - + @@ -1654,7 +1654,7 @@ Applies transforms and style overrides to glyphs within selected ranges. TextMod | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `anchorPoint` | Point | 0,0 | Anchor point offset, relative to glyph's default anchor position. Each glyph's default anchor is at `(advance × 0.5, 0)`, the horizontal center at baseline | +| `anchor` | Point | 0,0 | Anchor point offset, relative to glyph's default anchor position. Each glyph's default anchor is at `(advance × 0.5, 0)`, the horizontal center at baseline | | `position` | Point | 0,0 | Position offset | | `rotation` | float | 0 | Rotation | | `scale` | Point | 1,1 | Scale | @@ -1678,11 +1678,11 @@ The `factor` calculated by the selector ranges from [-1, 1] and controls the deg factor = clamp(selectorFactor × weight, -1, 1) // Position and rotation: apply factor linearly -transform = translate(-anchorPoint × factor) +transform = translate(-anchor × factor) × scale(1 + (scale - 1) × factor) // Scale interpolates from 1 to target value × skew(skew × factor, skewAxis) × rotate(rotation × factor) - × translate(anchorPoint × factor) + × translate(anchor × factor) × translate(position × factor) // Opacity: use absolute value of factor @@ -1891,7 +1891,7 @@ Rich text is achieved through multiple Text elements within a Group, each Text h Repeater duplicates accumulated content and rendered styles, applying progressive transforms to each copy. Repeater affects both Paths and glyph lists simultaneously, and does not trigger text-to-shape conversion. ```xml - + ``` | Attribute | Type | Default | Description | @@ -1899,7 +1899,7 @@ Repeater duplicates accumulated content and rendered styles, applying progressiv | `copies` | float | 3 | Number of copies | | `offset` | float | 0 | Start offset | | `order` | RepeaterOrder | belowOriginal | Stacking order | -| `anchorPoint` | Point | 0,0 | Anchor point | +| `anchor` | Point | 0,0 | Anchor point | | `position` | Point | 100,100 | Position offset per copy | | `rotation` | float | 0 | Rotation per copy | | `scale` | Point | 1,1 | Scale per copy | @@ -1909,11 +1909,11 @@ Repeater duplicates accumulated content and rendered styles, applying progressiv **Transform Calculation** (i-th copy, i starts from 0): ``` progress = i + offset -matrix = translate(-anchorPoint) +matrix = translate(-anchor) × scale(scale^progress) // Exponential scaling × rotate(rotation × progress) // Linear rotation × translate(position × progress) // Linear translation - × translate(anchorPoint) + × translate(anchor) ``` **Opacity Interpolation**: @@ -1972,7 +1972,7 @@ Group is a VectorElement container with transform properties. - + @@ -1982,7 +1982,7 @@ Group is a VectorElement container with transform properties. | Attribute | Type | Default | Description | |-----------|------|---------|-------------| -| `anchorPoint` | Point | 0,0 | Anchor point "x,y" | +| `anchor` | Point | 0,0 | Anchor point "x,y" | | `position` | Point | 0,0 | Position "x,y" | | `rotation` | float | 0 | Rotation angle | | `scale` | Point | 1,1 | Scale "sx,sy" | @@ -1994,7 +1994,7 @@ Group is a VectorElement container with transform properties. Transforms are applied in the following order: -1. Translate to negative anchor point (`translate(-anchorPoint)`) +1. Translate to negative anchor point (`translate(-anchor)`) 2. Scale (`scale`) 3. Skew (`skew` along `skewAxis` direction) 4. Rotate (`rotation`) @@ -2002,7 +2002,7 @@ Transforms are applied in the following order: **Transform Matrix**: ``` -M = translate(position) × rotate(rotation) × skew(skew, skewAxis) × scale(scale) × translate(-anchorPoint) +M = translate(position) × rotate(rotation) × skew(skew, skewAxis) × scale(scale) × translate(-anchor) ``` **Skew Transform**: @@ -2332,7 +2332,7 @@ The following example covers all major node types in PAGX, demonstrating complet - + @@ -2853,7 +2853,7 @@ Child elements: `CDATA` text, `GlyphRun`* | Attribute | Type | Default | |-----------|------|---------| -| `anchorPoint` | Point | 0,0 | +| `anchor` | Point | 0,0 | | `position` | Point | 0,0 | | `rotation` | float | 0 | | `scale` | Point | 1,1 | @@ -2914,7 +2914,7 @@ Child elements: `RangeSelector`* | `copies` | float | 3 | | `offset` | float | 0 | | `order` | RepeaterOrder | belowOriginal | -| `anchorPoint` | Point | 0,0 | +| `anchor` | Point | 0,0 | | `position` | Point | 100,100 | | `rotation` | float | 0 | | `scale` | Point | 1,1 | @@ -2925,7 +2925,7 @@ Child elements: `RangeSelector`* | Attribute | Type | Default | |-----------|------|---------| -| `anchorPoint` | Point | 0,0 | +| `anchor` | Point | 0,0 | | `position` | Point | 0,0 | | `rotation` | float | 0 | | `scale` | Point | 1,1 | diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index 7ab6c599aa..e158091d67 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -1644,7 +1644,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: 对选定范围内的字形应用变换和样式覆盖。TextModifier 可包含多个 RangeSelector 子元素,用于定义不同的选择范围和影响因子。 ```xml - + @@ -1652,7 +1652,7 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `anchorPoint` | Point | 0,0 | 锚点偏移,相对于字形默认锚点位置。每个字形的默认锚点位于 `(advance × 0.5, 0)`,即字形水平中心的基线位置 | +| `anchor` | Point | 0,0 | 锚点偏移,相对于字形默认锚点位置。每个字形的默认锚点位于 `(advance × 0.5, 0)`,即字形水平中心的基线位置 | | `position` | Point | 0,0 | 位置偏移 | | `rotation` | float | 0 | 旋转 | | `scale` | Point | 1,1 | 缩放 | @@ -1676,11 +1676,11 @@ Fill 和 Stroke 的 `placement` 属性控制相对于子图层的绘制顺序: factor = clamp(selectorFactor × weight, -1, 1) // 位置和旋转:线性应用 factor -transform = translate(-anchorPoint × factor) +transform = translate(-anchor × factor) × scale(1 + (scale - 1) × factor) // 缩放从 1 插值到目标值 × skew(skew × factor, skewAxis) × rotate(rotation × factor) - × translate(anchorPoint × factor) + × translate(anchor × factor) × translate(position × factor) // 透明度:使用 factor 的绝对值 @@ -1889,7 +1889,7 @@ TextLayout 是文本排版修改器,对累积的 Text 元素应用排版,会 复制累积的内容和已渲染的样式,对每个副本应用渐进变换。Repeater 对 Path 和字形列表同时生效,且不会触发文本转形状。 ```xml - + ``` | 属性 | 类型 | 默认值 | 说明 | @@ -1897,7 +1897,7 @@ TextLayout 是文本排版修改器,对累积的 Text 元素应用排版,会 | `copies` | float | 3 | 副本数 | | `offset` | float | 0 | 起始偏移 | | `order` | RepeaterOrder | belowOriginal | 堆叠顺序(见下方) | -| `anchorPoint` | Point | 0,0 | 锚点 | +| `anchor` | Point | 0,0 | 锚点 | | `position` | Point | 100,100 | 每个副本的位置偏移 | | `rotation` | float | 0 | 每个副本的旋转 | | `scale` | Point | 1,1 | 每个副本的缩放 | @@ -1907,11 +1907,11 @@ TextLayout 是文本排版修改器,对累积的 Text 元素应用排版,会 **变换计算**(第 i 个副本,i 从 0 开始): ``` progress = i + offset -matrix = translate(-anchorPoint) +matrix = translate(-anchor) × scale(scale^progress) // 指数缩放 × rotate(rotation × progress) // 线性旋转 × translate(position × progress) // 线性位移 - × translate(anchorPoint) + × translate(anchor) ``` **透明度插值**: @@ -1970,7 +1970,7 @@ Group 是带变换属性的矢量元素容器。 - + @@ -1980,7 +1980,7 @@ Group 是带变换属性的矢量元素容器。 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `anchorPoint` | Point | 0,0 | 锚点 "x,y" | +| `anchor` | Point | 0,0 | 锚点 "x,y" | | `position` | Point | 0,0 | 位置 "x,y" | | `rotation` | float | 0 | 旋转角度 | | `scale` | Point | 1,1 | 缩放 "sx,sy" | @@ -1992,7 +1992,7 @@ Group 是带变换属性的矢量元素容器。 变换按以下顺序应用(后应用的变换先计算): -1. 平移到锚点的负方向(`translate(-anchorPoint)`) +1. 平移到锚点的负方向(`translate(-anchor)`) 2. 缩放(`scale`) 3. 倾斜(`skew` 沿 `skewAxis` 方向) 4. 旋转(`rotation`) @@ -2000,7 +2000,7 @@ Group 是带变换属性的矢量元素容器。 **变换矩阵**: ``` -M = translate(position) × rotate(rotation) × skew(skew, skewAxis) × scale(scale) × translate(-anchorPoint) +M = translate(position) × rotate(rotation) × skew(skew, skewAxis) × scale(scale) × translate(-anchor) ``` **倾斜变换**: @@ -2330,7 +2330,7 @@ Layer / Group - + @@ -2853,7 +2853,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `anchorPoint` | Point | 0,0 | +| `anchor` | Point | 0,0 | | `position` | Point | 0,0 | | `rotation` | float | 0 | | `scale` | Point | 1,1 | @@ -2914,7 +2914,7 @@ Layer / Group | `copies` | float | 3 | | `offset` | float | 0 | | `order` | RepeaterOrder | belowOriginal | -| `anchorPoint` | Point | 0,0 | +| `anchor` | Point | 0,0 | | `position` | Point | 100,100 | | `rotation` | float | 0 | | `scale` | Point | 1,1 | @@ -2925,7 +2925,7 @@ Layer / Group | 属性 | 类型 | 默认值 | |------|------|--------| -| `anchorPoint` | Point | 0,0 | +| `anchor` | Point | 0,0 | | `position` | Point | 0,0 | | `rotation` | float | 0 | | `scale` | Point | 1,1 | diff --git a/pagx/spec/samples/5.7_group.pagx b/pagx/spec/samples/5.7_group.pagx index bc665d6b56..22a3d7ac7e 100644 --- a/pagx/spec/samples/5.7_group.pagx +++ b/pagx/spec/samples/5.7_group.pagx @@ -8,7 +8,7 @@ - + diff --git a/pagx/spec/samples/B.1_complete_example.pagx b/pagx/spec/samples/B.1_complete_example.pagx index 0bc3081d3c..ce67aa660a 100644 --- a/pagx/spec/samples/B.1_complete_example.pagx +++ b/pagx/spec/samples/B.1_complete_example.pagx @@ -120,7 +120,7 @@ - + From 6dd90eaa750fb8d47c7e1e36fa711f68174a9349 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 22:05:15 +0800 Subject: [PATCH 355/678] Rename anchorPoint to anchor in C++ code and PAGX Importer/Exporter to match spec. --- pagx/include/pagx/nodes/Group.h | 2 +- pagx/include/pagx/nodes/Repeater.h | 2 +- pagx/include/pagx/nodes/TextModifier.h | 2 +- pagx/src/PAGXExporter.cpp | 12 ++++++------ pagx/src/PAGXImporter.cpp | 12 ++++++------ pagx/src/tgfx/LayerBuilder.cpp | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pagx/include/pagx/nodes/Group.h b/pagx/include/pagx/nodes/Group.h index e2e29b19f7..7ae78aa442 100644 --- a/pagx/include/pagx/nodes/Group.h +++ b/pagx/include/pagx/nodes/Group.h @@ -35,7 +35,7 @@ class Group : public Element { /** * The anchor point for transformations. */ - Point anchorPoint = {}; + Point anchor = {}; /** * The position offset of the group. diff --git a/pagx/include/pagx/nodes/Repeater.h b/pagx/include/pagx/nodes/Repeater.h index 714fc505f6..bf897b15c0 100644 --- a/pagx/include/pagx/nodes/Repeater.h +++ b/pagx/include/pagx/nodes/Repeater.h @@ -50,7 +50,7 @@ class Repeater : public Element { /** * The anchor point for transformations. */ - Point anchorPoint = {}; + Point anchor = {}; /** * The position offset applied between each copy. The default value is {100, 100}. diff --git a/pagx/include/pagx/nodes/TextModifier.h b/pagx/include/pagx/nodes/TextModifier.h index 13cc945aa3..748e32ed2e 100644 --- a/pagx/include/pagx/nodes/TextModifier.h +++ b/pagx/include/pagx/nodes/TextModifier.h @@ -38,7 +38,7 @@ class TextModifier : public Element { /** * The anchor point for transformations. */ - Point anchorPoint = {}; + Point anchor = {}; /** * The position offset applied to selected characters. diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index aa52973323..1144ef89ef 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -666,8 +666,8 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, const Optio case NodeType::TextModifier: { auto modifier = static_cast(node); xml.openElement("TextModifier"); - if (modifier->anchorPoint.x != 0 || modifier->anchorPoint.y != 0) { - xml.addAttribute("anchorPoint", pointToString(modifier->anchorPoint)); + if (modifier->anchor.x != 0 || modifier->anchor.y != 0) { + xml.addAttribute("anchor", pointToString(modifier->anchor)); } if (modifier->position.x != 0 || modifier->position.y != 0) { xml.addAttribute("position", pointToString(modifier->position)); @@ -770,8 +770,8 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, const Optio if (repeater->order != RepeaterOrder::BelowOriginal) { xml.addAttribute("order", RepeaterOrderToString(repeater->order)); } - if (repeater->anchorPoint.x != 0 || repeater->anchorPoint.y != 0) { - xml.addAttribute("anchorPoint", pointToString(repeater->anchorPoint)); + if (repeater->anchor.x != 0 || repeater->anchor.y != 0) { + xml.addAttribute("anchor", pointToString(repeater->anchor)); } if (repeater->position.x != 100 || repeater->position.y != 100) { xml.addAttribute("position", pointToString(repeater->position)); @@ -788,8 +788,8 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, const Optio case NodeType::Group: { auto group = static_cast(node); xml.openElement("Group"); - if (group->anchorPoint.x != 0 || group->anchorPoint.y != 0) { - xml.addAttribute("anchorPoint", pointToString(group->anchorPoint)); + if (group->anchor.x != 0 || group->anchor.y != 0) { + xml.addAttribute("anchor", pointToString(group->anchor)); } if (group->position.x != 0 || group->position.y != 0) { xml.addAttribute("position", pointToString(group->position)); diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 42fe585879..3075803240 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -899,8 +899,8 @@ static TextModifier* parseTextModifier(const XMLNode* node, PAGXDocument* doc) { if (!modifier) { return nullptr; } - auto anchorStr = getAttribute(node, "anchorPoint", "0,0"); - modifier->anchorPoint = parsePoint(anchorStr); + auto anchorStr = getAttribute(node, "anchor", "0,0"); + modifier->anchor = parsePoint(anchorStr); auto positionStr = getAttribute(node, "position", "0,0"); modifier->position = parsePoint(positionStr); modifier->rotation = getFloatAttribute(node, "rotation", 0); @@ -984,8 +984,8 @@ static Repeater* parseRepeater(const XMLNode* node, PAGXDocument* doc) { repeater->copies = getFloatAttribute(node, "copies", 3); repeater->offset = getFloatAttribute(node, "offset", 0); repeater->order = RepeaterOrderFromString(getAttribute(node, "order", "belowOriginal")); - auto anchorStr = getAttribute(node, "anchorPoint", "0,0"); - repeater->anchorPoint = parsePoint(anchorStr); + auto anchorStr = getAttribute(node, "anchor", "0,0"); + repeater->anchor = parsePoint(anchorStr); auto positionStr = getAttribute(node, "position", "100,100"); repeater->position = parsePoint(positionStr); repeater->rotation = getFloatAttribute(node, "rotation", 0); @@ -1002,8 +1002,8 @@ static Group* parseGroup(const XMLNode* node, PAGXDocument* doc) { return nullptr; } // group->name (removed) = getAttribute(node, "name"); - auto anchorStr = getAttribute(node, "anchorPoint", "0,0"); - group->anchorPoint = parsePoint(anchorStr); + auto anchorStr = getAttribute(node, "anchor", "0,0"); + group->anchor = parsePoint(anchorStr); auto positionStr = getAttribute(node, "position", "0,0"); group->position = parsePoint(positionStr); group->rotation = getFloatAttribute(node, "rotation", 0); diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index cda4d1a89a..2be93b441b 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -779,7 +779,7 @@ class LayerBuilderImpl { repeater->setCopies(node->copies); repeater->setOffset(node->offset); repeater->setOrder(static_cast(node->order)); - repeater->setAnchorPoint(ToTGFX(node->anchorPoint)); + repeater->setAnchorPoint(ToTGFX(node->anchor)); repeater->setPosition(ToTGFX(node->position)); repeater->setRotation(node->rotation); repeater->setScale(ToTGFX(node->scale)); @@ -807,8 +807,8 @@ class LayerBuilderImpl { group->setElements(elements); // Apply transform properties - if (node->anchorPoint.x != 0 || node->anchorPoint.y != 0) { - group->setAnchorPoint(ToTGFX(node->anchorPoint)); + if (node->anchor.x != 0 || node->anchor.y != 0) { + group->setAnchorPoint(ToTGFX(node->anchor)); } if (node->position.x != 0 || node->position.y != 0) { group->setPosition(ToTGFX(node->position)); From a0819357ecbff1df76a80d92fe617c65fdc511b3 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 22:34:23 +0800 Subject: [PATCH 356/678] Redesign GlyphRun with split transform attributes and anchor-based transforms in PAGX spec. --- pagx/spec/pagx_spec.md | 186 +++++++++++++++++++++++----------- pagx/spec/pagx_spec.zh_CN.md | 188 +++++++++++++++++++++++------------ 2 files changed, 253 insertions(+), 121 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 6129c4f3bb..9c8802b356 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -1252,74 +1252,138 @@ GlyphRun defines pre-layout data for a group of glyphs, each GlyphRun independen | `font` | idref | (required) | Font resource reference `@id` | | `fontSize` | float | 12 | Rendering font size. Actual scale = `fontSize / font.unitsPerEm` | | `glyphs` | string | (required) | GlyphID sequence, comma-separated (0 means missing glyph) | -| `x` | float | 0 | Starting x coordinate (Default mode only) | -| `y` | float | 0 | Shared y coordinate (Default and Horizontal modes) | -| `xPositions` | string | - | x coordinate sequence, comma-separated (Horizontal mode) | -| `positions` | string | - | (x,y) coordinate sequence, semicolon-separated (Point mode) | -| `xforms` | string | - | RSXform sequence (scos,ssin,tx,ty), semicolon-separated (RSXform mode) | -| `matrices` | string | - | Matrix sequence (a,b,c,d,tx,ty), semicolon-separated (Matrix mode) | +| `x` | float | 0 | Overall X offset | +| `y` | float | 0 | Overall Y offset | +| `xOffsets` | string | - | Per-glyph X offset, comma-separated | +| `positions` | string | - | Per-glyph (x,y) offset, semicolon-separated | +| `anchors` | string | - | Per-glyph anchor offset (x,y), semicolon-separated. Default anchor is (advance×0.5, 0) | +| `scales` | string | - | Per-glyph scale (sx,sy), semicolon-separated. Default 1,1 | +| `rotations` | string | - | Per-glyph rotation angle (degrees), comma-separated. Default 0 | +| `skews` | string | - | Per-glyph skew angle (degrees), comma-separated. Default 0 | -**Positioning Mode Selection** (priority from high to low): -1. Has `matrices` → Matrix mode: Each glyph has full 2D affine transform -2. Has `xforms` → RSXform mode: Each glyph has rotation+scale+translation (path text) -3. Has `positions` → Point mode: Each glyph has independent (x,y) position (multi-line/complex layout) -4. Has `xPositions` → Horizontal mode: Each glyph has x coordinate, sharing `y` value (single-line horizontal text) -5. None of the above → Default mode: First glyph starts at `(x, y)`, subsequent glyphs are positioned by accumulating each Glyph's `advance` attribute in Font (most compact format) +**Attribute Stacking**: -**RSXform**: -RSXform is a compressed rotation+scale matrix with four components (scos, ssin, tx, ty): -``` -| scos -ssin tx | -| ssin scos ty | -| 0 0 1 | -``` -Where scos = scale × cos(angle), ssin = scale × sin(angle). +Position attributes `x`, `y`, `xOffsets`, `positions` are not mutually exclusive and can be combined. Final position is calculated as: -**Matrix**: -Matrix is a full 2D affine transformation matrix with six components (a, b, c, d, tx, ty): ``` -| a c tx | -| b d ty | -| 0 0 1 | +finalX[i] = x + xOffsets[i] + positions[i].x +finalY[i] = y + positions[i].y ``` +- When `xOffsets` is not specified, `xOffsets[i]` is treated as 0 +- When `positions` is not specified, `positions[i]` is treated as (0, 0) +- When neither `xOffsets` nor `positions` is specified: First glyph positioned at (x, y), subsequent glyphs positioned by accumulating each Glyph's `advance` attribute + +**Transform Application Order**: + +When a glyph has scale, rotation, or skew transforms, they are applied in the following order (consistent with TextModifier): + +1. Translate to anchor (`translate(-anchor)`) +2. Scale (`scale`) +3. Skew (`skew`, along vertical axis) +4. Rotate (`rotation`) +5. Translate back from anchor (`translate(anchor)`) +6. Translate to position (`translate(position)`) + +**Anchor Description**: + +- Each glyph's **default anchor** is at `(advance × 0.5, 0)`, the horizontal center at baseline +- The `anchors` attribute records offsets relative to the default anchor, final anchor = default anchor + anchors[i] + **Pre-layout Examples**: ```xml - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + +``` + +**Pre-layout Example with Transforms** (path text scenario): + +```xml + + + + + + + + + + + + + + + + + + + +``` + +**Example with Scale and Skew**: + +```xml + + + + + + + + + + + + + + + + + + + ``` ### 5.3 Painters @@ -2791,10 +2855,12 @@ Child elements: `CDATA` text, `GlyphRun`* | `glyphs` | string | (required) | | `x` | float | 0 | | `y` | float | 0 | -| `xPositions` | string | - | +| `xOffsets` | string | - | | `positions` | string | - | -| `xforms` | string | - | -| `matrices` | string | - | +| `anchors` | string | - | +| `scales` | string | - | +| `rotations` | string | - | +| `skews` | string | - | ### C.7 Painter Nodes diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index e158091d67..bec2f722e1 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -1252,74 +1252,138 @@ GlyphRun 定义一组字形的预排版数据,每个 GlyphRun 独立引用一 | `font` | idref | (必填) | 引用 Font 资源 `@id` | | `fontSize` | float | 12 | 渲染字号。实际缩放比例 = `fontSize / font.unitsPerEm` | | `glyphs` | string | (必填) | GlyphID 序列,逗号分隔(0 表示缺失字形) | -| `x` | float | 0 | 起始 x 坐标(仅 Default 模式) | -| `y` | float | 0 | 共享 y 坐标(Default 和 Horizontal 模式) | -| `xPositions` | string | - | x 坐标序列,逗号分隔(Horizontal 模式) | -| `positions` | string | - | (x,y) 坐标序列,分号分隔(Point 模式) | -| `xforms` | string | - | RSXform 序列 (scos,ssin,tx,ty),分号分隔(RSXform 模式) | -| `matrices` | string | - | Matrix 序列 (a,b,c,d,tx,ty),分号分隔(Matrix 模式) | +| `x` | float | 0 | 总体 X 偏移 | +| `y` | float | 0 | 总体 Y 偏移 | +| `xOffsets` | string | - | 每字形 X 偏移,逗号分隔 | +| `positions` | string | - | 每字形 (x,y) 偏移,分号分隔 | +| `anchors` | string | - | 每字形锚点偏移 (x,y),分号分隔。默认锚点为 (advance×0.5, 0) | +| `scales` | string | - | 每字形缩放 (sx,sy),分号分隔。默认 1,1 | +| `rotations` | string | - | 每字形旋转角度(度),逗号分隔。默认 0 | +| `skews` | string | - | 每字形斜切角度(度),逗号分隔。默认 0 | -**定位模式选择**(优先级从高到低): -1. 有 `matrices` → Matrix 模式:每个字形有完整 2D 仿射变换 -2. 有 `xforms` → RSXform 模式:每个字形有旋转+缩放+平移(路径文本) -3. 有 `positions` → Point 模式:每个字形有独立 (x,y) 位置(多行/复杂布局) -4. 有 `xPositions` → Horizontal 模式:每个字形有 x 坐标,共享 `y` 值(单行水平文本) -5. 以上都没有 → Default 模式:首个字形起始于 `(x, y)`,后续字形依次累加 Font 中每个 Glyph 的 `advance` 属性(最简洁格式) +**属性叠加关系**: -**RSXform 说明**: -RSXform 是压缩的旋转+缩放矩阵,四个分量 (scos, ssin, tx, ty) 表示: -``` -| scos -ssin tx | -| ssin scos ty | -| 0 0 1 | -``` -其中 scos = scale × cos(angle),ssin = scale × sin(angle)。 +位置属性 `x`、`y`、`xOffsets`、`positions` 不互斥,可叠加使用。最终位置计算如下: -**Matrix 说明**: -Matrix 是完整的 2D 仿射变换矩阵,六个分量 (a, b, c, d, tx, ty) 表示: ``` -| a c tx | -| b d ty | -| 0 0 1 | +finalX[i] = x + xOffsets[i] + positions[i].x +finalY[i] = y + positions[i].y ``` +- 未指定 `xOffsets` 时,`xOffsets[i]` 视为 0 +- 未指定 `positions` 时,`positions[i]` 视为 (0, 0) +- 不指定 `xOffsets` 和 `positions` 时:首个字形位于 (x, y),后续字形依次累加 advance + +**变换应用顺序**: + +当字形有 scale、rotation 或 skew 变换时,按以下顺序应用(与 TextModifier 一致): + +1. 平移到锚点(`translate(-anchor)`) +2. 缩放(`scale`) +3. 斜切(`skew`,沿垂直轴方向) +4. 旋转(`rotation`) +5. 平移回锚点(`translate(anchor)`) +6. 平移到位置(`translate(position)`) + +**锚点说明**: + +- 每个字形的**默认锚点**位于 `(advance × 0.5, 0)`,即字形水平中心的基线位置 +- `anchors` 属性记录的是相对于默认锚点的偏移,最终锚点 = 默认锚点 + anchors[i] + **预排版示例**: ```xml - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + +``` + +**带变换的预排版示例**(路径文本场景): + +```xml + + + + + + + + + + + + + + + + + + + +``` + +**带缩放和斜切的示例**: + +```xml + + + + + + + + + + + + + + + + + + + ``` ### 5.3 绘制器(Painters) @@ -2406,7 +2470,7 @@ Layer / Group - + @@ -2791,10 +2855,12 @@ Layer / Group | `glyphs` | string | (必填) | | `x` | float | 0 | | `y` | float | 0 | -| `xPositions` | string | - | +| `xOffsets` | string | - | | `positions` | string | - | -| `xforms` | string | - | -| `matrices` | string | - | +| `anchors` | string | - | +| `scales` | string | - | +| `rotations` | string | - | +| `skews` | string | - | ### C.7 绘制器节点 From b45043544e2761153e4b25a6db690b18e3cc20a6 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 22:34:37 +0800 Subject: [PATCH 357/678] Implement split GlyphRun attributes with anchor-based transforms. --- pagx/include/pagx/nodes/GlyphRun.h | 47 +++++----- pagx/src/MathUtil.h | 9 ++ pagx/src/PAGXExporter.cpp | 84 +++++++++-------- pagx/src/PAGXImporter.cpp | 83 +++++++++-------- pagx/src/tgfx/FontEmbedder.cpp | 142 +++++++++++++++++++++++++---- pagx/src/tgfx/Typesetter.cpp | 109 ++++++++++++++++++---- test/src/PAGXTest.cpp | 26 +++--- 7 files changed, 347 insertions(+), 153 deletions(-) diff --git a/pagx/include/pagx/nodes/GlyphRun.h b/pagx/include/pagx/nodes/GlyphRun.h index 44b5f4085a..f172a5af74 100644 --- a/pagx/include/pagx/nodes/GlyphRun.h +++ b/pagx/include/pagx/nodes/GlyphRun.h @@ -21,26 +21,11 @@ #include #include #include -#include "pagx/types/Matrix.h" #include "pagx/nodes/Node.h" #include "pagx/types/Point.h" namespace pagx { -/** - * RSXform represents a compressed rotation+scale matrix with four components (scos, ssin, tx, ty): - * | scos -ssin tx | - * | ssin scos ty | - * | 0 0 1 | - * where scos = scale × cos(angle), ssin = scale × sin(angle). - */ -struct RSXform { - float scos = 1.0f; - float ssin = 0.0f; - float tx = 0.0f; - float ty = 0.0f; -}; - class Font; /** @@ -65,34 +50,48 @@ class GlyphRun : public Node { std::vector glyphs = {}; /** - * Starting x coordinate for Default positioning mode. The default value is 0. + * Overall X offset. The default value is 0. */ float x = 0.0f; /** - * Shared y coordinate for Default and Horizontal positioning modes. The default value is 0. + * Overall Y offset. The default value is 0. */ float y = 0.0f; /** - * X coordinates for Horizontal positioning mode (each glyph has x, shares y). + * Per-glyph X offsets. Can be combined with positions. + * Final X = x + xOffsets[i] + positions[i].x */ - std::vector xPositions = {}; + std::vector xOffsets = {}; /** - * (x, y) coordinates for Point positioning mode. + * Per-glyph (x, y) offsets. Can be combined with x, y, and xOffsets. + * Final X = x + xOffsets[i] + positions[i].x + * Final Y = y + positions[i].y */ std::vector positions = {}; /** - * RSXform transforms for RSXform positioning mode (path text). + * Per-glyph anchor point offsets relative to the default anchor (advance * 0.5, 0). + * The anchor point affects the center of scale and rotation transforms. + */ + std::vector anchors = {}; + + /** + * Per-glyph scale factors (scaleX, scaleY). Default is (1, 1). + */ + std::vector scales = {}; + + /** + * Per-glyph rotation angles in degrees. Default is 0. */ - std::vector xforms = {}; + std::vector rotations = {}; /** - * Full 2D affine matrices for Matrix positioning mode. + * Per-glyph skew angles in degrees (along vertical axis). Default is 0. */ - std::vector matrices = {}; + std::vector skews = {}; NodeType nodeType() const override { return NodeType::GlyphRun; diff --git a/pagx/src/MathUtil.h b/pagx/src/MathUtil.h index 110b1a2bb2..1773ecca5f 100644 --- a/pagx/src/MathUtil.h +++ b/pagx/src/MathUtil.h @@ -23,9 +23,18 @@ namespace pagx { static constexpr float FloatNearlyZero = 1.0f / (1 << 12); +static constexpr float Pi = 3.14159265358979323846f; inline bool FloatNearlyEqual(float x, float y) { return std::abs(x - y) <= FloatNearlyZero; } +inline float DegreesToRadians(float degrees) { + return degrees * Pi / 180.0f; +} + +inline float RadiansToDegrees(float radians) { + return radians * 180.0f / Pi; +} + } // namespace pagx diff --git a/pagx/src/PAGXExporter.cpp b/pagx/src/PAGXExporter.cpp index 1144ef89ef..a7c9dabfdd 100644 --- a/pagx/src/PAGXExporter.cpp +++ b/pagx/src/PAGXExporter.cpp @@ -492,53 +492,65 @@ static void writeVectorElement(XMLBuilder& xml, const Element* node, const Optio xml.addRequiredAttribute("glyphs", glyphsStr); } - // Determine positioning mode - if (!run->matrices.empty()) { - // Matrix mode: semicolon-separated groups of 6 values - std::string matStr = {}; + // Write x/y overall offsets (only if non-zero) + xml.addAttribute("x", run->x, 0.0f); + xml.addAttribute("y", run->y, 0.0f); + + // Write xOffsets (comma-separated) + if (!run->xOffsets.empty()) { + xml.addRequiredAttribute("xOffsets", floatListToString(run->xOffsets)); + } + + // Write positions (semicolon-separated x,y pairs) + if (!run->positions.empty()) { + std::string posStr = {}; char buf[32] = {}; - for (size_t i = 0; i < run->matrices.size(); i++) { + for (size_t i = 0; i < run->positions.size(); i++) { if (i > 0) { - matStr += ";"; + posStr += ";"; } - const auto& m = run->matrices[i]; - snprintf(buf, sizeof(buf), "%g,%g,%g,%g,%g,%g", m.a, m.b, m.c, m.d, m.tx, m.ty); - matStr += buf; + snprintf(buf, sizeof(buf), "%g,%g", run->positions[i].x, run->positions[i].y); + posStr += buf; } - xml.addRequiredAttribute("matrices", matStr); - } else if (!run->xforms.empty()) { - // RSXform mode: semicolon-separated groups of 4 values - std::string xformsStr = {}; - char buf[64] = {}; - for (size_t i = 0; i < run->xforms.size(); i++) { + xml.addRequiredAttribute("positions", posStr); + } + + // Write anchors (semicolon-separated x,y pairs) + if (!run->anchors.empty()) { + std::string anchorsStr = {}; + char buf[32] = {}; + for (size_t i = 0; i < run->anchors.size(); i++) { if (i > 0) { - xformsStr += ";"; + anchorsStr += ";"; } - const auto& x = run->xforms[i]; - snprintf(buf, sizeof(buf), "%g,%g,%g,%g", x.scos, x.ssin, x.tx, x.ty); - xformsStr += buf; + snprintf(buf, sizeof(buf), "%g,%g", run->anchors[i].x, run->anchors[i].y); + anchorsStr += buf; } - xml.addRequiredAttribute("xforms", xformsStr); - } else if (!run->positions.empty()) { - // Point mode: semicolon-separated x,y pairs - std::string posStr = {}; + xml.addRequiredAttribute("anchors", anchorsStr); + } + + // Write scales (semicolon-separated sx,sy pairs) + if (!run->scales.empty()) { + std::string scalesStr = {}; char buf[32] = {}; - for (size_t i = 0; i < run->positions.size(); i++) { + for (size_t i = 0; i < run->scales.size(); i++) { if (i > 0) { - posStr += ";"; + scalesStr += ";"; } - snprintf(buf, sizeof(buf), "%g,%g", run->positions[i].x, run->positions[i].y); - posStr += buf; + snprintf(buf, sizeof(buf), "%g,%g", run->scales[i].x, run->scales[i].y); + scalesStr += buf; } - xml.addRequiredAttribute("positions", posStr); - } else if (!run->xPositions.empty()) { - // Horizontal mode - xml.addAttribute("y", run->y); - xml.addRequiredAttribute("xPositions", floatListToString(run->xPositions)); - } else { - // Default mode: use x/y as starting point - xml.addAttribute("x", run->x); - xml.addAttribute("y", run->y); + xml.addRequiredAttribute("scales", scalesStr); + } + + // Write rotations (comma-separated angles in degrees) + if (!run->rotations.empty()) { + xml.addRequiredAttribute("rotations", floatListToString(run->rotations)); + } + + // Write skews (comma-separated angles in degrees) + if (!run->skews.empty()) { + xml.addRequiredAttribute("skews", floatListToString(run->skews)); } xml.closeElementSelfClosing(); diff --git a/pagx/src/PAGXImporter.cpp b/pagx/src/PAGXImporter.cpp index 3075803240..8e3b312ebc 100644 --- a/pagx/src/PAGXImporter.cpp +++ b/pagx/src/PAGXImporter.cpp @@ -1316,16 +1316,15 @@ static GlyphRun* parseGlyphRun(const XMLNode* node, PAGXDocument* doc) { } } - // Parse xPositions (comma-separated x coordinates for Horizontal mode) - auto xPosStr = getAttribute(node, "xPositions"); - if (!xPosStr.empty()) { - run->xPositions = parseFloatList(xPosStr); + // Parse xOffsets (comma-separated x offsets) + auto xOffsetsStr = getAttribute(node, "xOffsets"); + if (!xOffsetsStr.empty()) { + run->xOffsets = parseFloatList(xOffsetsStr); } - // Parse positions (semicolon-separated x,y pairs for Point mode) + // Parse positions (semicolon-separated x,y pairs) auto posStr = getAttribute(node, "positions"); if (!posStr.empty()) { - // Split by semicolon size_t start = 0; size_t end = posStr.find(';'); while (start < posStr.size()) { @@ -1347,66 +1346,66 @@ static GlyphRun* parseGlyphRun(const XMLNode* node, PAGXDocument* doc) { } } - // Parse xforms (semicolon-separated scos,ssin,tx,ty for RSXform mode) - auto xformsStr = getAttribute(node, "xforms"); - if (!xformsStr.empty()) { + // Parse anchors (semicolon-separated x,y pairs) + auto anchorsStr = getAttribute(node, "anchors"); + if (!anchorsStr.empty()) { size_t start = 0; - size_t end = xformsStr.find(';'); - while (start < xformsStr.size()) { - std::string group = {}; + size_t end = anchorsStr.find(';'); + while (start < anchorsStr.size()) { + std::string pair = {}; if (end == std::string::npos) { - group = xformsStr.substr(start); + pair = anchorsStr.substr(start); } else { - group = xformsStr.substr(start, end - start); + pair = anchorsStr.substr(start, end - start); } - auto vals = ParseFloatList(group); - if (vals.size() >= 4) { - RSXform xform = {}; - xform.scos = vals[0]; - xform.ssin = vals[1]; - xform.tx = vals[2]; - xform.ty = vals[3]; - run->xforms.push_back(xform); + auto coords = ParseFloatList(pair); + if (coords.size() >= 2) { + run->anchors.push_back({coords[0], coords[1]}); } if (end == std::string::npos) { break; } start = end + 1; - end = xformsStr.find(';', start); + end = anchorsStr.find(';', start); } } - // Parse matrices (semicolon-separated a,b,c,d,tx,ty for Matrix mode) - auto matricesStr = getAttribute(node, "matrices"); - if (!matricesStr.empty()) { + // Parse scales (semicolon-separated sx,sy pairs) + auto scalesStr = getAttribute(node, "scales"); + if (!scalesStr.empty()) { size_t start = 0; - size_t end = matricesStr.find(';'); - while (start < matricesStr.size()) { - std::string group = {}; + size_t end = scalesStr.find(';'); + while (start < scalesStr.size()) { + std::string pair = {}; if (end == std::string::npos) { - group = matricesStr.substr(start); + pair = scalesStr.substr(start); } else { - group = matricesStr.substr(start, end - start); + pair = scalesStr.substr(start, end - start); } - auto vals = ParseFloatList(group); - if (vals.size() >= 6) { - Matrix m = {}; - m.a = vals[0]; - m.b = vals[1]; - m.c = vals[2]; - m.d = vals[3]; - m.tx = vals[4]; - m.ty = vals[5]; - run->matrices.push_back(m); + auto coords = ParseFloatList(pair); + if (coords.size() >= 2) { + run->scales.push_back({coords[0], coords[1]}); } if (end == std::string::npos) { break; } start = end + 1; - end = matricesStr.find(';', start); + end = scalesStr.find(';', start); } } + // Parse rotations (comma-separated angles in degrees) + auto rotationsStr = getAttribute(node, "rotations"); + if (!rotationsStr.empty()) { + run->rotations = parseFloatList(rotationsStr); + } + + // Parse skews (comma-separated angles in degrees) + auto skewsStr = getAttribute(node, "skews"); + if (!skewsStr.empty()) { + run->skews = parseFloatList(skewsStr); + } + return run; } diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 56eaea4306..970537e715 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -18,8 +18,9 @@ #include "pagx/FontEmbedder.h" #include +#include #include -#include "MathUtil.h" +#include "../MathUtil.h" #include "SVGPathParser.h" #include "pagx/nodes/Font.h" #include "pagx/nodes/Image.h" @@ -306,11 +307,13 @@ static GlyphRun* CreateGlyphRunForIndices( return glyphRun; } + float advanceScale = fontSize / static_cast(font->unitsPerEm); + switch (run.positioning) { case tgfx::GlyphPositioning::Horizontal: { glyphRun->y = run.offsetY; for (size_t i : indices) { - glyphRun->xPositions.push_back(run.positions[i]); + glyphRun->xOffsets.push_back(run.positions[i]); } break; } @@ -322,28 +325,131 @@ static GlyphRun* CreateGlyphRunForIndices( break; } case tgfx::GlyphPositioning::RSXform: { + // Decompose RSXform into position, scale, and rotation auto* xforms = reinterpret_cast(run.positions); - for (size_t i : indices) { - RSXform xform = {}; - xform.scos = xforms[i].scos; - xform.ssin = xforms[i].ssin; - xform.tx = xforms[i].tx; - xform.ty = xforms[i].ty; - glyphRun->xforms.push_back(xform); + bool hasNonDefaultScale = false; + bool hasNonDefaultRotation = false; + + // First pass: check if we have non-default values + for (size_t idx = 0; idx < indices.size(); idx++) { + size_t i = indices[idx]; + const auto& xform = xforms[i]; + float scale = std::sqrt(xform.scos * xform.scos + xform.ssin * xform.ssin); + float rotation = RadiansToDegrees(std::atan2(xform.ssin, xform.scos)); + if (!FloatNearlyEqual(scale, 1.0f)) { + hasNonDefaultScale = true; + } + if (!FloatNearlyEqual(rotation, 0.0f)) { + hasNonDefaultRotation = true; + } + } + + // Second pass: extract values + for (size_t idx = 0; idx < indices.size(); idx++) { + size_t i = indices[idx]; + const auto& xform = xforms[i]; + + // Get glyph advance for anchor calculation + float glyphAdvance = 0.0f; + if (glyphRun->glyphs[idx] > 0 && glyphRun->glyphs[idx] <= font->glyphs.size()) { + glyphAdvance = font->glyphs[glyphRun->glyphs[idx] - 1]->advance * advanceScale; + } + float defaultAnchorX = glyphAdvance * 0.5f; + + // Decompose: scale = sqrt(scos^2 + ssin^2), rotation = atan2(ssin, scos) + float scale = std::sqrt(xform.scos * xform.scos + xform.ssin * xform.ssin); + float rotation = RadiansToDegrees(std::atan2(xform.ssin, xform.scos)); + + // The RSXform position (tx, ty) is the position after the anchor transform + // We need to reverse-calculate the original position + // Original transform: translate(-anchor) -> scale -> rotate -> translate(anchor) -> translate(pos) + // RSXform encodes: scale*rotate matrix + final position + // So tx, ty = anchor + pos - rotatedScaledAnchor + // pos = tx - anchor + rotatedScaledAnchor - anchor... this is complex + // For simplicity, just store position as tx, ty (the final transformed position) + glyphRun->positions.push_back({xform.tx, xform.ty}); + + if (hasNonDefaultScale) { + glyphRun->scales.push_back({scale, scale}); + } + if (hasNonDefaultRotation) { + glyphRun->rotations.push_back(rotation); + } } break; } case tgfx::GlyphPositioning::Matrix: { + // Decompose full matrix into position, scale, rotation, skew auto* matrices = reinterpret_cast(run.positions); - for (size_t i : indices) { - Matrix m = {}; - m.a = matrices[i * 6 + 0]; - m.b = matrices[i * 6 + 1]; - m.c = matrices[i * 6 + 2]; - m.d = matrices[i * 6 + 3]; - m.tx = matrices[i * 6 + 4]; - m.ty = matrices[i * 6 + 5]; - glyphRun->matrices.push_back(m); + bool hasNonDefaultScale = false; + bool hasNonDefaultRotation = false; + bool hasNonDefaultSkew = false; + + // First pass: check if we have non-default values + for (size_t idx = 0; idx < indices.size(); idx++) { + size_t i = indices[idx]; + float a = matrices[i * 6 + 0]; + float b = matrices[i * 6 + 1]; + float c = matrices[i * 6 + 2]; + float d = matrices[i * 6 + 3]; + + // Decompose matrix: M = T * R * S * Skew + // For a uniform scale + rotation matrix: a = s*cos, b = s*sin, c = -s*sin, d = s*cos + float scaleX = std::sqrt(a * a + b * b); + float scaleY = std::sqrt(c * c + d * d); + float rotation = RadiansToDegrees(std::atan2(b, a)); + + // Check for skew (non-orthogonal matrix) + float dotProduct = a * c + b * d; + float skew = 0.0f; + if (scaleX > 0.001f && scaleY > 0.001f) { + skew = RadiansToDegrees(std::atan2(dotProduct, scaleX * scaleY)); + } + + if (!FloatNearlyEqual(scaleX, 1.0f) || !FloatNearlyEqual(scaleY, 1.0f)) { + hasNonDefaultScale = true; + } + if (!FloatNearlyEqual(rotation, 0.0f)) { + hasNonDefaultRotation = true; + } + if (!FloatNearlyEqual(skew, 0.0f)) { + hasNonDefaultSkew = true; + } + } + + // Second pass: extract values + for (size_t idx = 0; idx < indices.size(); idx++) { + size_t i = indices[idx]; + float a = matrices[i * 6 + 0]; + float b = matrices[i * 6 + 1]; + float c = matrices[i * 6 + 2]; + float d = matrices[i * 6 + 3]; + float tx = matrices[i * 6 + 4]; + float ty = matrices[i * 6 + 5]; + + // Store position + glyphRun->positions.push_back({tx, ty}); + + // Decompose matrix + float scaleX = std::sqrt(a * a + b * b); + float scaleY = std::sqrt(c * c + d * d); + float rotation = RadiansToDegrees(std::atan2(b, a)); + + float dotProduct = a * c + b * d; + float skew = 0.0f; + if (scaleX > 0.001f && scaleY > 0.001f) { + skew = RadiansToDegrees(std::atan2(dotProduct, scaleX * scaleY)); + } + + if (hasNonDefaultScale) { + glyphRun->scales.push_back({scaleX, scaleY}); + } + if (hasNonDefaultRotation) { + glyphRun->rotations.push_back(rotation); + } + if (hasNonDefaultSkew) { + glyphRun->skews.push_back(skew); + } } break; } diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index a4419cc452..9dd298ee2e 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -332,39 +332,108 @@ class TypesetterContext { tgfx::Font font(typeface, fontSizeForTypeface); size_t count = run->glyphs.size(); - // Determine positioning mode (priority: matrices > xforms > positions > xPositions > Default) - if (!run->matrices.empty() && run->matrices.size() >= count) { + // Check if we need matrix mode (has transforms) + bool hasTransforms = !run->scales.empty() || !run->rotations.empty() || !run->skews.empty() || + !run->anchors.empty(); + + if (hasTransforms) { + // Matrix mode: compute transform matrix for each glyph auto& buffer = builder.allocRunMatrix(font, count); memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); auto* matrices = reinterpret_cast(buffer.positions); + + float advanceScale = run->fontSize / static_cast(unitsPerEm); + for (size_t i = 0; i < count; i++) { - const auto& m = run->matrices[i]; - matrices[i * 6 + 0] = m.a; - matrices[i * 6 + 1] = m.b; - matrices[i * 6 + 2] = m.c; - matrices[i * 6 + 3] = m.d; - matrices[i * 6 + 4] = m.tx; - matrices[i * 6 + 5] = m.ty; - } - } else if (!run->xforms.empty() && run->xforms.size() >= count) { - auto& buffer = builder.allocRunRSXform(font, count); - memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); - auto* xforms = reinterpret_cast(buffer.positions); - for (size_t i = 0; i < count; i++) { - const auto& x = run->xforms[i]; - xforms[i] = tgfx::RSXform::Make(x.scos, x.ssin, x.tx, x.ty); + // Get glyph advance for default anchor calculation + float glyphAdvance = 0.0f; + if (run->font != nullptr && run->glyphs[i] > 0 && + run->glyphs[i] <= run->font->glyphs.size()) { + glyphAdvance = run->font->glyphs[run->glyphs[i] - 1]->advance * advanceScale; + } + + // Calculate position: x + xOffsets[i] + positions[i].x, y + positions[i].y + float posX = run->x; + float posY = run->y; + if (i < run->xOffsets.size()) { + posX += run->xOffsets[i]; + } + if (i < run->positions.size()) { + posX += run->positions[i].x; + posY += run->positions[i].y; + } + + // Default anchor: (advance * 0.5, 0) + float anchorX = glyphAdvance * 0.5f; + float anchorY = 0.0f; + if (i < run->anchors.size()) { + anchorX += run->anchors[i].x; + anchorY += run->anchors[i].y; + } + + // Get scale, rotation, skew + float scaleX = 1.0f, scaleY = 1.0f; + if (i < run->scales.size()) { + scaleX = run->scales[i].x; + scaleY = run->scales[i].y; + } + float rotation = 0.0f; + if (i < run->rotations.size()) { + rotation = run->rotations[i]; + } + float skew = 0.0f; + if (i < run->skews.size()) { + skew = run->skews[i]; + } + + // Build transform matrix following TextModifier order: + // 1. translate(-anchor) + // 2. scale + // 3. skew (along vertical axis) + // 4. rotation + // 5. translate(anchor) + // 6. translate(position) + tgfx::Matrix m = tgfx::Matrix::I(); + m.postTranslate(-anchorX, -anchorY); + m.postScale(scaleX, scaleY); + if (skew != 0.0f) { + float skewRad = DegreesToRadians(skew); + m.postSkew(std::tan(skewRad), 0.0f); + } + if (rotation != 0.0f) { + m.postRotate(rotation); + } + m.postTranslate(anchorX, anchorY); + m.postTranslate(posX, posY); + + // Store matrix (a, b, c, d, tx, ty) + matrices[i * 6 + 0] = m.getScaleX(); + matrices[i * 6 + 1] = m.getSkewY(); + matrices[i * 6 + 2] = m.getSkewX(); + matrices[i * 6 + 3] = m.getScaleY(); + matrices[i * 6 + 4] = m.getTranslateX(); + matrices[i * 6 + 5] = m.getTranslateY(); } } else if (!run->positions.empty() && run->positions.size() >= count) { + // Point mode: each glyph has (x, y) offset combined with overall x/y auto& buffer = builder.allocRunPos(font, count); memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); auto* positions = reinterpret_cast(buffer.positions); for (size_t i = 0; i < count; i++) { - positions[i] = tgfx::Point::Make(run->positions[i].x, run->positions[i].y); + float posX = run->x + run->positions[i].x; + float posY = run->y + run->positions[i].y; + if (i < run->xOffsets.size()) { + posX += run->xOffsets[i]; + } + positions[i] = tgfx::Point::Make(posX, posY); } - } else if (!run->xPositions.empty() && run->xPositions.size() >= count) { + } else if (!run->xOffsets.empty() && run->xOffsets.size() >= count) { + // Horizontal mode: x offsets + shared y auto& buffer = builder.allocRunPosH(font, count, run->y); memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); - memcpy(buffer.positions, run->xPositions.data(), count * sizeof(float)); + for (size_t i = 0; i < count; i++) { + buffer.positions[i] = run->x + run->xOffsets[i]; + } } else { // Default mode: use font's advance values to position glyphs auto& buffer = builder.allocRun(font, count, run->x, run->y); diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index e26810c1fc..625ba9d899 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -767,12 +767,12 @@ PAG_TEST(PAGXTest, GlyphRunHorizontal) { glyphRun->font = font; glyphRun->glyphs = {1, 2, 1}; glyphRun->y = 50; - glyphRun->xPositions = {10, 60, 110}; + glyphRun->xOffsets = {10, 60, 110}; EXPECT_EQ(glyphRun->nodeType(), pagx::NodeType::GlyphRun); EXPECT_EQ(glyphRun->font, font); EXPECT_EQ(glyphRun->glyphs.size(), 3u); - EXPECT_EQ(glyphRun->xPositions.size(), 3u); + EXPECT_EQ(glyphRun->xOffsets.size(), 3u); EXPECT_FLOAT_EQ(glyphRun->y, 50.0f); } @@ -794,23 +794,23 @@ PAG_TEST(PAGXTest, GlyphRunPointPositions) { } /** - * Test case: GlyphRun node with RSXform positioning + * Test case: GlyphRun node with rotations and scales */ -PAG_TEST(PAGXTest, GlyphRunRSXform) { +PAG_TEST(PAGXTest, GlyphRunTransforms) { auto doc = pagx::PAGXDocument::Make(0, 0); auto font = doc->makeNode("testFont"); auto glyphRun = doc->makeNode(); glyphRun->font = font; glyphRun->glyphs = {1, 2}; - - pagx::RSXform xform1 = {1, 0, 10, 20}; - pagx::RSXform xform2 = {0.707f, 0.707f, 60, 80}; - glyphRun->xforms = {xform1, xform2}; + glyphRun->positions = {{10, 20}, {60, 80}}; + glyphRun->rotations = {0, 45}; + glyphRun->scales = {{1, 1}, {1.5f, 1.5f}}; EXPECT_EQ(glyphRun->nodeType(), pagx::NodeType::GlyphRun); - EXPECT_EQ(glyphRun->xforms.size(), 2u); - EXPECT_FLOAT_EQ(glyphRun->xforms[0].scos, 1.0f); - EXPECT_FLOAT_EQ(glyphRun->xforms[1].ssin, 0.707f); + EXPECT_EQ(glyphRun->rotations.size(), 2u); + EXPECT_FLOAT_EQ(glyphRun->rotations[1], 45.0f); + EXPECT_EQ(glyphRun->scales.size(), 2u); + EXPECT_FLOAT_EQ(glyphRun->scales[1].x, 1.5f); } /** @@ -826,7 +826,7 @@ PAG_TEST(PAGXTest, TextPrecomposed) { glyphRun->font = font; glyphRun->glyphs = {1, 2, 3}; glyphRun->y = 24; - glyphRun->xPositions = {0, 24, 48}; + glyphRun->xOffsets = {0, 24, 48}; text->glyphRuns.push_back(glyphRun); EXPECT_EQ(text->nodeType(), pagx::NodeType::Text); @@ -1050,7 +1050,7 @@ PAG_TEST(PAGXTest, FontGlyphRoundTrip) { glyphRun->font = font; glyphRun->glyphs = {1, 2}; glyphRun->y = 60; - glyphRun->xPositions = {10, 60}; + glyphRun->xOffsets = {10, 60}; text->glyphRuns.push_back(glyphRun); auto fill = doc->makeNode(); From b30cb03549e6ca6745a2dc7bd7ca6b7cc0a885ed Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 22:37:33 +0800 Subject: [PATCH 358/678] Remove transform processing from Typesetter as it should be handled by tgfx layer. --- pagx/src/tgfx/Typesetter.cpp | 86 ++---------------------------------- 1 file changed, 4 insertions(+), 82 deletions(-) diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index 9dd298ee2e..a6ca6a634b 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -332,89 +332,11 @@ class TypesetterContext { tgfx::Font font(typeface, fontSizeForTypeface); size_t count = run->glyphs.size(); - // Check if we need matrix mode (has transforms) - bool hasTransforms = !run->scales.empty() || !run->rotations.empty() || !run->skews.empty() || - !run->anchors.empty(); + // Note: scales, rotations, skews, anchors are NOT processed here. + // These transform attributes should be handled by tgfx layer (similar to TextModifier). + // Typesetter only computes position information for TextBlob. - if (hasTransforms) { - // Matrix mode: compute transform matrix for each glyph - auto& buffer = builder.allocRunMatrix(font, count); - memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); - auto* matrices = reinterpret_cast(buffer.positions); - - float advanceScale = run->fontSize / static_cast(unitsPerEm); - - for (size_t i = 0; i < count; i++) { - // Get glyph advance for default anchor calculation - float glyphAdvance = 0.0f; - if (run->font != nullptr && run->glyphs[i] > 0 && - run->glyphs[i] <= run->font->glyphs.size()) { - glyphAdvance = run->font->glyphs[run->glyphs[i] - 1]->advance * advanceScale; - } - - // Calculate position: x + xOffsets[i] + positions[i].x, y + positions[i].y - float posX = run->x; - float posY = run->y; - if (i < run->xOffsets.size()) { - posX += run->xOffsets[i]; - } - if (i < run->positions.size()) { - posX += run->positions[i].x; - posY += run->positions[i].y; - } - - // Default anchor: (advance * 0.5, 0) - float anchorX = glyphAdvance * 0.5f; - float anchorY = 0.0f; - if (i < run->anchors.size()) { - anchorX += run->anchors[i].x; - anchorY += run->anchors[i].y; - } - - // Get scale, rotation, skew - float scaleX = 1.0f, scaleY = 1.0f; - if (i < run->scales.size()) { - scaleX = run->scales[i].x; - scaleY = run->scales[i].y; - } - float rotation = 0.0f; - if (i < run->rotations.size()) { - rotation = run->rotations[i]; - } - float skew = 0.0f; - if (i < run->skews.size()) { - skew = run->skews[i]; - } - - // Build transform matrix following TextModifier order: - // 1. translate(-anchor) - // 2. scale - // 3. skew (along vertical axis) - // 4. rotation - // 5. translate(anchor) - // 6. translate(position) - tgfx::Matrix m = tgfx::Matrix::I(); - m.postTranslate(-anchorX, -anchorY); - m.postScale(scaleX, scaleY); - if (skew != 0.0f) { - float skewRad = DegreesToRadians(skew); - m.postSkew(std::tan(skewRad), 0.0f); - } - if (rotation != 0.0f) { - m.postRotate(rotation); - } - m.postTranslate(anchorX, anchorY); - m.postTranslate(posX, posY); - - // Store matrix (a, b, c, d, tx, ty) - matrices[i * 6 + 0] = m.getScaleX(); - matrices[i * 6 + 1] = m.getSkewY(); - matrices[i * 6 + 2] = m.getSkewX(); - matrices[i * 6 + 3] = m.getScaleY(); - matrices[i * 6 + 4] = m.getTranslateX(); - matrices[i * 6 + 5] = m.getTranslateY(); - } - } else if (!run->positions.empty() && run->positions.size() >= count) { + if (!run->positions.empty() && run->positions.size() >= count) { // Point mode: each glyph has (x, y) offset combined with overall x/y auto& buffer = builder.allocRunPos(font, count); memcpy(buffer.glyphs, run->glyphs.data(), count * sizeof(tgfx::GlyphID)); From 06432dc4710fb92cd4c29c8aec5ff78f2b1e6d86 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 22:38:23 +0800 Subject: [PATCH 359/678] Add comments clarifying that scale, rotation, and skew transforms are applied around the anchor point. --- pagx/include/pagx/nodes/GlyphRun.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pagx/include/pagx/nodes/GlyphRun.h b/pagx/include/pagx/nodes/GlyphRun.h index f172a5af74..af4c1b778a 100644 --- a/pagx/include/pagx/nodes/GlyphRun.h +++ b/pagx/include/pagx/nodes/GlyphRun.h @@ -74,22 +74,25 @@ class GlyphRun : public Node { /** * Per-glyph anchor point offsets relative to the default anchor (advance * 0.5, 0). - * The anchor point affects the center of scale and rotation transforms. + * The anchor is the center point for scale, rotation, and skew transforms. */ std::vector anchors = {}; /** * Per-glyph scale factors (scaleX, scaleY). Default is (1, 1). + * Scaling is applied around the anchor point. */ std::vector scales = {}; /** * Per-glyph rotation angles in degrees. Default is 0. + * Rotation is applied around the anchor point. */ std::vector rotations = {}; /** * Per-glyph skew angles in degrees (along vertical axis). Default is 0. + * Skewing is applied around the anchor point. */ std::vector skews = {}; From 9da1e22c6f51c7af12338ca6b681cdb65b1507a6 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 5 Feb 2026 22:40:25 +0800 Subject: [PATCH 360/678] Improve GlyphRun attribute descriptions to clarify anchor-based transforms and attribute stacking. --- pagx/spec/pagx_spec.md | 14 +++++++------- pagx/spec/pagx_spec.zh_CN.md | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pagx/spec/pagx_spec.md b/pagx/spec/pagx_spec.md index 9c8802b356..2eff547efa 100644 --- a/pagx/spec/pagx_spec.md +++ b/pagx/spec/pagx_spec.md @@ -1256,14 +1256,14 @@ GlyphRun defines pre-layout data for a group of glyphs, each GlyphRun independen | `y` | float | 0 | Overall Y offset | | `xOffsets` | string | - | Per-glyph X offset, comma-separated | | `positions` | string | - | Per-glyph (x,y) offset, semicolon-separated | -| `anchors` | string | - | Per-glyph anchor offset (x,y), semicolon-separated. Default anchor is (advance×0.5, 0) | -| `scales` | string | - | Per-glyph scale (sx,sy), semicolon-separated. Default 1,1 | -| `rotations` | string | - | Per-glyph rotation angle (degrees), comma-separated. Default 0 | -| `skews` | string | - | Per-glyph skew angle (degrees), comma-separated. Default 0 | +| `anchors` | string | - | Per-glyph anchor offset (x,y), semicolon-separated. The anchor is the center point for scale, rotation, and skew transforms. Default anchor is (advance×0.5, 0) | +| `scales` | string | - | Per-glyph scale (sx,sy), semicolon-separated. Scaling is applied around the anchor point. Default 1,1 | +| `rotations` | string | - | Per-glyph rotation angle (degrees), comma-separated. Rotation is applied around the anchor point. Default 0 | +| `skews` | string | - | Per-glyph skew angle (degrees), comma-separated. Skewing is applied around the anchor point. Default 0 | -**Attribute Stacking**: +All attributes are optional and can be combined. When an attribute array is shorter than the glyph count, missing values use defaults. -Position attributes `x`, `y`, `xOffsets`, `positions` are not mutually exclusive and can be combined. Final position is calculated as: +**Position Calculation**: ``` finalX[i] = x + xOffsets[i] + positions[i].x @@ -1285,7 +1285,7 @@ When a glyph has scale, rotation, or skew transforms, they are applied in the fo 5. Translate back from anchor (`translate(anchor)`) 6. Translate to position (`translate(position)`) -**Anchor Description**: +**Anchor**: - Each glyph's **default anchor** is at `(advance × 0.5, 0)`, the horizontal center at baseline - The `anchors` attribute records offsets relative to the default anchor, final anchor = default anchor + anchors[i] diff --git a/pagx/spec/pagx_spec.zh_CN.md b/pagx/spec/pagx_spec.zh_CN.md index bec2f722e1..69042d2812 100644 --- a/pagx/spec/pagx_spec.zh_CN.md +++ b/pagx/spec/pagx_spec.zh_CN.md @@ -1256,14 +1256,14 @@ GlyphRun 定义一组字形的预排版数据,每个 GlyphRun 独立引用一 | `y` | float | 0 | 总体 Y 偏移 | | `xOffsets` | string | - | 每字形 X 偏移,逗号分隔 | | `positions` | string | - | 每字形 (x,y) 偏移,分号分隔 | -| `anchors` | string | - | 每字形锚点偏移 (x,y),分号分隔。默认锚点为 (advance×0.5, 0) | -| `scales` | string | - | 每字形缩放 (sx,sy),分号分隔。默认 1,1 | -| `rotations` | string | - | 每字形旋转角度(度),逗号分隔。默认 0 | -| `skews` | string | - | 每字形斜切角度(度),逗号分隔。默认 0 | +| `anchors` | string | - | 每字形锚点偏移 (x,y),分号分隔。锚点是缩放、旋转和斜切变换的中心点。默认锚点为 (advance×0.5, 0) | +| `scales` | string | - | 每字形缩放 (sx,sy),分号分隔。缩放围绕锚点进行。默认 1,1 | +| `rotations` | string | - | 每字形旋转角度(度),逗号分隔。旋转围绕锚点进行。默认 0 | +| `skews` | string | - | 每字形斜切角度(度),逗号分隔。斜切围绕锚点进行。默认 0 | -**属性叠加关系**: +所有属性均为可选,可任意组合使用。当属性数组长度小于字形数量时,缺失的值使用默认值。 -位置属性 `x`、`y`、`xOffsets`、`positions` 不互斥,可叠加使用。最终位置计算如下: +**位置计算**: ``` finalX[i] = x + xOffsets[i] + positions[i].x @@ -1285,7 +1285,7 @@ finalY[i] = y + positions[i].y 5. 平移回锚点(`translate(anchor)`) 6. 平移到位置(`translate(position)`) -**锚点说明**: +**锚点**: - 每个字形的**默认锚点**位于 `(advance × 0.5, 0)`,即字形水平中心的基线位置 - `anchors` 属性记录的是相对于默认锚点的偏移,最终锚点 = 默认锚点 + anchors[i] From e7ae86c365ab99b347910faff355d21ab46c1105 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Fri, 6 Feb 2026 14:29:02 +0800 Subject: [PATCH 361/678] Update tgfx version --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 143e8ba7da..66bfa8e9f8 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "def33a37bdddc28b5395ab9c6a2c572d440bcc60", + "commit": "30d56c27691b808eca1a1573fef3d94c84b2bbce", "dir": "third_party/tgfx" }, { From 5acf30bd57fd7776e2485954f8ea63b586831678 Mon Sep 17 00:00:00 2001 From: jinwuwu001 Date: Fri, 6 Feb 2026 14:29:18 +0800 Subject: [PATCH 362/678] Add debug log --- pagx/wechat/src/PAGXView.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pagx/wechat/src/PAGXView.cpp b/pagx/wechat/src/PAGXView.cpp index 6fdf1343ac..ef2332aeee 100644 --- a/pagx/wechat/src/PAGXView.cpp +++ b/pagx/wechat/src/PAGXView.cpp @@ -29,6 +29,11 @@ using namespace emscripten; namespace pagx { +// 最大缓存限制为 1GB +constexpr size_t MAX_CACHE_LIMIT = 1U * 1024 * 1024 * 1024; +// GPU 有效帧的过期时间,超过该帧数未使用的资源将被释放 +constexpr size_t EXPIRATION_FRAMES = 10 * 60; + std::shared_ptr PAGXView::MakeFrom(int width, int height) { if (width <= 0 || height <= 0) { return nullptr; @@ -183,6 +188,9 @@ bool PAGXView::draw() { return false; } + context->setCacheLimit(MAX_CACHE_LIMIT); + context->setResourceExpirationFrames(EXPIRATION_FRAMES); + if (surface == nullptr || surface->width() != _width || surface->height() != _height) { tgfx::GLFrameBufferInfo glInfo = {}; glInfo.id = 0; @@ -204,10 +212,33 @@ bool PAGXView::draw() { displayList.render(surface.get(), false); auto recording = context->flush(); + auto t1 = emscripten_get_now(); if (recording) { context->submit(std::move(recording)); } device->unlock(); + auto t2 = emscripten_get_now(); + + if ((t2-t1)>20) { + char logBuffer[256]; + snprintf(logBuffer, sizeof(logBuffer), + "[Context] submit: Total: %.2f ms | displayList.maxTilesRefinedPerFrame:%d | isZooming:%s | zoom:%.2f", + t2-t1, displayList.maxTilesRefinedPerFrame(),isZooming?"true":"false",lastZoom); + val::global("console").call("log", std::string(logBuffer)); + } + + static double lastLogTime = 0.0; + double now = emscripten_get_now(); + if (now - lastLogTime > 1000.0) { + size_t cacheLimit = context->cacheLimit(); + size_t expirationFrames = context->resourceExpirationFrames(); + char logBuffer[256]; + snprintf(logBuffer, sizeof(logBuffer), + "[Context] cacheLimit: %zu bytes (%.2f MB) | resourceExpirationFrames: %zu", + cacheLimit, static_cast(cacheLimit) / (1024.0 * 1024.0), expirationFrames); + val::global("console").call("log", std::string(logBuffer)); + lastLogTime = now; + } double frameEndMs = emscripten_get_now(); double frameDurationMs = frameEndMs - frameStartMs; From cee886e2321165db48f42c66beb653871ffccb38 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 6 Feb 2026 15:51:18 +0800 Subject: [PATCH 363/678] Integrate tgfx Text anchors API for per-glyph anchor support. --- DEPS | 2 +- pagx/include/pagx/TextGlyphs.h | 22 +++++++++++-- pagx/src/TextGlyphs.cpp | 26 +++++++++++---- pagx/src/tgfx/FontEmbedder.cpp | 18 +++++------ pagx/src/tgfx/LayerBuilder.cpp | 59 ++++++++++++++++++---------------- pagx/src/tgfx/Typesetter.cpp | 25 +++++++++++--- test/src/PAGXTest.cpp | 7 ++-- 7 files changed, 104 insertions(+), 55 deletions(-) diff --git a/DEPS b/DEPS index 1693fa9196..6ea7792439 100644 --- a/DEPS +++ b/DEPS @@ -12,7 +12,7 @@ }, { "url": "${PAG_GROUP}/tgfx.git", - "commit": "39198e37397209d25fd6cfedb790e7feabc02227", + "commit": "f454d3cb3c3a76a9e1d9fb1dce0910e2e422ba3f", "dir": "third_party/tgfx" }, { diff --git a/pagx/include/pagx/TextGlyphs.h b/pagx/include/pagx/TextGlyphs.h index 1e79ebfabd..e9c431c114 100644 --- a/pagx/include/pagx/TextGlyphs.h +++ b/pagx/include/pagx/TextGlyphs.h @@ -20,6 +20,8 @@ #include #include +#include +#include "tgfx/core/Point.h" #include "tgfx/core/TextBlob.h" namespace pagx { @@ -37,15 +39,24 @@ class TextGlyphs { TextGlyphs() = default; /** - * Sets the TextBlob for a Text node. + * Sets the TextBlob and optional anchors for a Text node. + * @param text The Text node. + * @param textBlob The shaped TextBlob. + * @param anchors Optional anchor offsets for each glyph. */ - void setTextBlob(Text* text, std::shared_ptr textBlob); + void setTextBlob(Text* text, std::shared_ptr textBlob, + std::vector anchors = {}); /** * Returns the TextBlob for the given Text node, or nullptr if not found. */ std::shared_ptr getTextBlob(const Text* text) const; + /** + * Returns the anchor offsets for the given Text node. + */ + const std::vector& getAnchors(const Text* text) const; + /** * Returns true if there are no mappings. */ @@ -54,7 +65,12 @@ class TextGlyphs { private: friend class FontEmbedder; - std::unordered_map> textBlobs = {}; + struct TextData { + std::shared_ptr textBlob = nullptr; + std::vector anchors = {}; + }; + + std::unordered_map textDataMap = {}; }; } // namespace pagx diff --git a/pagx/src/TextGlyphs.cpp b/pagx/src/TextGlyphs.cpp index 1c585030d3..99e28fdb71 100644 --- a/pagx/src/TextGlyphs.cpp +++ b/pagx/src/TextGlyphs.cpp @@ -20,22 +20,36 @@ namespace pagx { -void TextGlyphs::setTextBlob(Text* text, std::shared_ptr textBlob) { +static const std::vector EmptyAnchors = {}; + +void TextGlyphs::setTextBlob(Text* text, std::shared_ptr textBlob, + std::vector anchors) { if (text != nullptr && textBlob != nullptr) { - textBlobs[text] = std::move(textBlob); + TextData data = {}; + data.textBlob = std::move(textBlob); + data.anchors = std::move(anchors); + textDataMap[text] = std::move(data); } } std::shared_ptr TextGlyphs::getTextBlob(const Text* text) const { - auto it = textBlobs.find(const_cast(text)); - if (it != textBlobs.end()) { - return it->second; + auto it = textDataMap.find(const_cast(text)); + if (it != textDataMap.end()) { + return it->second.textBlob; } return nullptr; } +const std::vector& TextGlyphs::getAnchors(const Text* text) const { + auto it = textDataMap.find(const_cast(text)); + if (it != textDataMap.end()) { + return it->second.anchors; + } + return EmptyAnchors; +} + bool TextGlyphs::empty() const { - return textBlobs.empty(); + return textDataMap.empty(); } } // namespace pagx diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index 970537e715..aec6279b5d 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -470,11 +470,11 @@ bool FontEmbedder::embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { std::unordered_map bitmapBuilders = {}; // First pass: collect max font size for each vector glyph - for (const auto& [text, textBlob] : textGlyphs.textBlobs) { - if (textBlob == nullptr) { + for (const auto& [text, data] : textGlyphs.textDataMap) { + if (data.textBlob == nullptr) { continue; } - for (const auto& run : *textBlob) { + for (const auto& run : *data.textBlob) { for (size_t i = 0; i < run.glyphCount; ++i) { tgfx::GlyphID glyphID = run.glyphs[i]; if (IsVectorGlyph(run.font, glyphID)) { @@ -485,11 +485,11 @@ bool FontEmbedder::embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { } // Second pass: collect glyphs using max font size for vector, first encounter for bitmap - for (const auto& [text, textBlob] : textGlyphs.textBlobs) { - if (textBlob == nullptr) { + for (const auto& [text, data] : textGlyphs.textDataMap) { + if (data.textBlob == nullptr) { continue; } - for (const auto& run : *textBlob) { + for (const auto& run : *data.textBlob) { for (size_t i = 0; i < run.glyphCount; ++i) { tgfx::GlyphID glyphID = run.glyphs[i]; if (IsVectorGlyph(run.font, glyphID)) { @@ -513,14 +513,14 @@ bool FontEmbedder::embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { } // Third pass: create GlyphRuns for each Text - for (const auto& [text, textBlob] : textGlyphs.textBlobs) { - if (textBlob == nullptr) { + for (const auto& [text, data] : textGlyphs.textDataMap) { + if (data.textBlob == nullptr) { continue; } text->glyphRuns.clear(); - for (const auto& run : *textBlob) { + for (const auto& run : *data.textBlob) { if (run.glyphCount == 0) { continue; } diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index 2be93b441b..c1edffc69d 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -501,7 +501,7 @@ class LayerBuilderImpl { } std::shared_ptr convertRectangle(const Rectangle* node) { - auto rect = std::make_shared(); + auto rect = tgfx::Rectangle::Make(); rect->setCenter(ToTGFX(node->center)); rect->setSize({node->size.width, node->size.height}); rect->setRoundness(node->roundness); @@ -510,7 +510,7 @@ class LayerBuilderImpl { } std::shared_ptr convertEllipse(const Ellipse* node) { - auto ellipse = std::make_shared(); + auto ellipse = tgfx::Ellipse::Make(); ellipse->setCenter(ToTGFX(node->center)); ellipse->setSize({node->size.width, node->size.height}); ellipse->setReversed(node->reversed); @@ -518,7 +518,7 @@ class LayerBuilderImpl { } std::shared_ptr convertPolystar(const Polystar* node) { - auto polystar = std::make_shared(); + auto polystar = tgfx::Polystar::Make(); polystar->setCenter(ToTGFX(node->center)); polystar->setPointCount(node->pointCount); polystar->setOuterRadius(node->outerRadius); @@ -536,7 +536,7 @@ class LayerBuilderImpl { } std::shared_ptr convertPath(const Path* node) { - auto shapePath = std::make_shared(); + auto shapePath = tgfx::ShapePath::Make(); if (node->data) { shapePath->setPath(ToTGFX(*node->data)); } @@ -544,44 +544,47 @@ class LayerBuilderImpl { } std::shared_ptr convertText(const Text* node) { - auto tgfxText = std::make_shared(); - auto textBlob = _textGlyphs.getTextBlob(node); - if (textBlob) { - tgfxText->setTextBlob(textBlob); + if (textBlob == nullptr) { + return nullptr; + } + auto anchors = _textGlyphs.getAnchors(node); + auto tgfxText = tgfx::Text::Make(textBlob, anchors); + if (tgfxText) { + tgfxText->setPosition(tgfx::Point::Make(node->position.x, node->position.y)); } - tgfxText->setPosition(tgfx::Point::Make(node->position.x, node->position.y)); return tgfxText; } std::shared_ptr convertFill(const Fill* node) { - auto fill = std::make_shared(); - std::shared_ptr colorSource = nullptr; if (node->color) { colorSource = convertColorSource(node->color); } - - if (colorSource) { - fill->setColorSource(colorSource); + if (colorSource == nullptr) { + return nullptr; } - fill->setAlpha(node->alpha); + auto fill = tgfx::FillStyle::Make(colorSource); + if (fill) { + fill->setAlpha(node->alpha); + } return fill; } std::shared_ptr convertStroke(const Stroke* node) { - auto stroke = std::make_shared(); - std::shared_ptr colorSource = nullptr; if (node->color) { colorSource = convertColorSource(node->color); } - - if (colorSource) { - stroke->setColorSource(colorSource); + if (colorSource == nullptr) { + return nullptr; } + auto stroke = tgfx::StrokeStyle::Make(colorSource); + if (stroke == nullptr) { + return nullptr; + } stroke->setStrokeWidth(node->width); stroke->setAlpha(node->alpha); stroke->setLineCap(ToTGFX(node->cap)); @@ -743,7 +746,7 @@ class LayerBuilderImpl { } std::shared_ptr convertTrimPath(const TrimPath* node) { - auto trim = std::make_shared(); + auto trim = tgfx::TrimPath::Make(); trim->setStart(node->start); trim->setEnd(node->end); trim->setOffset(node->offset); @@ -751,7 +754,7 @@ class LayerBuilderImpl { } std::shared_ptr convertTextPath(const TextPath* node) { - auto textPath = std::make_shared(); + auto textPath = tgfx::TextPath::Make(); if (node->path != nullptr) { textPath->setPath(ToTGFX(*node->path)); } @@ -764,22 +767,22 @@ class LayerBuilderImpl { } std::shared_ptr convertRoundCorner(const RoundCorner* node) { - auto round = std::make_shared(); + auto round = tgfx::RoundCorner::Make(); round->setRadius(node->radius); return round; } std::shared_ptr convertMergePath(const MergePath*) { - auto merge = std::make_shared(); + auto merge = tgfx::MergePath::Make(); return merge; } std::shared_ptr convertRepeater(const Repeater* node) { - auto repeater = std::make_shared(); + auto repeater = tgfx::Repeater::Make(); repeater->setCopies(node->copies); repeater->setOffset(node->offset); repeater->setOrder(static_cast(node->order)); - repeater->setAnchorPoint(ToTGFX(node->anchor)); + repeater->setAnchor(ToTGFX(node->anchor)); repeater->setPosition(ToTGFX(node->position)); repeater->setRotation(node->rotation); repeater->setScale(ToTGFX(node->scale)); @@ -789,7 +792,7 @@ class LayerBuilderImpl { } std::shared_ptr convertGroup(const Group* node) { - auto group = std::make_shared(); + auto group = tgfx::VectorGroup::Make(); std::vector> elements; for (const auto& element : node->elements) { @@ -808,7 +811,7 @@ class LayerBuilderImpl { // Apply transform properties if (node->anchor.x != 0 || node->anchor.y != 0) { - group->setAnchorPoint(ToTGFX(node->anchor)); + group->setAnchor(ToTGFX(node->anchor)); } if (node->position.x != 0 || node->position.y != 0) { group->setPosition(ToTGFX(node->position)); diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index a6ca6a634b..998a31adbe 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -204,9 +204,10 @@ class TypesetterContext { if (textLayout == nullptr || allHaveEmbeddedData) { for (auto* text : textElements) { if (!text->glyphRuns.empty()) { - auto textBlob = buildTextBlobFromEmbeddedGlyphRuns(text); + std::vector anchors = {}; + auto textBlob = buildTextBlobFromEmbeddedGlyphRuns(text, &anchors); if (textBlob != nullptr) { - result.setTextBlob(text, textBlob); + result.setTextBlob(text, textBlob, std::move(anchors)); } } else { processTextWithoutLayout(text); @@ -309,7 +310,8 @@ class TypesetterContext { } } - std::shared_ptr buildTextBlobFromEmbeddedGlyphRuns(const Text* text) { + std::shared_ptr buildTextBlobFromEmbeddedGlyphRuns(const Text* text, + std::vector* outAnchors) { tgfx::TextBlobBuilder builder; for (const auto& run : text->glyphRuns) { @@ -332,7 +334,22 @@ class TypesetterContext { tgfx::Font font(typeface, fontSizeForTypeface); size_t count = run->glyphs.size(); - // Note: scales, rotations, skews, anchors are NOT processed here. + // Collect anchors for each glyph in this run + if (outAnchors != nullptr && !run->anchors.empty()) { + for (size_t i = 0; i < count; i++) { + if (i < run->anchors.size()) { + outAnchors->push_back(tgfx::Point::Make(run->anchors[i].x, run->anchors[i].y)); + } else { + outAnchors->push_back(tgfx::Point::Zero()); + } + } + } else if (outAnchors != nullptr) { + for (size_t i = 0; i < count; i++) { + outAnchors->push_back(tgfx::Point::Zero()); + } + } + + // Note: scales, rotations, skews are NOT processed here. // These transform attributes should be handled by tgfx layer (similar to TextModifier). // Typesetter only computes position information for TextBlob. diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 625ba9d899..cd3272124f 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -266,14 +266,13 @@ PAG_TEST(PAGXTest, LayerDirectContent) { { auto vectorLayer = tgfx::VectorLayer::Make(); - auto group = std::make_shared(); + auto group = tgfx::VectorGroup::Make(); - auto rect = std::make_shared(); + auto rect = tgfx::Rectangle::Make(); rect->setCenter(tgfx::Point::Make(50, 50)); rect->setSize({100, 100}); - auto fill = std::make_shared(); - fill->setColorSource(tgfx::SolidColor::Make(tgfx::Color::Red())); + auto fill = tgfx::FillStyle::Make(tgfx::SolidColor::Make(tgfx::Color::Red())); group->setElements({rect, fill}); vectorLayer->setContents({group}); From 7effe400832dbd87fe218bab8fac2a7f951c906a Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 6 Feb 2026 15:58:51 +0800 Subject: [PATCH 364/678] Refactor TextGlyphs to use ShapedText composite struct for cleaner API. --- pagx/include/pagx/TextGlyphs.h | 40 +++++++++++++++++----------------- pagx/src/TextGlyphs.cpp | 30 +++++++------------------ pagx/src/tgfx/FontEmbedder.cpp | 18 +++++++-------- pagx/src/tgfx/LayerBuilder.cpp | 7 +++--- pagx/src/tgfx/Typesetter.cpp | 32 +++++++++++++-------------- 5 files changed, 56 insertions(+), 71 deletions(-) diff --git a/pagx/include/pagx/TextGlyphs.h b/pagx/include/pagx/TextGlyphs.h index e9c431c114..eb036b582a 100644 --- a/pagx/include/pagx/TextGlyphs.h +++ b/pagx/include/pagx/TextGlyphs.h @@ -27,7 +27,21 @@ namespace pagx { class Text; -class FontEmbedderImpl; + +/** + * Shaped text data containing the TextBlob and per-glyph anchor offsets. + */ +struct ShapedText { + /** + * The shaped TextBlob containing glyph positions. + */ + std::shared_ptr textBlob = nullptr; + + /** + * Per-glyph anchor offsets relative to default anchor (advance * 0.5, 0). + */ + std::vector anchors = {}; +}; /** * TextGlyphs holds the text typesetting results, mapping Text nodes to their shaped TextBlob @@ -39,23 +53,14 @@ class TextGlyphs { TextGlyphs() = default; /** - * Sets the TextBlob and optional anchors for a Text node. - * @param text The Text node. - * @param textBlob The shaped TextBlob. - * @param anchors Optional anchor offsets for each glyph. - */ - void setTextBlob(Text* text, std::shared_ptr textBlob, - std::vector anchors = {}); - - /** - * Returns the TextBlob for the given Text node, or nullptr if not found. + * Sets the shaped text data for a Text node. */ - std::shared_ptr getTextBlob(const Text* text) const; + void set(Text* text, ShapedText data); /** - * Returns the anchor offsets for the given Text node. + * Returns the shaped text data for the given Text node, or nullptr if not found. */ - const std::vector& getAnchors(const Text* text) const; + const ShapedText* get(const Text* text) const; /** * Returns true if there are no mappings. @@ -65,12 +70,7 @@ class TextGlyphs { private: friend class FontEmbedder; - struct TextData { - std::shared_ptr textBlob = nullptr; - std::vector anchors = {}; - }; - - std::unordered_map textDataMap = {}; + std::unordered_map shapedTextMap = {}; }; } // namespace pagx diff --git a/pagx/src/TextGlyphs.cpp b/pagx/src/TextGlyphs.cpp index 99e28fdb71..f8f4237221 100644 --- a/pagx/src/TextGlyphs.cpp +++ b/pagx/src/TextGlyphs.cpp @@ -20,36 +20,22 @@ namespace pagx { -static const std::vector EmptyAnchors = {}; - -void TextGlyphs::setTextBlob(Text* text, std::shared_ptr textBlob, - std::vector anchors) { - if (text != nullptr && textBlob != nullptr) { - TextData data = {}; - data.textBlob = std::move(textBlob); - data.anchors = std::move(anchors); - textDataMap[text] = std::move(data); +void TextGlyphs::set(Text* text, ShapedText data) { + if (text != nullptr && data.textBlob != nullptr) { + shapedTextMap[text] = std::move(data); } } -std::shared_ptr TextGlyphs::getTextBlob(const Text* text) const { - auto it = textDataMap.find(const_cast(text)); - if (it != textDataMap.end()) { - return it->second.textBlob; +const ShapedText* TextGlyphs::get(const Text* text) const { + auto it = shapedTextMap.find(const_cast(text)); + if (it != shapedTextMap.end()) { + return &it->second; } return nullptr; } -const std::vector& TextGlyphs::getAnchors(const Text* text) const { - auto it = textDataMap.find(const_cast(text)); - if (it != textDataMap.end()) { - return it->second.anchors; - } - return EmptyAnchors; -} - bool TextGlyphs::empty() const { - return textDataMap.empty(); + return shapedTextMap.empty(); } } // namespace pagx diff --git a/pagx/src/tgfx/FontEmbedder.cpp b/pagx/src/tgfx/FontEmbedder.cpp index aec6279b5d..97876e97eb 100644 --- a/pagx/src/tgfx/FontEmbedder.cpp +++ b/pagx/src/tgfx/FontEmbedder.cpp @@ -470,11 +470,11 @@ bool FontEmbedder::embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { std::unordered_map bitmapBuilders = {}; // First pass: collect max font size for each vector glyph - for (const auto& [text, data] : textGlyphs.textDataMap) { - if (data.textBlob == nullptr) { + for (const auto& [text, shapedText] : textGlyphs.shapedTextMap) { + if (shapedText.textBlob == nullptr) { continue; } - for (const auto& run : *data.textBlob) { + for (const auto& run : *shapedText.textBlob) { for (size_t i = 0; i < run.glyphCount; ++i) { tgfx::GlyphID glyphID = run.glyphs[i]; if (IsVectorGlyph(run.font, glyphID)) { @@ -485,11 +485,11 @@ bool FontEmbedder::embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { } // Second pass: collect glyphs using max font size for vector, first encounter for bitmap - for (const auto& [text, data] : textGlyphs.textDataMap) { - if (data.textBlob == nullptr) { + for (const auto& [text, shapedText] : textGlyphs.shapedTextMap) { + if (shapedText.textBlob == nullptr) { continue; } - for (const auto& run : *data.textBlob) { + for (const auto& run : *shapedText.textBlob) { for (size_t i = 0; i < run.glyphCount; ++i) { tgfx::GlyphID glyphID = run.glyphs[i]; if (IsVectorGlyph(run.font, glyphID)) { @@ -513,14 +513,14 @@ bool FontEmbedder::embed(PAGXDocument* document, const TextGlyphs& textGlyphs) { } // Third pass: create GlyphRuns for each Text - for (const auto& [text, data] : textGlyphs.textDataMap) { - if (data.textBlob == nullptr) { + for (const auto& [text, shapedText] : textGlyphs.shapedTextMap) { + if (shapedText.textBlob == nullptr) { continue; } text->glyphRuns.clear(); - for (const auto& run : *data.textBlob) { + for (const auto& run : *shapedText.textBlob) { if (run.glyphCount == 0) { continue; } diff --git a/pagx/src/tgfx/LayerBuilder.cpp b/pagx/src/tgfx/LayerBuilder.cpp index c1edffc69d..42ee2d2e2a 100644 --- a/pagx/src/tgfx/LayerBuilder.cpp +++ b/pagx/src/tgfx/LayerBuilder.cpp @@ -544,12 +544,11 @@ class LayerBuilderImpl { } std::shared_ptr convertText(const Text* node) { - auto textBlob = _textGlyphs.getTextBlob(node); - if (textBlob == nullptr) { + auto shapedText = _textGlyphs.get(node); + if (shapedText == nullptr || shapedText->textBlob == nullptr) { return nullptr; } - auto anchors = _textGlyphs.getAnchors(node); - auto tgfxText = tgfx::Text::Make(textBlob, anchors); + auto tgfxText = tgfx::Text::Make(shapedText->textBlob, shapedText->anchors); if (tgfxText) { tgfxText->setPosition(tgfx::Point::Make(node->position.x, node->position.y)); } diff --git a/pagx/src/tgfx/Typesetter.cpp b/pagx/src/tgfx/Typesetter.cpp index 998a31adbe..aac6a9ca1c 100644 --- a/pagx/src/tgfx/Typesetter.cpp +++ b/pagx/src/tgfx/Typesetter.cpp @@ -204,10 +204,9 @@ class TypesetterContext { if (textLayout == nullptr || allHaveEmbeddedData) { for (auto* text : textElements) { if (!text->glyphRuns.empty()) { - std::vector anchors = {}; - auto textBlob = buildTextBlobFromEmbeddedGlyphRuns(text, &anchors); - if (textBlob != nullptr) { - result.setTextBlob(text, textBlob, std::move(anchors)); + auto shapedText = buildShapedTextFromEmbeddedGlyphRuns(text); + if (shapedText.textBlob != nullptr) { + result.set(text, std::move(shapedText)); } } else { processTextWithoutLayout(text); @@ -268,7 +267,9 @@ class TypesetterContext { auto textBlob = builder.build(); if (textBlob != nullptr) { - result.setTextBlob(info.text, textBlob); + ShapedText shapedText = {}; + shapedText.textBlob = textBlob; + result.set(info.text, std::move(shapedText)); } } } @@ -306,12 +307,14 @@ class TypesetterContext { auto textBlob = builder.build(); if (textBlob != nullptr) { - result.setTextBlob(text, textBlob); + ShapedText shapedText = {}; + shapedText.textBlob = textBlob; + result.set(text, std::move(shapedText)); } } - std::shared_ptr buildTextBlobFromEmbeddedGlyphRuns(const Text* text, - std::vector* outAnchors) { + ShapedText buildShapedTextFromEmbeddedGlyphRuns(const Text* text) { + ShapedText shapedText = {}; tgfx::TextBlobBuilder builder; for (const auto& run : text->glyphRuns) { @@ -335,18 +338,14 @@ class TypesetterContext { size_t count = run->glyphs.size(); // Collect anchors for each glyph in this run - if (outAnchors != nullptr && !run->anchors.empty()) { + if (!run->anchors.empty()) { for (size_t i = 0; i < count; i++) { if (i < run->anchors.size()) { - outAnchors->push_back(tgfx::Point::Make(run->anchors[i].x, run->anchors[i].y)); + shapedText.anchors.push_back(tgfx::Point::Make(run->anchors[i].x, run->anchors[i].y)); } else { - outAnchors->push_back(tgfx::Point::Zero()); + shapedText.anchors.push_back(tgfx::Point::Zero()); } } - } else if (outAnchors != nullptr) { - for (size_t i = 0; i < count; i++) { - outAnchors->push_back(tgfx::Point::Zero()); - } } // Note: scales, rotations, skews are NOT processed here. @@ -381,7 +380,8 @@ class TypesetterContext { } } - return builder.build(); + shapedText.textBlob = builder.build(); + return shapedText; } std::shared_ptr buildTypefaceFromFont(const Font* fontNode) { From 1840915e65603b10f45194b2bb26b235b1a0f473 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 6 Feb 2026 16:33:56 +0800 Subject: [PATCH 365/678] Simplify TextGlyphs to a struct and rename Impl classes to Context. --- pagx/include/pagx/FontEmbedder.h | 10 ++-- pagx/include/pagx/PAGXDocument.h | 2 +- pagx/include/pagx/TextGlyphs.h | 33 ++-------- pagx/include/pagx/Typesetter.h | 6 +- pagx/include/pagx/nodes/PathData.h | 2 +- pagx/src/TextGlyphs.cpp | 41 ------------- pagx/src/svg/SVGImporter.cpp | 96 +++++++++++++++--------------- pagx/src/svg/SVGParserInternal.h | 4 +- pagx/src/tgfx/FontEmbedder.cpp | 20 +++---- pagx/src/tgfx/LayerBuilder.cpp | 31 ++++++---- pagx/src/tgfx/Typesetter.cpp | 38 ++++++------ 11 files changed, 111 insertions(+), 172 deletions(-) delete mode 100644 pagx/src/TextGlyphs.cpp diff --git a/pagx/include/pagx/FontEmbedder.h b/pagx/include/pagx/FontEmbedder.h index 8841235a2c..47ede77b60 100644 --- a/pagx/include/pagx/FontEmbedder.h +++ b/pagx/include/pagx/FontEmbedder.h @@ -24,7 +24,7 @@ namespace pagx { /** - * FontEmbedder extracts glyph data from TextGlyphs and embeds it into the PAGXDocument. + * FontEmbedder extracts glyph data from TextGlyphsMap and embeds it into the PAGXDocument. * It creates Font nodes with glyph paths/images and updates Text nodes with GlyphRun data. * * Font merging strategy: @@ -37,18 +37,18 @@ class FontEmbedder { FontEmbedder() = default; /** - * Embeds font data from TextGlyphs into the document. + * Embeds font data from TextGlyphsMap into the document. * * This method: - * 1. Iterates all TextBlobs in textGlyphs, extracts glyph data (paths or images) + * 1. Iterates all TextBlobs in textGlyphsMap, extracts glyph data (paths or images) * 2. Merges glyphs into Font nodes (one for vector, one for bitmap) * 3. Creates GlyphRun nodes for each Text, referencing the embedded fonts * * @param document The document to embed fonts into (modified in place). - * @param textGlyphs The typesetting results containing Text -> TextBlob mappings. + * @param textGlyphsMap The typesetting results containing Text -> TextGlyphs mappings. * @return true if embedding succeeded, false otherwise. */ - bool embed(PAGXDocument* document, const TextGlyphs& textGlyphs); + bool embed(PAGXDocument* document, const TextGlyphsMap& textGlyphsMap); }; } // namespace pagx diff --git a/pagx/include/pagx/PAGXDocument.h b/pagx/include/pagx/PAGXDocument.h index 829431b3c4..a134fb8eea 100644 --- a/pagx/include/pagx/PAGXDocument.h +++ b/pagx/include/pagx/PAGXDocument.h @@ -110,7 +110,7 @@ class PAGXDocument { friend class PAGXImporter; friend class PAGXExporter; - friend class TypesetterImpl; + friend class TypesetterContext; }; } // namespace pagx diff --git a/pagx/include/pagx/TextGlyphs.h b/pagx/include/pagx/TextGlyphs.h index eb036b582a..200a4bcf70 100644 --- a/pagx/include/pagx/TextGlyphs.h +++ b/pagx/include/pagx/TextGlyphs.h @@ -29,9 +29,9 @@ namespace pagx { class Text; /** - * Shaped text data containing the TextBlob and per-glyph anchor offsets. + * Shaped glyph data containing the TextBlob and per-glyph anchor offsets. */ -struct ShapedText { +struct TextGlyphs { /** * The shaped TextBlob containing glyph positions. */ @@ -44,33 +44,8 @@ struct ShapedText { }; /** - * TextGlyphs holds the text typesetting results, mapping Text nodes to their shaped TextBlob - * representations. This class serves as the bridge between text typesetting (by Typesetter) and - * downstream consumers (LayerBuilder for rendering, FontEmbedder for font embedding). + * Mapping from Text nodes to their shaped glyph data. */ -class TextGlyphs { - public: - TextGlyphs() = default; - - /** - * Sets the shaped text data for a Text node. - */ - void set(Text* text, ShapedText data); - - /** - * Returns the shaped text data for the given Text node, or nullptr if not found. - */ - const ShapedText* get(const Text* text) const; - - /** - * Returns true if there are no mappings. - */ - bool empty() const; - - private: - friend class FontEmbedder; - - std::unordered_map shapedTextMap = {}; -}; +using TextGlyphsMap = std::unordered_map; } // namespace pagx diff --git a/pagx/include/pagx/Typesetter.h b/pagx/include/pagx/Typesetter.h index c1036b604a..52ac621a93 100644 --- a/pagx/include/pagx/Typesetter.h +++ b/pagx/include/pagx/Typesetter.h @@ -49,14 +49,14 @@ class Typesetter { void setFallbackTypefaces(std::vector> typefaces); /** - * Creates TextGlyphs for all Text nodes in the document. If a Text node has embedded GlyphRun + * Creates TextGlyphsMap for all Text nodes in the document. If a Text node has embedded GlyphRun * data (from a loaded PAGX file), it uses that data directly. Otherwise, it performs text * shaping using registered/fallback typefaces. TextLayout modifiers are processed to apply * alignment, line breaking, and other layout properties. * @param document The document containing Text nodes to typeset. - * @return TextGlyphs containing Text -> TextBlob mappings. + * @return TextGlyphsMap containing Text -> TextGlyphs mappings. */ - TextGlyphs createTextGlyphs(PAGXDocument* document); + TextGlyphsMap createTextGlyphs(PAGXDocument* document); private: friend class TypesetterContext; diff --git a/pagx/include/pagx/nodes/PathData.h b/pagx/include/pagx/nodes/PathData.h index 1874fc3c95..353dfcfa63 100644 --- a/pagx/include/pagx/nodes/PathData.h +++ b/pagx/include/pagx/nodes/PathData.h @@ -134,7 +134,7 @@ class PathData : public Node { bool _boundsDirty = true; friend class PAGXDocument; - friend class SVGParserImpl; + friend class SVGParserContext; friend PathData PathDataFromSVGString(const std::string& d); }; diff --git a/pagx/src/TextGlyphs.cpp b/pagx/src/TextGlyphs.cpp deleted file mode 100644 index f8f4237221..0000000000 --- a/pagx/src/TextGlyphs.cpp +++ /dev/null @@ -1,41 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making libpag available. -// -// Copyright (C) 2026 Tencent. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file -// except in compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "pagx/TextGlyphs.h" - -namespace pagx { - -void TextGlyphs::set(Text* text, ShapedText data) { - if (text != nullptr && data.textBlob != nullptr) { - shapedTextMap[text] = std::move(data); - } -} - -const ShapedText* TextGlyphs::get(const Text* text) const { - auto it = shapedTextMap.find(const_cast(text)); - if (it != shapedTextMap.end()) { - return &it->second; - } - return nullptr; -} - -bool TextGlyphs::empty() const { - return shapedTextMap.empty(); -} - -} // namespace pagx diff --git a/pagx/src/svg/SVGImporter.cpp b/pagx/src/svg/SVGImporter.cpp index b5bf5c0e65..cecdf775d6 100644 --- a/pagx/src/svg/SVGImporter.cpp +++ b/pagx/src/svg/SVGImporter.cpp @@ -33,7 +33,7 @@ namespace pagx { std::shared_ptr SVGImporter::Parse(const std::string& filePath, const Options& options) { - SVGParserImpl parser(options); + SVGParserContext parser(options); auto doc = parser.parseFile(filePath); if (doc) { // Convert relative paths to absolute paths @@ -60,7 +60,7 @@ std::shared_ptr SVGImporter::Parse(const std::string& filePath, std::shared_ptr SVGImporter::Parse(const uint8_t* data, size_t length, const Options& options) { - SVGParserImpl parser(options); + SVGParserContext parser(options); return parser.parse(data, length); } @@ -69,12 +69,12 @@ std::shared_ptr SVGImporter::ParseString(const std::string& svgCon return Parse(reinterpret_cast(svgContent.data()), svgContent.size(), options); } -// ============== SVGParserImpl ============== +// ============== SVGParserContext ============== -SVGParserImpl::SVGParserImpl(const SVGImporter::Options& options) : _options(options) { +SVGParserContext::SVGParserContext(const SVGImporter::Options& options) : _options(options) { } -std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t length) { +std::shared_ptr SVGParserContext::parse(const uint8_t* data, size_t length) { if (!data || length == 0) { return nullptr; } @@ -87,7 +87,7 @@ std::shared_ptr SVGParserImpl::parse(const uint8_t* data, size_t l return parseDOM(dom); } -std::shared_ptr SVGParserImpl::parseFile(const std::string& filePath) { +std::shared_ptr SVGParserContext::parseFile(const std::string& filePath) { auto dom = DOM::MakeFromFile(filePath); if (!dom) { return nullptr; @@ -96,7 +96,7 @@ std::shared_ptr SVGParserImpl::parseFile(const std::string& filePa return parseDOM(dom); } -std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, +std::string SVGParserContext::getAttribute(const std::shared_ptr& node, const std::string& name, const std::string& defaultValue) const { // CSS priority: style attribute > presentation attribute > CSS class rules @@ -242,7 +242,7 @@ std::string SVGParserImpl::getAttribute(const std::shared_ptr& node, return defaultValue; } -std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr& dom) { +std::shared_ptr SVGParserContext::parseDOM(const std::shared_ptr& dom) { auto root = dom->getRootNode(); if (!root || root->name != "svg") { return nullptr; @@ -367,7 +367,7 @@ std::shared_ptr SVGParserImpl::parseDOM(const std::shared_ptr return _document; } -InheritedStyle SVGParserImpl::computeInheritedStyle(const std::shared_ptr& element, +InheritedStyle SVGParserContext::computeInheritedStyle(const std::shared_ptr& element, const InheritedStyle& parentStyle) { InheritedStyle style = parentStyle; @@ -460,7 +460,7 @@ InheritedStyle SVGParserImpl::computeInheritedStyle(const std::shared_ptr& defsNode) { +void SVGParserContext::parseDefs(const std::shared_ptr& defsNode) { auto child = defsNode->getFirstChild(); while (child) { std::string id = getAttribute(child, "id"); @@ -475,7 +475,7 @@ void SVGParserImpl::parseDefs(const std::shared_ptr& defsNode) { } } -void SVGParserImpl::parseStyleElement(const std::shared_ptr& styleNode) { +void SVGParserContext::parseStyleElement(const std::shared_ptr& styleNode) { // Get the text content of the