From fc2706f42279fa3ab8dc90f259a52638d3ec7b70 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 22 Apr 2026 14:24:23 +0800 Subject: [PATCH 01/87] Ignore GSD planning directory from version control. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1c2e2b27e5..01e71ec915 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ local.properties # CodeBuddy .codebuddy/designs/ +# GSD workflow (local-only planning docs) +.planning/ + # Local config *.local.json *.local.md From f6723e6e9c5aeeca2081bbbee773ec0b7086897a Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 11:39:50 +0800 Subject: [PATCH 02/87] Add ImageEmbedder header declaring embed and lastErrorPath API. --- src/renderer/ImageEmbedder.h | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/renderer/ImageEmbedder.h diff --git a/src/renderer/ImageEmbedder.h b/src/renderer/ImageEmbedder.h new file mode 100644 index 0000000000..eeb12bf732 --- /dev/null +++ b/src/renderer/ImageEmbedder.h @@ -0,0 +1,58 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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" + +namespace pagx { + +/** + * ImageEmbedder reads every external-file Image node in a PAGXDocument and inlines the raw + * bytes via PAGXDocument::loadFileData. Unlike FontEmbedder, it does not require applyLayout() + * and has no ClearEmbeddedGlyphRuns-style helper — Image embedding has no layout dependency. + * + * URL-form paths (containing "://") are silently skipped; production PAGX does not use them. + * + * On the first read failure, embed() returns false and lastErrorPath() identifies the + * offending file. The document may be partially mutated on failure (earlier successful + * reads are already applied); callers must not write the output on failure. + */ +class ImageEmbedder { + public: + ImageEmbedder() = default; + + /** + * Embeds every external Image file referenced by the document. Returns true on full + * success. On failure, returns false and sets lastErrorPath() to the first unreadable + * file. + */ + bool embed(PAGXDocument* document); + + /** + * When embed() returns false, identifies which file could not be read. + */ + const std::string& lastErrorPath() const { + return lastErrorPath_; + } + + private: + std::string lastErrorPath_; +}; + +} // namespace pagx From 5b6a81859a0776588d3ccd550b46dc153b09343e Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 11:42:31 +0800 Subject: [PATCH 03/87] Implement ImageEmbedder with ReadFileToData supporting empty-file and partial-read detection. --- src/renderer/ImageEmbedder.cpp | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/renderer/ImageEmbedder.cpp diff --git a/src/renderer/ImageEmbedder.cpp b/src/renderer/ImageEmbedder.cpp new file mode 100644 index 0000000000..871b6b2b16 --- /dev/null +++ b/src/renderer/ImageEmbedder.cpp @@ -0,0 +1,58 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "renderer/ImageEmbedder.h" +#include +#include "pagx/types/Data.h" + +namespace pagx { + +static std::shared_ptr ReadFileToData(const std::string& path) { + std::ifstream in(path, std::ios::binary | std::ios::ate); + if (!in.is_open()) return nullptr; + std::streampos end = in.tellg(); + if (end <= 0) return nullptr; // covers tellg failure and empty file + auto size = static_cast(end); + in.seekg(0, std::ios::beg); + auto* buffer = new uint8_t[size]; + in.read(reinterpret_cast(buffer), static_cast(size)); + if (!in || static_cast(in.gcount()) != size) { + delete[] buffer; + return nullptr; + } + return pagx::Data::MakeAdopt(buffer, size); +} + +bool ImageEmbedder::embed(PAGXDocument* document) { + if (document == nullptr) return false; + auto paths = document->getExternalFilePaths(); + for (const auto& path : paths) { + if (path.find("://") != std::string::npos) { + continue; // URL per D1.3 — silently skip + } + auto data = ReadFileToData(path); + if (data == nullptr) { + lastErrorPath_ = path; + return false; + } + document->loadFileData(path, data); + } + return true; +} + +} // namespace pagx From 4c99b91d70b024f00427a6ca63eb296694b5c714 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 12:03:40 +0800 Subject: [PATCH 04/87] Add CommandEmbed header declaring RunEmbed entry point. --- src/cli/CommandEmbed.h | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/cli/CommandEmbed.h diff --git a/src/cli/CommandEmbed.h b/src/cli/CommandEmbed.h new file mode 100644 index 0000000000..c0619fd694 --- /dev/null +++ b/src/cli/CommandEmbed.h @@ -0,0 +1,35 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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::cli { + +/** + * Embeds fonts and images into a PAGX file as base64, producing a self-contained output. + * + * Flags: + * -o, --output Output file path (default: overwrite input) + * --file Register a font file (repeatable) + * --fallback Add a fallback font file or system font name (repeatable) + * --skip-fonts Skip the font-embed code path entirely + * --skip-images Skip the image-embed code path entirely + */ +int RunEmbed(int argc, char* argv[]); + +} // namespace pagx::cli From 849b4a601e17ea0ebd8ebf48be3a0df72b2bfc9c Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 12:05:03 +0800 Subject: [PATCH 05/87] Add pagx embed command orchestrating font and image embedding with skip flags. --- src/cli/CommandEmbed.cpp | 145 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/cli/CommandEmbed.cpp diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp new file mode 100644 index 0000000000..64221386b8 --- /dev/null +++ b/src/cli/CommandEmbed.cpp @@ -0,0 +1,145 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "cli/CommandEmbed.h" +#include +#include +#include +#include "cli/CliUtils.h" +#include "pagx/FontConfig.h" +#include "pagx/PAGXExporter.h" +#include "renderer/FontEmbedder.h" +#include "renderer/ImageEmbedder.h" + +struct EmbedOptions { + std::string inputFile = {}; + std::string outputFile = {}; + std::vector fontFiles = {}; + std::vector fallbacks = {}; + bool skipFonts = false; + bool skipImages = false; +}; + +static void PrintEmbedUsage() { + std::cout + << "Usage: pagx embed [options] \n" + << "\n" + << "Embed fonts and images into a PAGX file as base64.\n" + << "\n" + << "Options:\n" + << " -o, --output Output file path (default: overwrite input)\n" + << " --file Register a font file (can be specified multiple\n" + << " times)\n" + << " --fallback Add a fallback font file or system font name (can\n" + << " be specified multiple times)\n" + << " --skip-fonts Do not embed fonts\n" + << " --skip-images Do not embed images\n" + << " -h, --help Show this help message\n"; +} + +static int ParseEmbedOptions(int argc, char* argv[], EmbedOptions* options) { + int i = 1; + while (i < argc) { + std::string arg = argv[i]; + if ((arg == "-o" || arg == "--output") && i + 1 < argc) { + options->outputFile = argv[++i]; + } else if (arg == "--file" && i + 1 < argc) { + options->fontFiles.push_back(argv[++i]); + } else if (arg == "--fallback" && i + 1 < argc) { + options->fallbacks.push_back(argv[++i]); + } else if (arg == "--skip-fonts") { + options->skipFonts = true; + } else if (arg == "--skip-images") { + options->skipImages = true; + } else if (arg == "--help" || arg == "-h") { + PrintEmbedUsage(); + return -1; + } else if (arg[0] == '-') { + std::cerr << "pagx embed: unknown option '" << arg << "'\n"; + return 1; + } else if (options->inputFile.empty()) { + options->inputFile = arg; + } else { + std::cerr << "pagx embed: unexpected argument '" << arg << "'\n"; + return 1; + } + i++; + } + if (options->inputFile.empty()) { + std::cerr << "pagx embed: missing input file\n"; + return 1; + } + if (options->outputFile.empty()) { + options->outputFile = options->inputFile; + } + return 0; +} + +namespace pagx::cli { + +int RunEmbed(int argc, char* argv[]) { + EmbedOptions options = {}; + auto parseResult = ParseEmbedOptions(argc, argv, &options); + if (parseResult != 0) { + return parseResult == -1 ? 0 : parseResult; + } + + if (options.skipFonts && options.skipImages) { + std::cerr << "pagx embed: --skip-fonts and --skip-images cannot both be set\n"; + return 1; + } + + auto document = LoadDocument(options.inputFile, "pagx embed"); + if (document == nullptr) { + return 1; + } + if (document->hasUnresolvedImports()) { + std::cerr << "pagx embed: error: unresolved import directive, run 'pagx resolve' first\n"; + return 1; + } + + if (!options.skipFonts) { + FontConfig fontConfig = {}; + if (!LoadFontConfig(&fontConfig, options.fontFiles, options.fallbacks, "pagx embed")) { + return 1; + } + FontEmbedder::ClearEmbeddedGlyphRuns(document.get()); + document->applyLayout(&fontConfig); + FontEmbedder embedder = {}; + if (!embedder.embed(document.get())) { + std::cerr << "pagx embed: font embedding failed\n"; + return 1; + } + } + + if (!options.skipImages) { + ImageEmbedder imageEmbedder = {}; + if (!imageEmbedder.embed(document.get())) { + std::cerr << "pagx embed: failed to load image '" << imageEmbedder.lastErrorPath() << "'\n"; + return 1; + } + } + + auto xml = PAGXExporter::ToXML(*document); + if (!WriteStringToFile(xml, options.outputFile, "pagx embed")) { + return 1; + } + return 0; +} + +} // namespace pagx::cli From 14351016002996fed38d24894ee38e75a4b0ab81 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 12:06:19 +0800 Subject: [PATCH 06/87] Register pagx embed command and update top-level usage. --- src/cli/main.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cli/main.cpp b/src/cli/main.cpp index e90c32e092..1f9265c39d 100644 --- a/src/cli/main.cpp +++ b/src/cli/main.cpp @@ -20,6 +20,7 @@ #include #include #include "cli/CommandBounds.h" +#include "cli/CommandEmbed.h" #include "cli/CommandExport.h" #include "cli/CommandFont.h" #include "cli/CommandFormat.h" @@ -44,7 +45,8 @@ static void PrintUsage() { << " layout Display layout tree with bounds\n" << " render Render PAGX to an image file (supports crop and scale)\n" << " bounds Query rendered pixel bounds of layers (for crop regions)\n" - << " font Query font metrics or embed fonts into a PAGX file\n" + << " font Query font metrics\n" + << " embed Embed fonts and images into a PAGX file\n" << " format Format a PAGX file (indentation and attribute ordering)\n" << " import Import from another format (e.g. SVG) to PAGX\n" << " export Export a PAGX file to another format (e.g. SVG)\n" @@ -89,6 +91,9 @@ int main(int argc, char* argv[]) { if (command == "font") { return pagx::cli::RunFont(argc - 1, argv + 1); } + if (command == "embed") { + return pagx::cli::RunEmbed(argc - 1, argv + 1); + } if (command == "format") { return pagx::cli::RunFormat(argc - 1, argv + 1); } From c864a08805918f1903a654a128b05a7b5558ae61 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 12:06:55 +0800 Subject: [PATCH 07/87] Add embed_sample pagx and png fixtures for pagx embed CLI tests. --- resources/cli/embed_sample.pagx | 18 ++++++++++++++++++ resources/cli/embed_sample.png | 3 +++ 2 files changed, 21 insertions(+) create mode 100644 resources/cli/embed_sample.pagx create mode 100644 resources/cli/embed_sample.png diff --git a/resources/cli/embed_sample.pagx b/resources/cli/embed_sample.pagx new file mode 100644 index 0000000000..91679a8488 --- /dev/null +++ b/resources/cli/embed_sample.pagx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/cli/embed_sample.png b/resources/cli/embed_sample.png new file mode 100644 index 0000000000..5f9916c444 --- /dev/null +++ b/resources/cli/embed_sample.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b09451e86af4533189603e795a67386f26be8896d0b05fcc9a6a96b7fa067b1 +size 4778 From 0b403b1bdc20d892a31f9669af576aac08ebdd01 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 12:17:23 +0800 Subject: [PATCH 08/87] Fix FontEmbedder::ClearEmbeddedGlyphRuns to remove stale Font and GlyphRun nodes from document. Previously only cleared Text::glyphRuns vector but left Font, Glyph, GlyphRun, PathData, and bitmap Image nodes in document->nodes, causing duplicate nodes on re-embed. --- src/renderer/FontEmbedder.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index 36da461fd5..2a8b2f0f2c 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "base/utils/Log.h" #include "base/utils/MathUtil.h" #include "pagx/TextLayout.h" @@ -383,6 +384,10 @@ static void CollectSpacingGlyph( } } +static bool IsFontNode(const std::shared_ptr& node) { + return node->nodeType() == NodeType::Font; +} + void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { if (document == nullptr) { return; @@ -391,6 +396,33 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { for (auto* text : textOrder) { text->glyphRuns.clear(); } + auto& nodes = document->nodes; + nodes.erase(std::remove_if(nodes.begin(), nodes.end(), IsFontNode), nodes.end()); + + std::unordered_set toRemove = {}; + for (auto& node : document->nodes) { + auto type = node->nodeType(); + if (type == NodeType::Font) { + toRemove.insert(node.get()); + auto* font = static_cast(node.get()); + for (auto* glyph : font->glyphs) { + toRemove.insert(glyph); + if (glyph->path != nullptr) { + toRemove.insert(glyph->path); + } + if (glyph->image != nullptr) { + toRemove.insert(glyph->image); + } + } + } else if (type == NodeType::GlyphRun) { + toRemove.insert(node.get()); + } + } + document->nodes.erase(std::remove_if(document->nodes.begin(), document->nodes.end(), + [&toRemove](const std::unique_ptr& node) { + return toRemove.count(node.get()) > 0; + }), + document->nodes.end()); } bool FontEmbedder::embed(PAGXDocument* document) { From b02b1a7c1c4acff07e1097ab7dd489116c299a12 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 12:17:30 +0800 Subject: [PATCH 09/87] Add EMBED CLI tests covering default embed skip flags missing image and idempotency. 9 CLI_TEST cases cover EMBED-01..05, EMBED-07, EMBED-08, EMBED-10, EMBED-11. --- test/src/PAGXCliTest.cpp | 172 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 869c255417..340f1b2c87 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -24,6 +24,7 @@ #include #include "base/PAGTest.h" #include "cli/CommandBounds.h" +#include "cli/CommandEmbed.h" #include "cli/CommandExport.h" #include "cli/CommandFont.h" #include "cli/CommandFormat.h" @@ -35,6 +36,7 @@ #include "pagx/PAGXDocument.h" #include "pagx/PAGXExporter.h" #include "pagx/PAGXImporter.h" +#include "pagx/nodes/Image.h" #include "tgfx/core/Bitmap.h" #include "tgfx/core/ImageCodec.h" #include "tgfx/core/Pixmap.h" @@ -2752,4 +2754,174 @@ CLI_TEST(PAGXCliTest, Verify_PainterLeakClean) { EXPECT_EQ(output.find("painter leaks geometry"), std::string::npos); } +//============================================================================== +// Embed tests +//============================================================================== + +CLI_TEST(PAGXCliTest, Embed_BothDefault_EmbedsFontsAndImages) { + auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); + auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto outPagx = TempDir() + "/embed_both_out.pagx"; + // EMBED-09 implicitly covered: embed_sample.pagx references embed_sample.png by relative path; + // resolution happens at PAGXImporter::FromFile load time per D1.2. + std::streambuf* oldCout = std::cout.rdbuf(); + std::ostringstream capturedOut; + std::cout.rdbuf(capturedOut.rdbuf()); + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", outPagx}); + std::cout.rdbuf(oldCout); + EXPECT_EQ(ret, 0); + EXPECT_NE(capturedOut.str().find("pagx embed: wrote"), std::string::npos); + auto document = pagx::PAGXImporter::FromFile(outPagx); + ASSERT_NE(document, nullptr); + EXPECT_TRUE(document->getExternalFilePaths().empty()); + bool hasImageData = false; + for (auto& node : document->nodes) { + if (node->nodeType() == pagx::NodeType::Image) { + auto* image = static_cast(node.get()); + if (image->data != nullptr) { + hasImageData = true; + } + } + } + EXPECT_TRUE(hasImageData); +} + +CLI_TEST(PAGXCliTest, Embed_SkipFonts_ImagesOnly) { + auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); + auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto outPagx = TempDir() + "/embed_skipfonts_out.pagx"; + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--skip-fonts", tempPagx, "-o", outPagx}); + EXPECT_EQ(ret, 0); + auto document = pagx::PAGXImporter::FromFile(outPagx); + ASSERT_NE(document, nullptr); + bool hasImageData = false; + for (auto& node : document->nodes) { + if (node->nodeType() == pagx::NodeType::Image) { + auto* image = static_cast(node.get()); + if (image->data != nullptr) { + hasImageData = true; + } + } + } + EXPECT_TRUE(hasImageData); +} + +CLI_TEST(PAGXCliTest, Embed_SkipImages_FontsOnly) { + auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); + auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto outPagx = TempDir() + "/embed_skipimgs_out.pagx"; + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--skip-images", tempPagx, "-o", outPagx}); + EXPECT_EQ(ret, 0); + auto document = pagx::PAGXImporter::FromFile(outPagx); + ASSERT_NE(document, nullptr); + bool hasFilePath = false; + bool hasNoImageData = true; + for (auto& node : document->nodes) { + if (node->nodeType() == pagx::NodeType::Image) { + auto* image = static_cast(node.get()); + if (!image->filePath.empty()) { + hasFilePath = true; + } + if (image->data != nullptr) { + hasNoImageData = false; + } + } + } + EXPECT_TRUE(hasFilePath); + EXPECT_TRUE(hasNoImageData); +} + +CLI_TEST(PAGXCliTest, Embed_BothSkipFlags_ExitsWithError) { + auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); + auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto contentBefore = ReadFile(tempPagx); + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--skip-fonts", "--skip-images", tempPagx}); + std::cerr.rdbuf(oldCerr); + EXPECT_EQ(ret, 1); + EXPECT_NE(capturedErr.str().find("pagx embed: --skip-fonts and --skip-images cannot both be set"), + std::string::npos); + auto contentAfter = ReadFile(tempPagx); + EXPECT_EQ(contentBefore, contentAfter); +} + +CLI_TEST(PAGXCliTest, Embed_FontFlags_AcceptedLikeOldSubcommand) { + auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); + auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto outPagx = TempDir() + "/embed_fontflags_out.pagx"; + auto fontPath = ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf"); + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--file", fontPath, "-o", outPagx, tempPagx}); + EXPECT_EQ(ret, 0); +} + +CLI_TEST(PAGXCliTest, Embed_AlreadyEmbeddedImage_IsNoOp) { + auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); + auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto out1 = TempDir() + "/embed_idempot_pass1.pagx"; + auto out2 = TempDir() + "/embed_idempot_pass2.pagx"; + auto ret1 = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", out1}); + EXPECT_EQ(ret1, 0); + auto ret2 = CallRun(pagx::cli::RunEmbed, {"embed", out1, "-o", out2}); + EXPECT_EQ(ret2, 0); + EXPECT_EQ(ReadFile(out1), ReadFile(out2)); +} + +CLI_TEST(PAGXCliTest, Embed_MissingImage_FailsLoud) { + auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_missing.pagx"); + auto content = ReadFile(tempPagx); + auto pos = content.find("embed_sample.png"); + ASSERT_NE(pos, std::string::npos); + content.replace(pos, strlen("embed_sample.png"), "missing.png"); + std::ofstream out(tempPagx); + out << content; + out.close(); + auto outPagx = TempDir() + "/embed_missing_out.pagx"; + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", outPagx}); + std::cerr.rdbuf(oldCerr); + EXPECT_EQ(ret, 1); + EXPECT_NE(capturedErr.str().find("pagx embed: failed to load image '"), std::string::npos); + EXPECT_FALSE(std::filesystem::exists(outPagx)); +} + +CLI_TEST(PAGXCliTest, Embed_Success_PrintsWroteAndExitsZero) { + auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); + auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto outPagx = TempDir() + "/embed_success_out.pagx"; + std::streambuf* oldCout = std::cout.rdbuf(); + std::ostringstream capturedOut; + std::cout.rdbuf(capturedOut.rdbuf()); + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", outPagx}); + std::cout.rdbuf(oldCout); + EXPECT_EQ(ret, 0); + auto output = capturedOut.str(); + EXPECT_NE(output.find("pagx embed: wrote"), std::string::npos); + EXPECT_NE(output.find(outPagx), std::string::npos); +} + +CLI_TEST(PAGXCliTest, Embed_Usage_NoInputErrors_HelpPrints) { + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); + auto ret = CallRun(pagx::cli::RunEmbed, {"embed"}); + std::cerr.rdbuf(oldCerr); + EXPECT_EQ(ret, 1); + EXPECT_NE(capturedErr.str().find("pagx embed: missing input file"), std::string::npos); + + std::streambuf* oldCout = std::cout.rdbuf(); + std::ostringstream capturedOut; + std::cout.rdbuf(capturedOut.rdbuf()); + auto helpRet = CallRun(pagx::cli::RunEmbed, {"embed", "--help"}); + std::cout.rdbuf(oldCout); + EXPECT_EQ(helpRet, 0); + auto helpOutput = capturedOut.str(); + EXPECT_NE(helpOutput.find("Usage: pagx embed"), std::string::npos); + EXPECT_NE(helpOutput.find("--skip-fonts"), std::string::npos); + EXPECT_NE(helpOutput.find("--skip-images"), std::string::npos); +} + } // namespace pag From 78de811b3d92d2a52c6a21208d1b542941500272 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 12:18:46 +0800 Subject: [PATCH 10/87] Fix FontEmbedder::ClearEmbeddedGlyphRuns to remove stale Font and GlyphRun nodes from document. Previously only cleared Text::glyphRuns vector but left Font, Glyph, GlyphRun, PathData, and bitmap Image nodes in document->nodes, causing duplicate nodes on re-embed. --- src/renderer/FontEmbedder.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index 2a8b2f0f2c..aaf3fbc755 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -384,10 +384,6 @@ static void CollectSpacingGlyph( } } -static bool IsFontNode(const std::shared_ptr& node) { - return node->nodeType() == NodeType::Font; -} - void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { if (document == nullptr) { return; @@ -396,8 +392,6 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { for (auto* text : textOrder) { text->glyphRuns.clear(); } - auto& nodes = document->nodes; - nodes.erase(std::remove_if(nodes.begin(), nodes.end(), IsFontNode), nodes.end()); std::unordered_set toRemove = {}; for (auto& node : document->nodes) { From 9da0451f99fdcdd8bcefc0778f162d8a20d14211 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 12:22:23 +0800 Subject: [PATCH 11/87] Replace lambda with explicit loop in FontEmbedder::ClearEmbeddedGlyphRuns to follow coding convention. --- src/renderer/FontEmbedder.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index aaf3fbc755..f742acc29a 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -412,11 +412,14 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { toRemove.insert(node.get()); } } - document->nodes.erase(std::remove_if(document->nodes.begin(), document->nodes.end(), - [&toRemove](const std::unique_ptr& node) { - return toRemove.count(node.get()) > 0; - }), - document->nodes.end()); + auto& nodes = document->nodes; + size_t writeIdx = 0; + for (size_t readIdx = 0; readIdx < nodes.size(); readIdx++) { + if (toRemove.count(nodes[readIdx].get()) == 0) { + nodes[writeIdx++] = std::move(nodes[readIdx]); + } + } + nodes.resize(writeIdx); } bool FontEmbedder::embed(PAGXDocument* document) { From 2961465804253e0323dbdc3910d1ea002051b30f Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 14:20:51 +0800 Subject: [PATCH 12/87] Retire pagx font embed subcommand with redirect error and drop dead helpers. --- src/cli/CommandFont.cpp | 104 +--------------------------------------- 1 file changed, 2 insertions(+), 102 deletions(-) diff --git a/src/cli/CommandFont.cpp b/src/cli/CommandFont.cpp index ae65a30826..8c5c4843e8 100644 --- a/src/cli/CommandFont.cpp +++ b/src/cli/CommandFont.cpp @@ -21,10 +21,7 @@ #include #include #include -#include #include "cli/CliUtils.h" -#include "pagx/PAGXExporter.h" -#include "renderer/FontEmbedder.h" #include "tgfx/core/Font.h" #include "tgfx/core/Typeface.h" @@ -162,103 +159,6 @@ static int RunFontInfo(int argc, char* argv[]) { return 0; } -// ---- font embed ---- - -struct FontEmbedOptions { - std::string inputFile = {}; - std::string outputFile = {}; - std::vector fontFiles = {}; - std::vector fallbacks = {}; -}; - -static void PrintFontEmbedUsage() { - std::cout - << "Usage: pagx font embed [options] \n" - << "\n" - << "Embed fonts into a PAGX file by performing text layout and glyph extraction.\n" - << "\n" - << "Options:\n" - << " -o, --output Output file path (default: overwrite input)\n" - << " --file Register a font file (can be specified multiple\n" - << " times)\n" - << " --fallback Add a fallback font file or system font name (can\n" - << " be specified multiple times)\n" - << " -h, --help Show this help message\n"; -} - -// Returns 0 on success, -1 if help was printed, 1 on error. -static int ParseFontEmbedOptions(int argc, char* argv[], FontEmbedOptions* options) { - int i = 1; - while (i < argc) { - std::string arg = argv[i]; - if ((arg == "-o" || arg == "--output") && i + 1 < argc) { - options->outputFile = argv[++i]; - } else if (arg == "--file" && i + 1 < argc) { - options->fontFiles.push_back(argv[++i]); - } else if (arg == "--fallback" && i + 1 < argc) { - options->fallbacks.push_back(argv[++i]); - } else if (arg == "--help" || arg == "-h") { - PrintFontEmbedUsage(); - return -1; - } else if (arg[0] == '-') { - std::cerr << "pagx font embed: unknown option '" << arg << "'\n"; - return 1; - } else if (options->inputFile.empty()) { - options->inputFile = arg; - } else { - std::cerr << "pagx font embed: unexpected argument '" << arg << "'\n"; - return 1; - } - i++; - } - if (options->inputFile.empty()) { - std::cerr << "pagx font embed: missing input file\n"; - return 1; - } - if (options->outputFile.empty()) { - options->outputFile = options->inputFile; - } - return 0; -} - -static int RunFontEmbed(int argc, char* argv[]) { - FontEmbedOptions options = {}; - auto parseResult = ParseFontEmbedOptions(argc, argv, &options); - if (parseResult != 0) { - return parseResult == -1 ? 0 : parseResult; - } - - auto document = LoadDocument(options.inputFile, "pagx font embed"); - if (document == nullptr) { - return 1; - } - if (document->hasUnresolvedImports()) { - std::cerr << "pagx font embed: error: unresolved import directive, run 'pagx resolve' first\n"; - return 1; - } - - FontConfig fontConfig = {}; - if (!LoadFontConfig(&fontConfig, options.fontFiles, options.fallbacks, "pagx font embed")) { - return 1; - } - - FontEmbedder::ClearEmbeddedGlyphRuns(document.get()); - document->applyLayout(&fontConfig); - - FontEmbedder embedder = {}; - if (!embedder.embed(document.get())) { - std::cerr << "pagx font embed: font embedding failed\n"; - return 1; - } - - auto xml = PAGXExporter::ToXML(*document); - if (!WriteStringToFile(xml, options.outputFile, "pagx font embed")) { - return 1; - } - - return 0; -} - // ---- main entry ---- static void PrintFontUsage() { @@ -266,7 +166,6 @@ static void PrintFontUsage() { << "\n" << "Subcommands:\n" << " info Query font identity and metrics\n" - << " embed Embed fonts into a PAGX file\n" << "\n" << "Run 'pagx font --help' for details.\n"; } @@ -288,7 +187,8 @@ int RunFont(int argc, char* argv[]) { return RunFontInfo(argc - 1, argv + 1); } if (subcommand == "embed") { - return RunFontEmbed(argc - 1, argv + 1); + std::cerr << "pagx font: 'embed' subcommand has been removed, use 'pagx embed' instead\n"; + return 1; } std::cerr << "pagx font: unknown subcommand '" << subcommand << "'\n"; From 7e7d61e7271dae7402357a818364702122e32f77 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 14:21:36 +0800 Subject: [PATCH 13/87] Revise CommandFont header doc comment to query-only scope. --- src/cli/CommandFont.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cli/CommandFont.h b/src/cli/CommandFont.h index 8eb819fa11..0d9b28c934 100644 --- a/src/cli/CommandFont.h +++ b/src/cli/CommandFont.h @@ -21,11 +21,10 @@ namespace pagx::cli { /** - * Manages font operations: querying font metrics and embedding fonts into PAGX files. + * Queries font metrics. * * Subcommands: * info - Query font identity and metrics from a file or system font - * embed - Embed fonts into a PAGX file with optional fallback list */ int RunFont(int argc, char* argv[]); From 103b09b3fec4a0c6323328cdf60003af25932948 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 14:23:35 +0800 Subject: [PATCH 14/87] Add FONT-01 CLI test asserting pagx font embed redirect error. --- test/src/PAGXCliTest.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 340f1b2c87..254b7957e8 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -548,6 +548,33 @@ CLI_TEST(PAGXCliTest, Font_UnknownSubcommand) { EXPECT_NE(ret, 0); } +CLI_TEST(PAGXCliTest, FontEmbed_Retired_PrintsRedirectError) { + const std::string expected = + "pagx font: 'embed' subcommand has been removed, use 'pagx embed' instead"; + + // Variant 1: with a positional argument + { + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); + auto ret = CallRun(pagx::cli::RunFont, {"font", "embed", "some.pagx"}); + std::cerr.rdbuf(oldCerr); + EXPECT_EQ(ret, 1); + EXPECT_NE(capturedErr.str().find(expected), std::string::npos); + } + + // Variant 2: no extra arguments + { + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); + auto ret = CallRun(pagx::cli::RunFont, {"font", "embed"}); + std::cerr.rdbuf(oldCerr); + EXPECT_EQ(ret, 1); + EXPECT_NE(capturedErr.str().find(expected), std::string::npos); + } +} + //============================================================================== // Lint tests //============================================================================== From 097a12793530552774dfee995738bd6c5181f33a Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 16:38:27 +0800 Subject: [PATCH 15/87] Add std::nothrow to ImageEmbedder buffer allocation to honor no-exceptions policy. --- src/renderer/ImageEmbedder.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/ImageEmbedder.cpp b/src/renderer/ImageEmbedder.cpp index 871b6b2b16..ce22bfe149 100644 --- a/src/renderer/ImageEmbedder.cpp +++ b/src/renderer/ImageEmbedder.cpp @@ -29,7 +29,8 @@ static std::shared_ptr ReadFileToData(const std::string& path) { if (end <= 0) return nullptr; // covers tellg failure and empty file auto size = static_cast(end); in.seekg(0, std::ios::beg); - auto* buffer = new uint8_t[size]; + auto* buffer = new (std::nothrow) uint8_t[size]; + if (buffer == nullptr) return nullptr; in.read(reinterpret_cast(buffer), static_cast(size)); if (!in || static_cast(in.gcount()) != size) { delete[] buffer; From f7436e63c26a5c49492f6fad4caf59af53e9405c Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 16:39:11 +0800 Subject: [PATCH 16/87] Document full reset contract of FontEmbedder::ClearEmbeddedGlyphRuns including node removal. --- src/renderer/FontEmbedder.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/renderer/FontEmbedder.h b/src/renderer/FontEmbedder.h index 43f56394f7..0cdfcc3571 100644 --- a/src/renderer/FontEmbedder.h +++ b/src/renderer/FontEmbedder.h @@ -36,9 +36,15 @@ class FontEmbedder { FontEmbedder() = default; /** - * Clears existing embedded GlyphRuns from all Text nodes in the document. Call this before - * applyLayout() when re-embedding a file that already has embedded fonts, so that layout - * performs runtime shaping instead of using stale embedded data. + * Resets previously-embedded font data in the document so it can be re-embedded from scratch. + * Clears the embedded GlyphRuns vector on every Text node and removes previously-installed + * Font nodes (along with their Glyph, PathData, and Image children) plus any orphan GlyphRun + * nodes from document->nodes. Call this before applyLayout() when re-embedding a file that + * already has embedded fonts, so that layout performs runtime shaping instead of using stale + * embedded data. + * + * Warning: any external pointers or IDs referencing the Image or PathData nodes previously + * installed by a font-embed pass will become dangling after this call. */ static void ClearEmbeddedGlyphRuns(PAGXDocument* document); From 56e0165f3a77adc30de26174f519c5b786af6eaf Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 23 Apr 2026 17:29:13 +0800 Subject: [PATCH 17/87] Trim CommandEmbed header doc to a summary and defer flag list to PrintEmbedUsage. --- src/cli/CommandEmbed.h | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/cli/CommandEmbed.h b/src/cli/CommandEmbed.h index c0619fd694..76fefb48f1 100644 --- a/src/cli/CommandEmbed.h +++ b/src/cli/CommandEmbed.h @@ -22,13 +22,7 @@ namespace pagx::cli { /** * Embeds fonts and images into a PAGX file as base64, producing a self-contained output. - * - * Flags: - * -o, --output Output file path (default: overwrite input) - * --file Register a font file (repeatable) - * --fallback Add a fallback font file or system font name (repeatable) - * --skip-fonts Skip the font-embed code path entirely - * --skip-images Skip the image-embed code path entirely + * Run `pagx embed --help` for the full flag list. */ int RunEmbed(int argc, char* argv[]); From c5d04d1d91ac5ea0ac7fe9d494fcdb308e4427d4 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 14:32:20 +0800 Subject: [PATCH 18/87] Add FontFamilyEntry struct and AllFontFamilies declaration to SystemFonts.h. --- src/pagx/SystemFonts.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/pagx/SystemFonts.h b/src/pagx/SystemFonts.h index 220935681d..8169824189 100644 --- a/src/pagx/SystemFonts.h +++ b/src/pagx/SystemFonts.h @@ -34,6 +34,15 @@ struct FontLocation { int ttcIndex = 0; }; +/** + * Describes a single system font family and its available styles. Returned by + * SystemFonts::AllFontFamilies for font enumeration purposes. + */ +struct FontFamilyEntry { + std::string family = {}; + std::vector styles = {}; +}; + /** * Provides access to system fallback fonts by querying native platform APIs. On macOS, this uses * CTFontCopyDefaultCascadeListForLanguages with the user's language preferences. On Linux, this @@ -47,6 +56,15 @@ class SystemFonts { * language preferences. No Typeface objects are created; callers should load fonts on demand. */ static std::vector FallbackTypefaces(); + + /** + * Returns every installed system font family with its available styles. On failure + * (platform API unavailable, enumeration fails), returns an empty vector silently. + * Styles within a family are deduplicated (first-occurrence-wins). Order within + * styles is platform-native order. Order of families across the vector is NOT + * guaranteed (callers should sort as needed). + */ + static std::vector AllFontFamilies(); }; } // namespace pagx From ebcebb76f518981dcb9f5f065ef2dd333dbc67d7 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 14:46:13 +0800 Subject: [PATCH 19/87] Implement SystemFonts::AllFontFamilies for macOS, Windows, Linux, and generic fallback. --- src/pagx/SystemFonts.cpp | 248 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index bbadcb38f8..e98f0865fd 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -160,11 +160,93 @@ std::vector SystemFonts::FallbackTypefaces() { return fallbacks; } +std::vector SystemFonts::AllFontFamilies() { + CFArrayRef familyNames = CTFontManagerCopyAvailableFontFamilyNames(); + if (familyNames == nullptr) { + return {}; + } + + std::vector entries = {}; + CFIndex familyCount = CFArrayGetCount(familyNames); + entries.reserve(static_cast(familyCount)); + + // Build a reusable mandatory-attributes set containing only kCTFontFamilyNameAttribute so that + // CTFontDescriptorCreateMatchingFontDescriptors returns every descriptor whose family name + // matches exactly — i.e. every member of the family. + const void* mandatoryKeys[] = {kCTFontFamilyNameAttribute}; + CFSetRef mandatoryAttributes = + CFSetCreate(kCFAllocatorDefault, mandatoryKeys, 1, &kCFTypeSetCallBacks); + + for (CFIndex i = 0; i < familyCount; i++) { + auto cfFamilyName = static_cast(CFArrayGetValueAtIndex(familyNames, i)); + if (cfFamilyName == nullptr) { + continue; + } + auto familyStr = StringFromCFString(cfFamilyName); + if (familyStr.empty()) { + continue; + } + + FontFamilyEntry entry = {}; + entry.family = std::move(familyStr); + + const void* attrKeys[] = {kCTFontFamilyNameAttribute}; + const void* attrValues[] = {cfFamilyName}; + CFDictionaryRef attributes = + CFDictionaryCreate(kCFAllocatorDefault, attrKeys, attrValues, 1, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + if (attributes != nullptr) { + CTFontDescriptorRef familyDescriptor = CTFontDescriptorCreateWithAttributes(attributes); + CFRelease(attributes); + if (familyDescriptor != nullptr) { + CFArrayRef members = + CTFontDescriptorCreateMatchingFontDescriptors(familyDescriptor, mandatoryAttributes); + CFRelease(familyDescriptor); + if (members != nullptr) { + CFIndex memberCount = CFArrayGetCount(members); + std::set seenStyles = {}; + entry.styles.reserve(static_cast(memberCount)); + + for (CFIndex j = 0; j < memberCount; j++) { + auto descriptor = static_cast(CFArrayGetValueAtIndex(members, j)); + if (descriptor == nullptr) { + continue; + } + auto cfStyle = static_cast( + CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute)); + if (cfStyle == nullptr) { + continue; + } + auto styleStr = StringFromCFString(cfStyle); + CFRelease(cfStyle); + if (styleStr.empty()) { + continue; + } + if (seenStyles.insert(styleStr).second) { + entry.styles.push_back(std::move(styleStr)); + } + } + CFRelease(members); + } + } + } + + entries.push_back(std::move(entry)); + } + + if (mandatoryAttributes != nullptr) { + CFRelease(mandatoryAttributes); + } + CFRelease(familyNames); + return entries; +} + } // namespace pagx #elif defined(_WIN32) #include +#include #include #pragma comment(lib, "dwrite.lib") @@ -220,6 +302,37 @@ static std::string GetFamilyName(IDWriteFontFamily* fontFamily) { return WideToUTF8(wide.c_str(), static_cast(length)); } +static std::string GetFaceName(IDWriteFont* font) { + IDWriteLocalizedStrings* names = nullptr; + HRESULT hr = font->GetFaceNames(&names); + if (FAILED(hr) || names == nullptr) { + return {}; + } + + UINT32 index = 0; + BOOL exists = FALSE; + names->FindLocaleName(L"en-us", &index, &exists); + if (!exists) { + index = 0; + } + + UINT32 length = 0; + hr = names->GetStringLength(index, &length); + if (FAILED(hr) || length == 0) { + SafeRelease(&names); + return {}; + } + + std::wstring wide(static_cast(length) + 1, L'\0'); + hr = names->GetString(index, wide.data(), length + 1); + SafeRelease(&names); + if (FAILED(hr)) { + return {}; + } + + return WideToUTF8(wide.c_str(), static_cast(length)); +} + std::vector SystemFonts::FallbackTypefaces() { IDWriteFactory* factory = nullptr; HRESULT hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), @@ -262,12 +375,81 @@ std::vector SystemFonts::FallbackTypefaces() { return fallbacks; } +std::vector SystemFonts::AllFontFamilies() { + IDWriteFactory* factory = nullptr; + HRESULT hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), + reinterpret_cast(&factory)); + if (FAILED(hr) || factory == nullptr) { + return {}; + } + + IDWriteFontCollection* fontCollection = nullptr; + hr = factory->GetSystemFontCollection(&fontCollection); + if (FAILED(hr) || fontCollection == nullptr) { + SafeRelease(&factory); + return {}; + } + + UINT32 familyCount = fontCollection->GetFontFamilyCount(); + std::vector entries = {}; + entries.reserve(familyCount); + + for (UINT32 i = 0; i < familyCount; i++) { + IDWriteFontFamily* fontFamily = nullptr; + hr = fontCollection->GetFontFamily(i, &fontFamily); + if (FAILED(hr) || fontFamily == nullptr) { + continue; + } + + auto familyName = GetFamilyName(fontFamily); + if (familyName.empty()) { + SafeRelease(&fontFamily); + continue; + } + + FontFamilyEntry entry = {}; + entry.family = std::move(familyName); + + UINT32 fontCount = fontFamily->GetFontCount(); + std::set seenStyles = {}; + entry.styles.reserve(fontCount); + + for (UINT32 j = 0; j < fontCount; j++) { + IDWriteFont* font = nullptr; + hr = fontFamily->GetFont(j, &font); + if (FAILED(hr) || font == nullptr) { + continue; + } + if (font->GetSimulations() != DWRITE_FONT_SIMULATIONS_NONE) { + SafeRelease(&font); + continue; + } + auto faceName = GetFaceName(font); + SafeRelease(&font); + if (faceName.empty()) { + continue; + } + if (seenStyles.insert(faceName).second) { + entry.styles.push_back(std::move(faceName)); + } + } + + SafeRelease(&fontFamily); + entries.push_back(std::move(entry)); + } + + SafeRelease(&fontCollection); + SafeRelease(&factory); + return entries; +} + } // namespace pagx #elif defined(__linux__) #include #include +#include #include #include @@ -336,6 +518,68 @@ std::vector SystemFonts::FallbackTypefaces() { return fallbacks; } +std::vector SystemFonts::AllFontFamilies() { + FcPattern* pattern = FcPatternCreate(); + if (pattern == nullptr) { + return {}; + } + FcObjectSet* objectSet = FcObjectSetBuild(FC_FAMILY, FC_STYLE, (char*)0); + if (objectSet == nullptr) { + FcPatternDestroy(pattern); + return {}; + } + FcFontSet* fontSet = FcFontList(nullptr, pattern, objectSet); + FcObjectSetDestroy(objectSet); + FcPatternDestroy(pattern); + if (fontSet == nullptr) { + return {}; + } + + std::vector entries = {}; + std::map familyIndex = {}; + std::vector > seenStylesPerEntry = {}; + + for (int i = 0; i < fontSet->nfont; i++) { + FcPattern* font = fontSet->fonts[i]; + FcChar8* familyRaw = nullptr; + if (FcPatternGetString(font, FC_FAMILY, 0, &familyRaw) != FcResultMatch || + familyRaw == nullptr) { + continue; + } + std::string familyStr(reinterpret_cast(familyRaw)); + if (familyStr.empty()) { + continue; + } + + auto it = familyIndex.find(familyStr); + if (it == familyIndex.end()) { + FontFamilyEntry entry = {}; + entry.family = familyStr; + entries.push_back(std::move(entry)); + seenStylesPerEntry.push_back({}); + familyIndex[familyStr] = entries.size() - 1; + it = familyIndex.find(familyStr); + } + + FcChar8* styleRaw = nullptr; + if (FcPatternGetString(font, FC_STYLE, 0, &styleRaw) != FcResultMatch || styleRaw == nullptr) { + continue; + } + std::string styleStr(reinterpret_cast(styleRaw)); + if (styleStr.empty()) { + continue; + } + + size_t idx = it->second; + if (seenStylesPerEntry[idx].insert(styleStr).second) { + entries[idx].styles.push_back(std::move(styleStr)); + } + } + + FcFontSetDestroy(fontSet); + return entries; +} + } // namespace pagx #else @@ -346,6 +590,10 @@ std::vector SystemFonts::FallbackTypefaces() { return {}; } +std::vector SystemFonts::AllFontFamilies() { + return {}; +} + } // namespace pagx #endif From f0794c7c04535d790ccc56a67ff864f3fb596992 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 14:56:10 +0800 Subject: [PATCH 20/87] Flatten pagx font into a single query command and delete the info subcommand layer. --- src/cli/CommandFont.cpp | 126 +++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 73 deletions(-) diff --git a/src/cli/CommandFont.cpp b/src/cli/CommandFont.cpp index 8c5c4843e8..da28c4638b 100644 --- a/src/cli/CommandFont.cpp +++ b/src/cli/CommandFont.cpp @@ -18,7 +18,6 @@ #include "cli/CommandFont.h" #include -#include #include #include #include "cli/CliUtils.h" @@ -27,80 +26,98 @@ namespace pagx::cli { -// ---- font info ---- +// ---- font query ---- -struct FontInfoOptions { +struct FontOptions { std::string fontFile = {}; std::string fontName = {}; float fontSize = 12.0f; bool jsonOutput = false; }; -static void PrintFontInfoUsage() { - std::cout << "Usage: pagx font info [options]\n" +static void PrintFontUsage() { + std::cout << "Usage: pagx font [options]\n" << "\n" - << "Query font identity and metrics.\n" + << "Query a font file, a system font by name, or enumerate system font families.\n" << "\n" << "Options:\n" - << " --file Font file path\n" - << " --name System font name (e.g., \"Arial\" or \"Arial,Bold\")\n" + << " --file Query a font file\n" + << " --name Query a system font (e.g., \"Arial\" or \"Arial,Bold\")\n" << " --size Font size in points (default: 12)\n" << " --json Output in JSON format\n" + << " --list List every installed system font family\n" + << " -h, --help Show this help\n" << "\n" - << "Either --file or --name is required (mutually exclusive).\n"; + << "Exactly one of --file, --name, or --list must be specified.\n"; } -// Returns 0 on success, -1 if help was printed, 1 on error. -static int ParseFontInfoOptions(int argc, char* argv[], FontInfoOptions* options) { +int RunFont(int argc, char* argv[]) { + if (argc < 2) { + PrintFontUsage(); + return 1; + } + + std::string first = argv[1]; + if (first == "--help" || first == "-h") { + PrintFontUsage(); + return 0; + } + + if (first == "info") { + std::cerr << "pagx font: 'info' subcommand has been removed, use 'pagx font' instead\n"; + return 1; + } + + if (first == "embed") { + std::cerr << "pagx font: 'embed' subcommand has been removed, use 'pagx embed' instead\n"; + return 1; + } + + FontOptions options = {}; int i = 1; while (i < argc) { std::string arg = argv[i]; if (arg == "--file" && i + 1 < argc) { - options->fontFile = argv[++i]; + options.fontFile = argv[++i]; } else if (arg == "--name" && i + 1 < argc) { - options->fontName = argv[++i]; + options.fontName = argv[++i]; } else if (arg == "--size" && i + 1 < argc) { char* endPtr = nullptr; - options->fontSize = strtof(argv[++i], &endPtr); - if (endPtr == argv[i] || *endPtr != '\0' || !std::isfinite(options->fontSize) || - options->fontSize <= 0.0f) { - std::cerr << "pagx font info: invalid font size '" << argv[i] << "'\n"; + options.fontSize = strtof(argv[++i], &endPtr); + if (endPtr == argv[i] || *endPtr != '\0' || !std::isfinite(options.fontSize) || + options.fontSize <= 0.0f) { + std::cerr << "pagx font: invalid font size '" << argv[i] << "'\n"; return 1; } } else if (arg == "--json") { - options->jsonOutput = true; + options.jsonOutput = true; } else if (arg == "--help" || arg == "-h") { - PrintFontInfoUsage(); - return -1; + PrintFontUsage(); + return 0; } else if (arg[0] == '-') { - std::cerr << "pagx font info: unknown option '" << arg << "'\n"; + std::cerr << "pagx font: unknown option '" << arg << "'\n"; + return 1; + } else { + std::cerr << "pagx font: unexpected argument '" << arg << "'\n"; return 1; } i++; } - if (options->fontFile.empty() && options->fontName.empty()) { - std::cerr << "pagx font info: either --file or --name is required\n"; + + if (!options.fontFile.empty() && !options.fontName.empty()) { + std::cerr << "pagx font: --file and --name are mutually exclusive\n"; return 1; } - if (!options->fontFile.empty() && !options->fontName.empty()) { - std::cerr << "pagx font info: --file and --name are mutually exclusive\n"; + if (options.fontFile.empty() && options.fontName.empty()) { + std::cerr << "pagx font: either --file or --name is required\n"; return 1; } - return 0; -} - -static int RunFontInfo(int argc, char* argv[]) { - FontInfoOptions options = {}; - auto parseResult = ParseFontInfoOptions(argc, argv, &options); - if (parseResult != 0) { - return parseResult == -1 ? 0 : parseResult; - } std::shared_ptr typeface = nullptr; if (!options.fontFile.empty()) { typeface = tgfx::Typeface::MakeFromPath(options.fontFile); if (typeface == nullptr) { - std::cerr << "pagx font info: failed to load font file '" << options.fontFile << "'\n"; + std::cerr << "pagx font: failed to load font file '" << options.fontFile << "'\n"; return 1; } } else { @@ -111,7 +128,7 @@ static int RunFontInfo(int argc, char* argv[]) { commaPos != std::string::npos ? options.fontName.substr(commaPos + 1) : std::string(); typeface = ResolveSystemTypeface(family, style); if (typeface == nullptr) { - std::cerr << "pagx font info: font '" << options.fontName << "' not found\n"; + std::cerr << "pagx font: font '" << options.fontName << "' not found\n"; return 1; } } @@ -159,41 +176,4 @@ static int RunFontInfo(int argc, char* argv[]) { return 0; } -// ---- main entry ---- - -static void PrintFontUsage() { - std::cout << "Usage: pagx font [options]\n" - << "\n" - << "Subcommands:\n" - << " info Query font identity and metrics\n" - << "\n" - << "Run 'pagx font --help' for details.\n"; -} - -int RunFont(int argc, char* argv[]) { - if (argc < 2) { - PrintFontUsage(); - return 1; - } - - std::string subcommand = argv[1]; - - if (subcommand == "--help" || subcommand == "-h") { - PrintFontUsage(); - return 0; - } - - if (subcommand == "info") { - return RunFontInfo(argc - 1, argv + 1); - } - if (subcommand == "embed") { - std::cerr << "pagx font: 'embed' subcommand has been removed, use 'pagx embed' instead\n"; - return 1; - } - - std::cerr << "pagx font: unknown subcommand '" << subcommand << "'\n"; - std::cerr << "Run 'pagx font --help' for usage.\n"; - return 1; -} - } // namespace pagx::cli From dcfdf363f26fa16e6f4bc531ce0c6527ff0e8848 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 14:58:18 +0800 Subject: [PATCH 21/87] Rename FontInfo tests to Font, add info redirect and help surface tests. --- test/src/PAGXCliTest.cpp | 65 +++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 254b7957e8..35d48ef256 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -516,33 +516,78 @@ CLI_TEST(PAGXCliTest, Render_XPathNoMatch) { // Font tests //============================================================================== -CLI_TEST(PAGXCliTest, FontInfo_FromFile) { +CLI_TEST(PAGXCliTest, Font_FromFile) { auto fontPath = ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf"); - auto ret = CallRun(pagx::cli::RunFont, {"font", "info", "--file", fontPath}); + auto ret = CallRun(pagx::cli::RunFont, {"font", "--file", fontPath}); EXPECT_EQ(ret, 0); } -CLI_TEST(PAGXCliTest, FontInfo_JsonOutput) { +CLI_TEST(PAGXCliTest, Font_JsonOutput) { auto fontPath = ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf"); - auto ret = CallRun(pagx::cli::RunFont, {"font", "info", "--file", fontPath, "--json"}); + auto ret = CallRun(pagx::cli::RunFont, {"font", "--file", fontPath, "--json"}); EXPECT_EQ(ret, 0); } -CLI_TEST(PAGXCliTest, FontInfo_FileNotFound) { - auto ret = CallRun(pagx::cli::RunFont, {"font", "info", "--file", "nonexistent.ttf"}); +CLI_TEST(PAGXCliTest, Font_FileNotFound) { + auto ret = CallRun(pagx::cli::RunFont, {"font", "--file", "nonexistent.ttf"}); EXPECT_NE(ret, 0); } -CLI_TEST(PAGXCliTest, FontInfo_MutualExclusive) { - auto ret = CallRun(pagx::cli::RunFont, {"font", "info", "--file", "x.ttf", "--name", "Arial"}); +CLI_TEST(PAGXCliTest, Font_MutualExclusive) { + auto ret = CallRun(pagx::cli::RunFont, {"font", "--file", "x.ttf", "--name", "Arial"}); EXPECT_NE(ret, 0); } -CLI_TEST(PAGXCliTest, FontInfo_NoSource) { - auto ret = CallRun(pagx::cli::RunFont, {"font", "info"}); +CLI_TEST(PAGXCliTest, Font_NoSource) { + auto ret = CallRun(pagx::cli::RunFont, {"font"}); EXPECT_NE(ret, 0); } +CLI_TEST(PAGXCliTest, FontInfo_Retired_PrintsRedirectError) { + const std::string expected = + "pagx font: 'info' subcommand has been removed, use 'pagx font' instead"; + + // Variant 1: with a positional argument + { + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); + auto ret = CallRun(pagx::cli::RunFont, {"font", "info", "--file", "x.otf"}); + std::cerr.rdbuf(oldCerr); + EXPECT_EQ(ret, 1); + EXPECT_NE(capturedErr.str().find(expected), std::string::npos); + } + + // Variant 2: no extra arguments + { + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); + auto ret = CallRun(pagx::cli::RunFont, {"font", "info"}); + std::cerr.rdbuf(oldCerr); + EXPECT_EQ(ret, 1); + EXPECT_NE(capturedErr.str().find(expected), std::string::npos); + } +} + +CLI_TEST(PAGXCliTest, Font_HelpShowsNewSurface) { + std::streambuf* oldCout = std::cout.rdbuf(); + std::ostringstream capturedOut; + std::cout.rdbuf(capturedOut.rdbuf()); + auto ret = CallRun(pagx::cli::RunFont, {"font", "--help"}); + std::cout.rdbuf(oldCout); + + EXPECT_EQ(ret, 0); + auto help = capturedOut.str(); + EXPECT_NE(help.find("--list"), std::string::npos); + EXPECT_NE(help.find("--file"), std::string::npos); + EXPECT_NE(help.find("--name"), std::string::npos); + EXPECT_NE(help.find("--size"), std::string::npos); + EXPECT_NE(help.find("--json"), std::string::npos); + EXPECT_EQ(help.find("embed"), std::string::npos); + EXPECT_EQ(help.find("info"), std::string::npos); +} + CLI_TEST(PAGXCliTest, Font_UnknownSubcommand) { auto ret = CallRun(pagx::cli::RunFont, {"font", "xyz"}); EXPECT_NE(ret, 0); From 15b57ea7fd79c7ac22f4c954b78278d491f83779 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 15:04:02 +0800 Subject: [PATCH 22/87] Add --list flag parsing, validation, and system font list formatting to pagx font. --- src/cli/CommandFont.cpp | 64 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/src/cli/CommandFont.cpp b/src/cli/CommandFont.cpp index da28c4638b..340ebbadd8 100644 --- a/src/cli/CommandFont.cpp +++ b/src/cli/CommandFont.cpp @@ -17,10 +17,13 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "cli/CommandFont.h" +#include #include #include #include +#include #include "cli/CliUtils.h" +#include "pagx/SystemFonts.h" #include "tgfx/core/Font.h" #include "tgfx/core/Typeface.h" @@ -33,6 +36,7 @@ struct FontOptions { std::string fontName = {}; float fontSize = 12.0f; bool jsonOutput = false; + bool listMode = false; }; static void PrintFontUsage() { @@ -51,6 +55,45 @@ static void PrintFontUsage() { << "Exactly one of --file, --name, or --list must be specified.\n"; } +static bool FontFamilyLess(const pagx::FontFamilyEntry& lhs, const pagx::FontFamilyEntry& rhs) { + return lhs.family < rhs.family; +} + +static void FormatFontListText(const std::vector& entries) { + for (const auto& entry : entries) { + if (entry.styles.empty()) { + std::cout << entry.family << "\n"; + continue; + } + std::cout << entry.family << " ("; + for (size_t i = 0; i < entry.styles.size(); i++) { + if (i > 0) { + std::cout << ", "; + } + std::cout << entry.styles[i]; + } + std::cout << ")\n"; + } +} + +static void FormatFontListJson(const std::vector& entries) { + std::cout << "["; + for (size_t i = 0; i < entries.size(); i++) { + if (i > 0) { + std::cout << ","; + } + std::cout << "{\"family\":\"" << EscapeJson(entries[i].family) << "\",\"styles\":["; + for (size_t j = 0; j < entries[i].styles.size(); j++) { + if (j > 0) { + std::cout << ","; + } + std::cout << "\"" << EscapeJson(entries[i].styles[j]) << "\""; + } + std::cout << "]}"; + } + std::cout << "]\n"; +} + int RunFont(int argc, char* argv[]) { if (argc < 2) { PrintFontUsage(); @@ -91,6 +134,8 @@ int RunFont(int argc, char* argv[]) { } } else if (arg == "--json") { options.jsonOutput = true; + } else if (arg == "--list") { + options.listMode = true; } else if (arg == "--help" || arg == "-h") { PrintFontUsage(); return 0; @@ -104,15 +149,30 @@ int RunFont(int argc, char* argv[]) { i++; } + if (options.listMode && (!options.fontFile.empty() || !options.fontName.empty())) { + std::cerr << "pagx font: --list cannot be combined with --file or --name\n"; + return 1; + } if (!options.fontFile.empty() && !options.fontName.empty()) { std::cerr << "pagx font: --file and --name are mutually exclusive\n"; return 1; } - if (options.fontFile.empty() && options.fontName.empty()) { - std::cerr << "pagx font: either --file or --name is required\n"; + if (!options.listMode && options.fontFile.empty() && options.fontName.empty()) { + std::cerr << "pagx font: either --file, --name, or --list is required\n"; return 1; } + if (options.listMode) { + auto entries = pagx::SystemFonts::AllFontFamilies(); + std::sort(entries.begin(), entries.end(), FontFamilyLess); + if (options.jsonOutput) { + FormatFontListJson(entries); + } else { + FormatFontListText(entries); + } + return 0; + } + std::shared_ptr typeface = nullptr; if (!options.fontFile.empty()) { typeface = tgfx::Typeface::MakeFromPath(options.fontFile); From 3b31a255472d000cad296efca4971a62fa0f3bc9 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 15:05:27 +0800 Subject: [PATCH 23/87] Add FontList text, JSON, and mutual exclusion tests for pagx font --list. --- test/src/PAGXCliTest.cpp | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 35d48ef256..1f0f5f92d1 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -588,6 +588,64 @@ CLI_TEST(PAGXCliTest, Font_HelpShowsNewSurface) { EXPECT_EQ(help.find("info"), std::string::npos); } +CLI_TEST(PAGXCliTest, FontList_TextOutput) { + std::streambuf* oldCout = std::cout.rdbuf(); + std::ostringstream capturedOut; + std::cout.rdbuf(capturedOut.rdbuf()); + auto ret = CallRun(pagx::cli::RunFont, {"font", "--list"}); + std::cout.rdbuf(oldCout); + + EXPECT_EQ(ret, 0); + auto out = capturedOut.str(); + EXPECT_FALSE(out.empty()); + EXPECT_NE(out.find('\n'), std::string::npos); +} + +CLI_TEST(PAGXCliTest, FontList_JsonOutput) { + std::streambuf* oldCout = std::cout.rdbuf(); + std::ostringstream capturedOut; + std::cout.rdbuf(capturedOut.rdbuf()); + auto ret = CallRun(pagx::cli::RunFont, {"font", "--list", "--json"}); + std::cout.rdbuf(oldCout); + + EXPECT_EQ(ret, 0); + auto out = capturedOut.str(); + EXPECT_FALSE(out.empty()); + auto trimEnd = out.find_last_not_of(" \t\r\n"); + ASSERT_NE(trimEnd, std::string::npos); + auto trimStart = out.find_first_not_of(" \t\r\n"); + ASSERT_NE(trimStart, std::string::npos); + EXPECT_EQ(out[trimStart], '['); + EXPECT_EQ(out[trimEnd], ']'); + EXPECT_NE(out.find("{\"family\":"), std::string::npos); +} + +CLI_TEST(PAGXCliTest, FontList_MutualExclusive) { + const std::string expected = "pagx font: --list cannot be combined with --file or --name"; + + // Variant 1: --list + --file + { + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); + auto ret = CallRun(pagx::cli::RunFont, {"font", "--list", "--file", "x.otf"}); + std::cerr.rdbuf(oldCerr); + EXPECT_EQ(ret, 1); + EXPECT_NE(capturedErr.str().find(expected), std::string::npos); + } + + // Variant 2: --list + --name + { + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); + auto ret = CallRun(pagx::cli::RunFont, {"font", "--list", "--name", "Arial"}); + std::cerr.rdbuf(oldCerr); + EXPECT_EQ(ret, 1); + EXPECT_NE(capturedErr.str().find(expected), std::string::npos); + } +} + CLI_TEST(PAGXCliTest, Font_UnknownSubcommand) { auto ret = CallRun(pagx::cli::RunFont, {"font", "xyz"}); EXPECT_NE(ret, 0); From a6e759b8cce0e1d1b56bf4a0160bcb19f827100c Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 16:54:16 +0800 Subject: [PATCH 24/87] Update CommandFont.h comment to reflect removed subcommands. --- src/cli/CommandFont.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/cli/CommandFont.h b/src/cli/CommandFont.h index 0d9b28c934..f15d2e2e9b 100644 --- a/src/cli/CommandFont.h +++ b/src/cli/CommandFont.h @@ -21,10 +21,7 @@ namespace pagx::cli { /** - * Queries font metrics. - * - * Subcommands: - * info - Query font identity and metrics from a file or system font + * Queries font metrics from a file or system font. */ int RunFont(int argc, char* argv[]); From 1bad5ebea538d8a778a537d1b8b86dfc59a135f6 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 16:54:40 +0800 Subject: [PATCH 25/87] Eliminate redundant map lookup in SystemFonts Linux AllFontFamilies. --- src/pagx/SystemFonts.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index e98f0865fd..db7c6406e9 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -557,8 +557,7 @@ std::vector SystemFonts::AllFontFamilies() { entry.family = familyStr; entries.push_back(std::move(entry)); seenStylesPerEntry.push_back({}); - familyIndex[familyStr] = entries.size() - 1; - it = familyIndex.find(familyStr); + it = familyIndex.insert({familyStr, entries.size() - 1}).first; } FcChar8* styleRaw = nullptr; From 32f58fe7dcd1e18a4db2e6b9ab2b71fc0ea53dd4 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 16:54:48 +0800 Subject: [PATCH 26/87] Move EmbedOptions and helpers inside namespace pagx::cli. --- src/cli/CommandEmbed.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp index 64221386b8..43ea3577cd 100644 --- a/src/cli/CommandEmbed.cpp +++ b/src/cli/CommandEmbed.cpp @@ -26,6 +26,8 @@ #include "renderer/FontEmbedder.h" #include "renderer/ImageEmbedder.h" +namespace pagx::cli { + struct EmbedOptions { std::string inputFile = {}; std::string outputFile = {}; @@ -90,8 +92,6 @@ static int ParseEmbedOptions(int argc, char* argv[], EmbedOptions* options) { return 0; } -namespace pagx::cli { - int RunEmbed(int argc, char* argv[]) { EmbedOptions options = {}; auto parseResult = ParseEmbedOptions(argc, argv, &options); From b8c47d25f1f131e8e326d989d5731576298a82d6 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 16:54:56 +0800 Subject: [PATCH 27/87] Rename misleading Font_HelpShowsNewSurface test to Font_HelpShowsCurrentFlags. --- test/src/PAGXCliTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 1f0f5f92d1..321a5ac648 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -570,7 +570,7 @@ CLI_TEST(PAGXCliTest, FontInfo_Retired_PrintsRedirectError) { } } -CLI_TEST(PAGXCliTest, Font_HelpShowsNewSurface) { +CLI_TEST(PAGXCliTest, Font_HelpShowsCurrentFlags) { std::streambuf* oldCout = std::cout.rdbuf(); std::ostringstream capturedOut; std::cout.rdbuf(capturedOut.rdbuf()); From a3b86aaf139af866b40bc51117b08b0c6144e964 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 17:00:54 +0800 Subject: [PATCH 28/87] Update npm README to reflect renamed font and embed commands. --- cli/npm/README.md | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/cli/npm/README.md b/cli/npm/README.md index 00c4dbb764..299a65830d 100644 --- a/cli/npm/README.md +++ b/cli/npm/README.md @@ -22,8 +22,8 @@ npm install -g @libpag/pagx | `pagx optimize` | Validate, optimize, and format in one step | | `pagx format` | Format a PAGX file with consistent indentation and attribute ordering | | `pagx bounds` | Query the precise rendered bounds of layers | -| `pagx font info` | Query font identity and metrics from a file or system font | -| `pagx font embed` | Embed fonts into a PAGX file with glyph extraction | +| `pagx font` | Query a font file, a system font by name, or list system font families | +| `pagx embed` | Embed fonts and images into a PAGX file as base64 | ## Usage Examples @@ -65,13 +65,22 @@ pagx bounds --xpath "//Layer[@id='btn']" input.pagx pagx bounds --xpath "//Layer[@id='icon']" --relative "//Layer[@id='card']" --json input.pagx # Query system font metrics at 24pt -pagx font info --name "Arial" --size 24 +pagx font --name "Arial" --size 24 # Query font metrics from a file -pagx font info --file CustomFont.ttf --json +pagx font --file CustomFont.ttf --json -# Embed fonts with a custom fallback -pagx font embed --file BrandFont.ttf --fallback "Arial" input.pagx +# List all installed system font families +pagx font --list + +# Embed fonts and images into a PAGX file +pagx embed input.pagx + +# Embed with a custom font file and fallback +pagx embed --file BrandFont.ttf --fallback "Arial" input.pagx + +# Embed images only (skip font embedding) +pagx embed --skip-fonts input.pagx ``` ## Command Reference @@ -147,26 +156,30 @@ coordinates by default. `--id` and `--xpath` are mutually exclusive. Without either, outputs bounds for all layers. -### `pagx font info [options]` +### `pagx font [options]` -Query font identity and metrics. Requires either `--file` or `--name` (mutually exclusive). +Query a font file, a system font by name, or list system font families. Exactly one of `--file`, +`--name`, or `--list` must be specified. | Option | Description | |--------|-------------| -| `--file ` | Font file path | -| `--name ` | System font name (e.g., `"Arial"` or `"Arial,Bold"`) | +| `--file ` | Query a font file | +| `--name ` | Query a system font (e.g., `"Arial"` or `"Arial,Bold"`) | | `--size ` | Font size in points (default: `12`) | | `--json` | Output in JSON format | +| `--list` | List every installed system font family | -### `pagx font embed [options] ` +### `pagx embed [options] ` -Embed fonts into a PAGX file by performing text layout and glyph extraction. +Embed fonts and images into a PAGX file as base64. | Option | Description | |--------|-------------| | `-o, --output ` | Output file path (default: overwrite input) | | `--file ` | Register a font file (repeatable) | | `--fallback ` | Add a fallback font file or system font name (repeatable) | +| `--skip-fonts` | Do not embed fonts | +| `--skip-images` | Do not embed images | ## Supported Platforms From 43d0ebd8a85cc864f729fdda9c6f6c47846bd722 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 17:01:00 +0800 Subject: [PATCH 29/87] Add font-not-embedded assertion to Embed_SkipFonts_ImagesOnly test. --- test/src/PAGXCliTest.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 321a5ac648..406d40c5c1 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -2925,6 +2925,7 @@ CLI_TEST(PAGXCliTest, Embed_SkipFonts_ImagesOnly) { auto document = pagx::PAGXImporter::FromFile(outPagx); ASSERT_NE(document, nullptr); bool hasImageData = false; + bool hasFontNode = false; for (auto& node : document->nodes) { if (node->nodeType() == pagx::NodeType::Image) { auto* image = static_cast(node.get()); @@ -2932,8 +2933,12 @@ CLI_TEST(PAGXCliTest, Embed_SkipFonts_ImagesOnly) { hasImageData = true; } } + if (node->nodeType() == pagx::NodeType::Font) { + hasFontNode = true; + } } EXPECT_TRUE(hasImageData); + EXPECT_FALSE(hasFontNode); } CLI_TEST(PAGXCliTest, Embed_SkipImages_FontsOnly) { From 15cb770ad14552d853d9dc5b025649731defe41c Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 17:18:52 +0800 Subject: [PATCH 30/87] Add font-embedded assertion to Embed_SkipImages_FontsOnly test. --- test/src/PAGXCliTest.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 406d40c5c1..5a54d97c00 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -2951,6 +2951,7 @@ CLI_TEST(PAGXCliTest, Embed_SkipImages_FontsOnly) { ASSERT_NE(document, nullptr); bool hasFilePath = false; bool hasNoImageData = true; + bool hasFontNode = false; for (auto& node : document->nodes) { if (node->nodeType() == pagx::NodeType::Image) { auto* image = static_cast(node.get()); @@ -2961,9 +2962,13 @@ CLI_TEST(PAGXCliTest, Embed_SkipImages_FontsOnly) { hasNoImageData = false; } } + if (node->nodeType() == pagx::NodeType::Font) { + hasFontNode = true; + } } EXPECT_TRUE(hasFilePath); EXPECT_TRUE(hasNoImageData); + EXPECT_TRUE(hasFontNode); } CLI_TEST(PAGXCliTest, Embed_BothSkipFlags_ExitsWithError) { From 5cdebbf695bc817fda6c170d37955dce03bf548d Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 17:18:59 +0800 Subject: [PATCH 31/87] Deduplicate file reads for identical paths in ImageEmbedder. --- src/renderer/ImageEmbedder.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/ImageEmbedder.cpp b/src/renderer/ImageEmbedder.cpp index ce22bfe149..11fa429292 100644 --- a/src/renderer/ImageEmbedder.cpp +++ b/src/renderer/ImageEmbedder.cpp @@ -18,6 +18,7 @@ #include "renderer/ImageEmbedder.h" #include +#include #include "pagx/types/Data.h" namespace pagx { @@ -42,10 +43,14 @@ static std::shared_ptr ReadFileToData(const std::string& path) { bool ImageEmbedder::embed(PAGXDocument* document) { if (document == nullptr) return false; auto paths = document->getExternalFilePaths(); + std::unordered_set loaded; for (const auto& path : paths) { if (path.find("://") != std::string::npos) { continue; // URL per D1.3 — silently skip } + if (!loaded.insert(path).second) { + continue; + } auto data = ReadFileToData(path); if (data == nullptr) { lastErrorPath_ = path; From 0091dd1052e5b23d1d9968a0ad7510dcf32113af Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 17:19:06 +0800 Subject: [PATCH 32/87] Warn when --size is used with --list in pagx font command. --- src/cli/CommandFont.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cli/CommandFont.cpp b/src/cli/CommandFont.cpp index 340ebbadd8..6a7daec855 100644 --- a/src/cli/CommandFont.cpp +++ b/src/cli/CommandFont.cpp @@ -163,6 +163,9 @@ int RunFont(int argc, char* argv[]) { } if (options.listMode) { + if (options.fontSize != 12.0f) { + std::cerr << "pagx font: warning: --size is ignored in --list mode\n"; + } auto entries = pagx::SystemFonts::AllFontFamilies(); std::sort(entries.begin(), entries.end(), FontFamilyLess); if (options.jsonOutput) { From a627d272b772b102a1bbf3956ddfcb5182f0d3c4 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 17:34:50 +0800 Subject: [PATCH 33/87] Fix embed command description to distinguish font glyph embedding from image base64 encoding. --- src/cli/CommandEmbed.cpp | 2 +- src/cli/CommandEmbed.h | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp index 43ea3577cd..8fc5632089 100644 --- a/src/cli/CommandEmbed.cpp +++ b/src/cli/CommandEmbed.cpp @@ -41,7 +41,7 @@ static void PrintEmbedUsage() { std::cout << "Usage: pagx embed [options] \n" << "\n" - << "Embed fonts and images into a PAGX file as base64.\n" + << "Embed font glyphs and images into a PAGX file for self-contained output.\n" << "\n" << "Options:\n" << " -o, --output Output file path (default: overwrite input)\n" diff --git a/src/cli/CommandEmbed.h b/src/cli/CommandEmbed.h index 76fefb48f1..04917dd312 100644 --- a/src/cli/CommandEmbed.h +++ b/src/cli/CommandEmbed.h @@ -21,8 +21,9 @@ namespace pagx::cli { /** - * Embeds fonts and images into a PAGX file as base64, producing a self-contained output. - * Run `pagx embed --help` for the full flag list. + * Embeds font glyphs and images into a PAGX file, producing a self-contained output. + * Font embedding extracts glyph paths/images from laid-out text; image embedding inlines + * external files as base64. Run `pagx embed --help` for the full flag list. */ int RunEmbed(int argc, char* argv[]); From 5bb47f954b25fa0f510513a780c0f4cfbbb536a7 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 24 Apr 2026 18:00:48 +0800 Subject: [PATCH 34/87] Reuse existing image_as_mask.png for embed tests instead of a duplicated fixture. --- resources/cli/embed_sample.pagx | 2 +- resources/cli/embed_sample.png | 3 --- test/src/PAGXCliTest.cpp | 28 ++++++++++++++++++---------- 3 files changed, 19 insertions(+), 14 deletions(-) delete mode 100644 resources/cli/embed_sample.png diff --git a/resources/cli/embed_sample.pagx b/resources/cli/embed_sample.pagx index 91679a8488..8b17a92120 100644 --- a/resources/cli/embed_sample.pagx +++ b/resources/cli/embed_sample.pagx @@ -13,6 +13,6 @@ - + diff --git a/resources/cli/embed_sample.png b/resources/cli/embed_sample.png deleted file mode 100644 index 5f9916c444..0000000000 --- a/resources/cli/embed_sample.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b09451e86af4533189603e795a67386f26be8896d0b05fcc9a6a96b7fa067b1 -size 4778 diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 5a54d97c00..a1ce01595e 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -69,6 +69,14 @@ static std::string CopyToTemp(const std::string& resourceName, const std::string return dst; } +static std::string CopyResourceToTemp(const std::string& resourceRelPath, + const std::string& tempName) { + auto src = ProjectPath::Absolute(resourceRelPath); + auto dst = TempDir() + "/" + tempName; + std::filesystem::copy_file(src, dst, std::filesystem::copy_options::overwrite_existing); + return dst; +} + static bool CompareRenderedImage(const std::string& imagePath, const std::string& key) { auto codec = ImageCodec::MakeFrom(imagePath); if (codec == nullptr) { @@ -2890,9 +2898,9 @@ CLI_TEST(PAGXCliTest, Verify_PainterLeakClean) { CLI_TEST(PAGXCliTest, Embed_BothDefault_EmbedsFontsAndImages) { auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); - auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); auto outPagx = TempDir() + "/embed_both_out.pagx"; - // EMBED-09 implicitly covered: embed_sample.pagx references embed_sample.png by relative path; + // EMBED-09 implicitly covered: embed_sample.pagx references image_as_mask.png by relative path; // resolution happens at PAGXImporter::FromFile load time per D1.2. std::streambuf* oldCout = std::cout.rdbuf(); std::ostringstream capturedOut; @@ -2918,7 +2926,7 @@ CLI_TEST(PAGXCliTest, Embed_BothDefault_EmbedsFontsAndImages) { CLI_TEST(PAGXCliTest, Embed_SkipFonts_ImagesOnly) { auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); - auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); auto outPagx = TempDir() + "/embed_skipfonts_out.pagx"; auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--skip-fonts", tempPagx, "-o", outPagx}); EXPECT_EQ(ret, 0); @@ -2943,7 +2951,7 @@ CLI_TEST(PAGXCliTest, Embed_SkipFonts_ImagesOnly) { CLI_TEST(PAGXCliTest, Embed_SkipImages_FontsOnly) { auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); - auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); auto outPagx = TempDir() + "/embed_skipimgs_out.pagx"; auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--skip-images", tempPagx, "-o", outPagx}); EXPECT_EQ(ret, 0); @@ -2973,7 +2981,7 @@ CLI_TEST(PAGXCliTest, Embed_SkipImages_FontsOnly) { CLI_TEST(PAGXCliTest, Embed_BothSkipFlags_ExitsWithError) { auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); - auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); auto contentBefore = ReadFile(tempPagx); std::streambuf* oldCerr = std::cerr.rdbuf(); std::ostringstream capturedErr; @@ -2989,7 +2997,7 @@ CLI_TEST(PAGXCliTest, Embed_BothSkipFlags_ExitsWithError) { CLI_TEST(PAGXCliTest, Embed_FontFlags_AcceptedLikeOldSubcommand) { auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); - auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); auto outPagx = TempDir() + "/embed_fontflags_out.pagx"; auto fontPath = ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf"); auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--file", fontPath, "-o", outPagx, tempPagx}); @@ -2998,7 +3006,7 @@ CLI_TEST(PAGXCliTest, Embed_FontFlags_AcceptedLikeOldSubcommand) { CLI_TEST(PAGXCliTest, Embed_AlreadyEmbeddedImage_IsNoOp) { auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); - auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); auto out1 = TempDir() + "/embed_idempot_pass1.pagx"; auto out2 = TempDir() + "/embed_idempot_pass2.pagx"; auto ret1 = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", out1}); @@ -3011,9 +3019,9 @@ CLI_TEST(PAGXCliTest, Embed_AlreadyEmbeddedImage_IsNoOp) { CLI_TEST(PAGXCliTest, Embed_MissingImage_FailsLoud) { auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_missing.pagx"); auto content = ReadFile(tempPagx); - auto pos = content.find("embed_sample.png"); + auto pos = content.find("image_as_mask.png"); ASSERT_NE(pos, std::string::npos); - content.replace(pos, strlen("embed_sample.png"), "missing.png"); + content.replace(pos, strlen("image_as_mask.png"), "missing.png"); std::ofstream out(tempPagx); out << content; out.close(); @@ -3030,7 +3038,7 @@ CLI_TEST(PAGXCliTest, Embed_MissingImage_FailsLoud) { CLI_TEST(PAGXCliTest, Embed_Success_PrintsWroteAndExitsZero) { auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); - auto tempPng = CopyToTemp("embed_sample.png", "embed_sample.png"); + auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); auto outPagx = TempDir() + "/embed_success_out.pagx"; std::streambuf* oldCout = std::cout.rdbuf(); std::ostringstream capturedOut; From 08f5f14d33e4308e15a7e1e599351575d11eb43c Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:11:05 +0800 Subject: [PATCH 35/87] Align npm README embed command description and flag text with CLI help output. --- cli/npm/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/npm/README.md b/cli/npm/README.md index 299a65830d..07132f6697 100644 --- a/cli/npm/README.md +++ b/cli/npm/README.md @@ -23,7 +23,7 @@ npm install -g @libpag/pagx | `pagx format` | Format a PAGX file with consistent indentation and attribute ordering | | `pagx bounds` | Query the precise rendered bounds of layers | | `pagx font` | Query a font file, a system font by name, or list system font families | -| `pagx embed` | Embed fonts and images into a PAGX file as base64 | +| `pagx embed` | Embed font glyphs and images into a PAGX file for self-contained output | ## Usage Examples @@ -171,15 +171,15 @@ Query a font file, a system font by name, or list system font families. Exactly ### `pagx embed [options] ` -Embed fonts and images into a PAGX file as base64. +Embed font glyphs and images into a PAGX file for self-contained output. | Option | Description | |--------|-------------| | `-o, --output ` | Output file path (default: overwrite input) | | `--file ` | Register a font file (repeatable) | | `--fallback ` | Add a fallback font file or system font name (repeatable) | -| `--skip-fonts` | Do not embed fonts | -| `--skip-images` | Do not embed images | +| `--skip-fonts` | Skip font embedding | +| `--skip-images` | Skip image embedding | ## Supported Platforms From 7d678ecc6f9dade25ed35781a4518028bdcefe52 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:11:35 +0800 Subject: [PATCH 36/87] Assert font node is embedded in Embed_FontFlags_AcceptedLikeOldSubcommand. --- test/src/PAGXCliTest.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index a1ce01595e..a50295dbbe 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -3002,6 +3002,15 @@ CLI_TEST(PAGXCliTest, Embed_FontFlags_AcceptedLikeOldSubcommand) { auto fontPath = ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf"); auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--file", fontPath, "-o", outPagx, tempPagx}); EXPECT_EQ(ret, 0); + auto document = pagx::PAGXImporter::FromFile(outPagx); + ASSERT_NE(document, nullptr); + bool hasFontNode = false; + for (auto& node : document->nodes) { + if (node->nodeType() == pagx::NodeType::Font) { + hasFontNode = true; + } + } + EXPECT_TRUE(hasFontNode); } CLI_TEST(PAGXCliTest, Embed_AlreadyEmbeddedImage_IsNoOp) { From 0dcaef44fc7ac814b052188f4bc3aca4a48c68a7 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:11:47 +0800 Subject: [PATCH 37/87] Assert font node is embedded in Embed_BothDefault_EmbedsFontsAndImages. --- test/src/PAGXCliTest.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index a50295dbbe..fd3054349f 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -2913,6 +2913,7 @@ CLI_TEST(PAGXCliTest, Embed_BothDefault_EmbedsFontsAndImages) { ASSERT_NE(document, nullptr); EXPECT_TRUE(document->getExternalFilePaths().empty()); bool hasImageData = false; + bool hasFontNode = false; for (auto& node : document->nodes) { if (node->nodeType() == pagx::NodeType::Image) { auto* image = static_cast(node.get()); @@ -2920,8 +2921,12 @@ CLI_TEST(PAGXCliTest, Embed_BothDefault_EmbedsFontsAndImages) { hasImageData = true; } } + if (node->nodeType() == pagx::NodeType::Font) { + hasFontNode = true; + } } EXPECT_TRUE(hasImageData); + EXPECT_TRUE(hasFontNode); } CLI_TEST(PAGXCliTest, Embed_SkipFonts_ImagesOnly) { From 9b37b0342712636ce7522bfe19eb54d7065d1519 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:12:00 +0800 Subject: [PATCH 38/87] Assert FontList text output contains at least two non-empty family lines. --- test/src/PAGXCliTest.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index fd3054349f..56147b955e 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -607,6 +607,22 @@ CLI_TEST(PAGXCliTest, FontList_TextOutput) { auto out = capturedOut.str(); EXPECT_FALSE(out.empty()); EXPECT_NE(out.find('\n'), std::string::npos); + int nonEmptyLines = 0; + size_t start = 0; + while (start < out.size()) { + size_t end = out.find('\n', start); + if (end == std::string::npos) { + end = out.size(); + } + if (end > start) { + std::string line = out.substr(start, end - start); + if (line.find_first_not_of(" \t\r") != std::string::npos) { + ++nonEmptyLines; + } + } + start = end + 1; + } + EXPECT_GE(nonEmptyLines, 2); } CLI_TEST(PAGXCliTest, FontList_JsonOutput) { From 36b1000d59af771096d729afb36f1f88cbc09e43 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:12:11 +0800 Subject: [PATCH 39/87] Loosen FontList_JsonOutput to check for family and styles tokens instead of exact prefix. --- test/src/PAGXCliTest.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 56147b955e..bcca6f4afc 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -641,7 +641,8 @@ CLI_TEST(PAGXCliTest, FontList_JsonOutput) { ASSERT_NE(trimStart, std::string::npos); EXPECT_EQ(out[trimStart], '['); EXPECT_EQ(out[trimEnd], ']'); - EXPECT_NE(out.find("{\"family\":"), std::string::npos); + EXPECT_NE(out.find("\"family\""), std::string::npos); + EXPECT_NE(out.find("\"styles\""), std::string::npos); } CLI_TEST(PAGXCliTest, FontList_MutualExclusive) { From 40ae84c9441a48f9c487c410238afb2caf3eabd5 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:12:33 +0800 Subject: [PATCH 40/87] Use word-boundary check for retired info subcommand in Font_HelpShowsCurrentFlags. --- test/src/PAGXCliTest.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index bcca6f4afc..ea697f6d5f 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -593,7 +593,9 @@ CLI_TEST(PAGXCliTest, Font_HelpShowsCurrentFlags) { EXPECT_NE(help.find("--size"), std::string::npos); EXPECT_NE(help.find("--json"), std::string::npos); EXPECT_EQ(help.find("embed"), std::string::npos); - EXPECT_EQ(help.find("info"), std::string::npos); + EXPECT_EQ(help.find(" info "), std::string::npos); + EXPECT_EQ(help.find(" info\n"), std::string::npos); + EXPECT_EQ(help.find("\n info "), std::string::npos); } CLI_TEST(PAGXCliTest, FontList_TextOutput) { From f07cedd1856c5694bffc7266af6c6a0fd36fafce Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:13:29 +0800 Subject: [PATCH 41/87] Document macOS AllFontFamilies API choice over CTFontManagerCopyAvailableMembersOfFontFamily. --- src/pagx/SystemFonts.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index db7c6406e9..d2707eb771 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -173,6 +173,9 @@ std::vector SystemFonts::AllFontFamilies() { // Build a reusable mandatory-attributes set containing only kCTFontFamilyNameAttribute so that // CTFontDescriptorCreateMatchingFontDescriptors returns every descriptor whose family name // matches exactly — i.e. every member of the family. + // Uses CTFontDescriptorCreateMatchingFontDescriptors instead of + // CTFontManagerCopyAvailableMembersOfFontFamily for consistency with the descriptor-based + // workflow; both produce equivalent family-member sets. const void* mandatoryKeys[] = {kCTFontFamilyNameAttribute}; CFSetRef mandatoryAttributes = CFSetCreate(kCFAllocatorDefault, mandatoryKeys, 1, &kCFTypeSetCallBacks); From 3735a123e2cf8cdc2014a30a30c9ec394092d3a6 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:13:47 +0800 Subject: [PATCH 42/87] Remove pre-C++11 template space in SystemFonts.cpp nested vector type. --- src/pagx/SystemFonts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index d2707eb771..c6783f9eea 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -540,7 +540,7 @@ std::vector SystemFonts::AllFontFamilies() { std::vector entries = {}; std::map familyIndex = {}; - std::vector > seenStylesPerEntry = {}; + std::vector> seenStylesPerEntry = {}; for (int i = 0; i < fontSet->nfont; i++) { FcPattern* font = fontSet->fonts[i]; From d7d838478048f44331b00c4286752d8023fa2e5f Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:14:11 +0800 Subject: [PATCH 43/87] Update SystemFonts class doc to cover AllFontFamilies in addition to fallback typefaces. --- src/pagx/SystemFonts.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pagx/SystemFonts.h b/src/pagx/SystemFonts.h index 8169824189..cdee6f6a40 100644 --- a/src/pagx/SystemFonts.h +++ b/src/pagx/SystemFonts.h @@ -44,10 +44,11 @@ struct FontFamilyEntry { }; /** - * Provides access to system fallback fonts by querying native platform APIs. On macOS, this uses - * CTFontCopyDefaultCascadeListForLanguages with the user's language preferences. On Linux, this - * uses fontconfig's FcFontSort to enumerate system fonts in priority order. On Windows, this - * enumerates the system font collection via DirectWrite. + * Provides access to system font metadata — fallback typefaces used during text rendering and + * the full list of installed font families. On macOS, this uses CoreText + * (CTFontCopyDefaultCascadeListForLanguages / CTFontManagerCopyAvailableFontFamilyNames). On + * Linux, this uses fontconfig (FcFontSort / FcFontList). On Windows, this uses DirectWrite + * (IDWriteFontCollection). */ class SystemFonts { public: From 0352774ebbc2425ce6caeed20e315ef31258c252 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:14:39 +0800 Subject: [PATCH 44/87] Track --size presence via bool flag so explicit default value still triggers warning. --- src/cli/CommandFont.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/CommandFont.cpp b/src/cli/CommandFont.cpp index 6a7daec855..46b1f1a83d 100644 --- a/src/cli/CommandFont.cpp +++ b/src/cli/CommandFont.cpp @@ -35,6 +35,7 @@ struct FontOptions { std::string fontFile = {}; std::string fontName = {}; float fontSize = 12.0f; + bool sizeSpecified = false; bool jsonOutput = false; bool listMode = false; }; @@ -132,6 +133,7 @@ int RunFont(int argc, char* argv[]) { std::cerr << "pagx font: invalid font size '" << argv[i] << "'\n"; return 1; } + options.sizeSpecified = true; } else if (arg == "--json") { options.jsonOutput = true; } else if (arg == "--list") { @@ -163,7 +165,7 @@ int RunFont(int argc, char* argv[]) { } if (options.listMode) { - if (options.fontSize != 12.0f) { + if (options.sizeSpecified) { std::cerr << "pagx font: warning: --size is ignored in --list mode\n"; } auto entries = pagx::SystemFonts::AllFontFamilies(); From a601ce13417c646eadfd985a105f0717b439f15a Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:17:14 +0800 Subject: [PATCH 45/87] Revert template space fix that conflicts with project clang-format configuration. --- src/pagx/SystemFonts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index c6783f9eea..d2707eb771 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -540,7 +540,7 @@ std::vector SystemFonts::AllFontFamilies() { std::vector entries = {}; std::map familyIndex = {}; - std::vector> seenStylesPerEntry = {}; + std::vector > seenStylesPerEntry = {}; for (int i = 0; i < fontSet->nfont; i++) { FcPattern* font = fontSet->fonts[i]; From 2ed17a17e60d4a818297694a96ea2dcd87452f27 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 11:20:58 +0800 Subject: [PATCH 46/87] codeformat --- src/cli/CommandEmbed.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp index 8fc5632089..5689c6b6ee 100644 --- a/src/cli/CommandEmbed.cpp +++ b/src/cli/CommandEmbed.cpp @@ -49,8 +49,8 @@ static void PrintEmbedUsage() { << " times)\n" << " --fallback Add a fallback font file or system font name (can\n" << " be specified multiple times)\n" - << " --skip-fonts Do not embed fonts\n" - << " --skip-images Do not embed images\n" + << " --skip-fonts Skip font embedding\n" + << " --skip-images Skip image embedding\n" << " -h, --help Show this help message\n"; } From da92e93e3db74d33247eef1c0ff97e5793d367e3 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 27 Apr 2026 14:33:35 +0800 Subject: [PATCH 47/87] Revert LayerBuilder.cpp and .gitignore to main branch state. --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 01e71ec915..1c2e2b27e5 100644 --- a/.gitignore +++ b/.gitignore @@ -48,9 +48,6 @@ local.properties # CodeBuddy .codebuddy/designs/ -# GSD workflow (local-only planning docs) -.planning/ - # Local config *.local.json *.local.md From 2c79ea405b8e7e31a119114d63feacf8f116fd09 Mon Sep 17 00:00:00 2001 From: codywwang Date: Tue, 28 Apr 2026 15:41:36 +0800 Subject: [PATCH 48/87] Add codebase map to .planning/codebase/. --- .planning/codebase/ARCHITECTURE.md | 204 +++++++++++++++++ .planning/codebase/CONCERNS.md | 236 +++++++++++++++++++ .planning/codebase/CONVENTIONS.md | 166 ++++++++++++++ .planning/codebase/INTEGRATIONS.md | 318 ++++++++++++++++++++++++++ .planning/codebase/STACK.md | 172 ++++++++++++++ .planning/codebase/STRUCTURE.md | 353 +++++++++++++++++++++++++++++ .planning/codebase/TESTING.md | 214 +++++++++++++++++ 7 files changed, 1663 insertions(+) create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000000..bb8b1f87c6 --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,204 @@ + +# Architecture + +**Analysis Date:** 2025-07-15 + +## System Overview + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ Public API Layer │ +│ include/pag/pag.h include/pag/file.h include/pag/decoder.h │ +│ C API: include/pag/c/ │ PAGX API: include/pagx/ │ +└──────────┬──────────────┬───────────────────────┬────────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────────────┐ ┌──────────────────────────────┐ +│ Codec Layer │ │ Rendering Layer │ │ PAGX / CLI Tools │ +│ src/codec/ │ │ src/rendering/ │ │ src/pagx/ src/cli/ │ +│ (decode / │ │ (playback + │ │ (XML import/export, │ +│ encode │ │ GPU submit) │ │ SVG, command-line tool) │ +│ PAG binary)│ │ │ │ │ +└──────┬───────┘ └────────┬─────────┘ └──────────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Data Model src/base/ │ +│ Composition, Layer, Shape, Effect, Keyframe, Transform, Sequence │ +└──────────────────────────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Text Layout Engine src/renderer/ │ +│ HarfBuzz shaping, line breaking, BiDi, font embedding │ +└──────────────────────────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ TGFX 2D Graphics Engine (third_party/tgfx) │ +│ GPU backend (OpenGL/Metal/Vulkan), path rendering, image decoding │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Platform Abstraction src/platform/ │ +│ android/ ios/ mac/ cocoa/ win/ linux/ ohos/ web/ qt/ swiftshader/ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Core Subsystems + +| Subsystem | Location | Responsibility | +|-----------|----------|----------------| +| Data Model | `src/base/` | PAG scene graph: Compositions, Layers, Shapes, Effects, Keyframes | +| Codec | `src/codec/` | Tag-based binary PAG format encode/decode; LZ4 compression; MP4 muxing | +| Rendering | `src/rendering/` | PAGPlayer/PAGSurface/PAGStage, frame caches, GPU draw submission | +| Text Layout | `src/renderer/` | HarfBuzz text shaping, BiDi, line breaking, font embedding | +| PAGX | `src/pagx/` | XML format import/export, SVG support, layout engine | +| CLI | `src/cli/` | `pagx-cli` commands: embed, export, import, font, render, verify, etc. | +| Platform | `src/platform/` | GPU context init, hardware video decode, system font loading, event loop | +| C Bindings | `src/c/` | C language API wrapping the C++ public API | +| Public C++ API | `include/pag/` | `PAGPlayer`, `PAGFile`, `PAGSurface`, `PAGImage`, `PAGFont`, etc. | +| PAGX Public API | `include/pagx/` | `PAGXImporter`, `PAGXExporter`, `PAGXDocument`, node/type headers | + +## Data Flow + +### Primary Playback Path + +1. **Load PAG file** — `PAGFile::Load(path)` (`include/pag/file.h`, `src/rendering/layers/PAGFile.cpp`) +2. **Binary decode** — `Codec::Decode()` reads tag stream (`src/codec/Codec.cpp`) +3. **Build object tree** — `Composition`, `Layer`, `Shape` objects in `src/base/` +4. **Create player** — `PAGPlayer` owns `PAGStage` + `RenderCache` (`src/rendering/PAGPlayer.cpp`) +5. **Attach surface** — `PAGPlayer::setSurface(PAGSurface)` wraps platform `Drawable` +6. **Set progress** — `PAGPlayer::setProgress(t)` updates animation state on `PAGStage` +7. **Render** — `PAGPlayer::flush()` → `PAGStage::draw()` → per-layer renderers → TGFX GPU submit + +### PAGX Conversion Path + +1. **Import** — `PAGXImporter::Import(path)` parses XML via Expat (`src/pagx/PAGXImporter.cpp`) +2. **Build PAGX scene graph** — `PAGXDocument` + `pagx::Node` tree (`include/pagx/`) +3. **Export to PAG** — `PAGXExporter::Export()` serializes binary PAG (`src/pagx/PAGXExporter.cpp`) +4. **CLI command** — `CommandImport` / `CommandExport` wrappers in `src/cli/` + +### Render Frame Flow (detail) + +``` +PAGPlayer::flush() + └─ RenderCache::beginFrame() [src/rendering/caches/RenderCache.cpp] + └─ PAGStage::draw(recorder) [src/rendering/layers/PAGStage.cpp] + └─ PAGLayer::draw() [src/rendering/layers/PAGLayer.cpp] + └─ LayerRenderer::draw() [src/rendering/renderers/LayerRenderer.cpp] + ├─ ContentCache hit? [src/rendering/caches/] + ├─ ShapeRenderer / TextRenderer / FilterRenderer + └─ TrackMatteRenderer / MaskRenderer + └─ RenderCache::attachToContext() → TGFX draw calls → GPU submit +``` + +### State Management + +- All mutations guarded by `rootLocker` (a shared mutex on `PAGStage`) +- `LockGuard` / `ScopedLock` used consistently in `PAGPlayer` public methods +- Cache invalidation via `ContentVersion` dirty-tracking (`src/rendering/layers/ContentVersion.h`) + +## Layer Hierarchy + +### Composition Classes (`src/base/`) + +``` +Composition (abstract) +├─ VectorComposition — vector layers, shapes, text +├─ BitmapComposition — raster bitmap sequence +└─ VideoComposition — H.264/H.265 video sequence +``` + +### Layer Classes (`src/base/`) + +``` +Layer (abstract) +├─ ImageLayer — bitmap image content +├─ ShapeLayer — vector shape groups +├─ SolidLayer — solid color fill +├─ TextLayer — text with animator +├─ PreComposeLayer — nested composition reference +└─ CameraLayer — 3D camera +``` + +### PAG Runtime Layer Classes (`src/rendering/layers/`) + +``` +PAGLayer (public API wrapper around data-model Layer) +├─ PAGComposition — wraps Composition; supports child layers +│ ├─ PAGFile — top-level file wrapper +│ └─ PAGStage — root render graph (owns RenderCache) +├─ PAGImageLayer +├─ PAGShapeLayer +├─ PAGSolidLayer +└─ PAGTextLayer +``` + +### Renderer Classes (`src/rendering/renderers/`) + +| Renderer | Handles | +|----------|---------| +| `LayerRenderer` | Dispatch to typed renderers; apply transforms/effects | +| `ShapeRenderer` | Vector shape drawing via TGFX paths | +| `TextRenderer` | Text layout integration; glyph drawing | +| `FilterRenderer` | Effect/filter chain application | +| `CompositionRenderer` | Nested composition flattening | +| `MaskRenderer` | Alpha/luma mask application | +| `TrackMatteRenderer` | Track matte compositing | +| `TransformRenderer` | Transform/opacity application | +| `TextAnimatorRenderer` | Per-character text animation | + +### Cache Classes (`src/rendering/caches/`) + +| Cache | Stores | +|-------|--------| +| `RenderCache` | Master cache coordinator; owns GPU context attachment | +| `LayerCache` | Per-layer transform/content snapshots | +| `ShapeContentCache` | Pre-rasterized shape geometry | +| `TextContentCache` | Shaped text glyph runs | +| `ImageContentCache` | Decoded image bitmaps | +| `CompositionCache` | Flattened composition snapshots | +| `SequenceFile` | Disk-backed video/bitmap sequence cache | +| `DiskCache` | General disk I/O cache with worker thread | + +## Key Design Patterns + +### 1. Data Model / Runtime Wrapper Split +`src/base/` contains pure data (`Layer`, `Composition`, `Shape`, etc.) with no rendering logic. `src/rendering/layers/` wraps these in `PAGLayer`/`PAGComposition` runtime objects that hold cache state and expose the public API. Data objects are decoded once; runtime wrappers are created per `PAGPlayer` instance. + +### 2. Tag-Based Codec +The PAG binary format uses a tag-stream design (`src/codec/tags/`). Each feature (effect, shape type, layer style) maps to one or more numeric tag IDs. New features add new tags without breaking backward compatibility. Tags above a known version are skipped by older decoders. + +### 3. Dirty-Region Cache Invalidation +`ContentVersion` counters (`src/rendering/layers/ContentVersion.h`) track when layer content changes. Renderers check version numbers before re-rasterizing, enabling frame-to-frame content reuse without explicit cache invalidation calls. + +### 4. Platform Abstraction via `Drawable` +`PAGSurface` holds a `Drawable` interface (`src/rendering/drawables/`). Each platform implements `Drawable` to provide a TGFX `Surface` backed by the platform's GPU context (EGL on Android, CAMetalLayer on iOS/Mac, etc.). + +### 5. Sequence Decode Pipeline +Video/bitmap sequences have a dedicated async pipeline: `SequenceInfo` → `SequenceImageQueue` → hardware decoder (via platform layer) → `RenderCache` frame promotion. `DiskIOWorker` handles background I/O for disk-cached sequences. + +### 6. No Exceptions / No RTTI +The codebase prohibits `throw`/`try`/`catch` and `dynamic_cast`. Error propagation uses return values, `nullptr`, or output parameters. + +## Error Handling + +**Strategy:** Return `nullptr` / empty objects on failure; no exceptions. + +**Patterns:** +- `PAGFile::Load()` returns `nullptr` on decode failure +- `std::shared_ptr` used for all heap-allocated public objects (automatic lifetime) +- Platform implementations return empty on silent failure (e.g., `src/cli/`, `src/pagx/SystemFonts.cpp`) + +## Cross-Cutting Concerns + +**Logging:** No `LOG` macros in `src/cli/` or `src/pagx/SystemFonts.cpp`; silent empty-return convention. +**Thread safety:** `rootLocker` mutex on `PAGStage`; `LockGuard`/`ScopedLock` wrappers. +**Memory:** `std::shared_ptr` ownership throughout public API; `RenderCache` manages GPU resource lifetime. +**GPU:** All GPU calls go through TGFX (`third_party/tgfx`); libpag never calls GPU APIs directly. + +--- + +*Architecture analysis: 2025-07-15* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000000..e7b095f799 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,236 @@ +# Codebase Concerns + +**Analysis Date:** 2025-01-27 + +--- + +## Tech Debt + +### Raw Memory Management in Codec Layer + +- Issue: `src/codec/` and `src/codec/tags/` extensively use raw `new[]`/`delete[]` and manual `delete` for object lifetimes, instead of `std::unique_ptr` or `std::vector`. This includes `DecodeStream`, `EncodeStream`, `CodecContext`, `LayerTag`, `Attributes.h`, and `AttributeHelper.h`. +- Files: `src/codec/utils/EncodeStream.cpp`, `src/codec/utils/DecodeStream.h`, `src/codec/utils/EncodeStream.h`, `src/codec/tags/LayerTag.cpp`, `src/codec/AttributeHelper.h`, `src/codec/Attributes.h`, `src/codec/CodecContext.cpp`, `src/codec/DataTypes.cpp` +- Impact: Memory leak risk on early-return error paths; manual ownership tracking required when extending the codec. +- Fix approach: Migrate raw arrays to `std::vector` or `std::unique_ptr`; use RAII wrappers for C-style allocations. + +### `mutable` Members in Stream Classes (Convention Violation) + +- Issue: `DecodeStream::context` and `EncodeStream::context` are declared `mutable StreamContext*`, used to record errors from `const` read/write methods. This violates the project convention to avoid `mutable`. +- Files: `src/codec/utils/DecodeStream.h:233`, `src/codec/utils/EncodeStream.h:228` +- Impact: Misleads callers about const-correctness; mutating error state through const methods obscures whether a read produced side effects. +- Fix approach: Make all error-producing read/write methods non-const, passing context explicitly or storing it non-mutably. + +### Lambda Usage in MP4Generator and SVGExporter + +- Issue: `src/codec/mp4/MP4Generator.cpp` contains ~17 lambdas (`[&]`, `[this]`) for all write-function callbacks. `src/pagx/svg/SVGExporter.cpp` and `src/platform/web/NativeTextShaper.cpp` also use lambdas. This violates the project convention to avoid lambdas in favor of explicit methods. +- Files: `src/codec/mp4/MP4Generator.cpp`, `src/pagx/svg/SVGExporter.cpp`, `src/platform/web/NativeTextShaper.cpp` +- Impact: Harder to read call stacks in debuggers; inconsistency with the rest of the codebase. +- Fix approach: Refactor each lambda into a named private method or static function. + +### `std::function` Usage in Codec Attribute Helpers + +- Issue: `src/codec/AttributeHelper.h` stores `std::function` and `std::function` as fields in `CustomAttribute`, and `src/codec/tags/ShapeTag.cpp` uses `std::unordered_map>`. `std::function` carries heap allocation and type-erasure overhead. +- Files: `src/codec/AttributeHelper.h`, `src/codec/tags/ShapeTag.cpp` +- Impact: Performance cost in hot codec paths; indirection obscures static dispatch. +- Fix approach: Replace with function pointer templates or virtual dispatch where appropriate. + +### Dead Code: OHOS Hardware Texture Path Disabled with `if (false)` + +- Issue: `OHOSVideoDecoder::onRenderFrame()` has the hardware NativeBuffer texture path guarded by `if (false && codecCategory == HARDWARE)`. The code exists but is permanently bypassed, requiring asynchronous HarmonyOS hardware decoding to be re-enabled once the platform fixes a jitter bug. +- Files: `src/platform/ohos/OHOSVideoDecoder.cpp:264` +- Impact: HarmonyOS hardware video decoding silently falls back to software YUV path, reducing performance on real devices. The dead code path may bitrot. +- Fix approach: Re-enable when HarmonyOS fixes the jitter issue; track with an upstream HarmonyOS issue link. + +--- + +## Complexity Hotspots + +### CommandVerify.cpp — 2841 Lines + +- Files: `src/cli/CommandVerify.cpp` +- Problem: Single file handles the entire `pagx verify` command: layer validation, painter deduplication, geometry analysis, heuristic rule engine. No sub-module decomposition. +- Why it's a concern: Every new verification rule adds to an already large file; hard to unit-test individual rules in isolation. +- Improvement path: Extract each verification check into a separate `Verifier` class or function file under `src/cli/verify/`. + +### SVGImporter.cpp — 2761 Lines + +- Files: `src/pagx/svg/SVGImporter.cpp` +- Problem: Handles the complete SVG-to-PAGX translation: element parsing, geometry import, gradient import, text layout, group merging, unknown-node preservation. Monolithic single-file implementation. +- Why it's a concern: SVG spec coverage is incomplete; adding new SVG features requires navigating 2700+ lines. +- Improvement path: Split by SVG element category (shapes, text, gradients, groups) into sub-files. + +### PAGXImporter.cpp — 2202 Lines + +- Files: `src/pagx/PAGXImporter.cpp` +- Problem: Parses every PAGX XML node type in a single file with deeply nested `if`/`switch` logic per attribute. +- Why it's a concern: Adding new PAGX node attributes or tag versions requires searching through 2200 lines; error-prone when tag versions accumulate. +- Improvement path: Generate per-type parse functions from a schema or split into per-node-category files. + +### TagCode Enum — 94 Versioned Tags in Public API + +- Files: `include/pag/file.h:51–152` +- Problem: The `TagCode` enum has 94 defined codes with version suffixes (`V2`, `V3`, `Extra`, `ExtraV2`). Reserved ranges (`34~44`) indicate past breakage. The public header exposes the full enum to all consumers and pulls in RTTR headers when `PAG_USE_RTTR` is defined. +- Why it's a concern: Every new format feature requires a new `V(N+1)` tag, and decoders must handle all historical versions forever. Version proliferation makes the tag table hard to audit. +- Improvement path: Document a tag retirement policy; consider a single extensible tag with a version field for new additions. + +### TextLayout.cpp — 1585 Lines + +- Files: `src/pagx/TextLayout.cpp` +- Problem: Handles HarfBuzz shaping, line breaking, SheenBidi BiDi resolution, and glyph metric computation all in one file. +- Why it's a concern: Complex bidirectional text + shaping logic is notoriously hard to test; changes risk regressions across scripts. +- Improvement path: Unit-test individual shaping cases; split BiDi logic into `BidiLayout.cpp`. + +--- + +## Missing Abstractions + +### No Linux Platform Implementation + +- Problem: `CMakeLists.txt` references `src/platform/linux/` (line 471) for native Linux builds, but the directory does not exist. Linux is listed as a supported platform in documentation, but there is no `NativePlatform`, font loading, or GPU context code for it. +- Impact: Linux builds without `USE_NATIVE_PLATFORM` fall through to the Qt backend (`src/platform/qt/`), which only provides a GPU drawable stub without font loading or display link. +- Fix approach: Implement a `src/platform/linux/` with at minimum `NativePlatform.cpp` (fontconfig font loading) and `GPUDrawable.cpp` (EGL context). + +### No Notification Mechanism for PAGImage Scale Factor Invalidation + +- Problem: `ImageReplacement::getScaleFactor()` recomputes the content matrix on every call but has no mechanism to notify upper layers when the `PAGImage` scale mode or matrix changes, requiring callers to re-query. +- Files: `src/rendering/editing/ImageReplacement.cpp:52–57` +- Impact: Callers may cache a stale scale factor after a PAGImage replacement update. +- Fix approach: Implement a dirty/observer notification from `PAGImage` to `ImageReplacement` when scale mode or matrix changes. + +### Platform Abstraction Has No Default Display Link on Most Platforms + +- Problem: `Platform::createDisplayLink()` returns `nullptr` by default; only iOS/macOS/Android/OHOS implement it. The Qt and Win platforms do not, limiting animation-loop integration on desktop. +- Files: `src/platform/Platform.h:92`, `src/platform/qt/NativePlatform.cpp`, `src/platform/win/NativePlatform.cpp` +- Impact: `PAGAnimator` cannot drive itself automatically on Win/Qt — callers must implement their own timer loop. +- Fix approach: Provide a generic timer-based `DisplayLink` fallback in `src/platform/Platform.cpp`. + +--- + +## Risk Areas + +### Codec Error Handling: `throwException` Is Not a Real Exception + +- Problem: `StreamContext::throwException()` (named misleadingly) merely appends to an `errorMessages` vector and returns a bool. Callers use the `PAGThrowError` macro to log and record errors, but decoding continues unless the caller explicitly checks `hasException()`. Corrupted data can cause reads past the end of a stream before the error is noticed. +- Files: `src/codec/utils/StreamContext.h` +- Impact: Malformed or fuzzer-generated PAG files may partially decode into undefined state before errors surface; the 116 fuzz corpus files (`resources/fuzz/`) suggest this is a known concern. +- Fix approach: Make `checkEndOfFile` abort reads immediately by returning early (it already exists in `DecodeStream`); audit all callers of `PAGThrowError` to ensure they propagate the error upward promptly. + +### Fuzz Corpus Is Static (116 Files) + +- Problem: `test/src/PAGFuzzTest.cpp` loads every file in `resources/fuzz/` and decodes them. The corpus is manually curated (116 files) with no continuous fuzzing infrastructure. +- Files: `test/src/PAGFuzzTest.cpp`, `resources/fuzz/` +- Impact: Coverage-guided fuzzing (libFuzzer/AFL) is not integrated; new codec paths added for future tags may introduce vulnerabilities that the static corpus won't discover. +- Fix approach: Add a `PAGFuzzTarget` build target for libFuzzer; seed with the existing corpus. + +### OHOS Async Decode Condition Variable Deadlock Risk + +- Problem: `OHOSVideoDecoder::onSendBytes()` and `onRenderFrame()` use `condition_variable::wait()` with a lambda capturing `this`. If the codec callback thread fails to push to the queue (codec error, shutdown race), `onSendBytes` will block indefinitely. +- Files: `src/platform/ohos/OHOSVideoDecoder.cpp:165–168`, `src/platform/ohos/OHOSVideoDecoder.cpp:208` +- Impact: Potential hang on OHOS when hardware codec enters an error state. +- Fix approach: Add a timeout or a cancellation flag to the `wait()` calls to allow graceful teardown. + +### RTTR Macro Pollution in Public Header + +- Problem: `include/pag/file.h` conditionally includes 9 RTTR headers and defines `RTTR_AUTO_REGISTER_CLASS` on all public types when `PAG_USE_RTTR` is defined. This couples the public API to a reflection library that most consumers do not need. +- Files: `include/pag/file.h:25–44` +- Impact: Increases compile time for all consumers; RTTR headers pull in `clang diagnostic` suppressions globally. +- Fix approach: Move RTTR registration to a separate `include/pag/file_rttr.h` that consumers opt in to explicitly. + +### `reinterpret_cast` Across HarfBuzz C Callbacks + +- Problem: `src/renderer/TextShaper.cpp` uses `reinterpret_cast` to store and retrieve typed pointers (`DataPointer*`, `DataPointer*`) in HarfBuzz `void*` user-data slots, then `delete`s them in destroy callbacks. +- Files: `src/renderer/TextShaper.cpp:41–49` +- Impact: Type confusion if a wrong destroy callback is called; manual ownership requires careful pairing of create/destroy across C API boundaries. +- Fix approach: Wrap user-data in a typed destructor struct; consider `std::unique_ptr` with a custom deleter where the C API allows. + +--- + +## Known Issues (TODO/FIXME Analysis) + +### [OHOS] Hardware Video Decode Disabled + +- Location: `src/platform/ohos/OHOSVideoDecoder.cpp:262` +- Owner: kevingpqi +- Issue: Asynchronous hardware decoding on HarmonyOS causes video jitter. Hardware texture path is disabled with `if (false && ...)` until HarmonyOS platform fixes the issue. +- Risk: HarmonyOS users always run software decode; no automatic re-enablement when platform is fixed. + +### [Rendering] PAGPlayer `renderingTime()` Metric Stale + +- Location: `src/rendering/PAGPlayer.cpp:395` +- Owner: domrjchen +- Issue: Performance monitoring panel of PAGViewer has not been updated to display new timing properties added to `RenderCache`. +- Risk: Developers using `renderingTime()` may miss new breakdown metrics; documentation mismatch. + +### [Editing] PAGImage Scale Factor Notification Missing + +- Location: `src/rendering/editing/ImageReplacement.cpp:53` +- Owner: domrjchen +- Issue: No notification mechanism exists to invalidate/reset `scaleFactor` when `PAGImage` scale mode or matrix changes. +- Risk: Callers may receive stale scale factors after image replacement updates. + +### [Rendering] BulgeFilter SwiftShader Anomaly + +- Location: `src/rendering/filters/BulgeFilter.cpp:130` +- Issue: Using `mix()` to eliminate branch statements in the BulgeFilter GLSL shader causes visual artifacts on SwiftShader. Workaround is an explicit `if (distance <= 1.0)` branch. Root cause unresolved. +- Risk: If SwiftShader is used for CI screenshot testing, BulgeFilter output may differ from GPU results. + +--- + +## Platform Gaps + +### Linux + +- Missing: `src/platform/linux/` directory referenced in `CMakeLists.txt:471` does not exist. +- Consequence: Linux native platform builds (`USE_NATIVE_PLATFORM`) will fail at cmake configuration. Linux builds fall back to Qt platform with no font loading. +- Coverage: No Linux-specific tests exist. + +### Qt / Desktop Windows + +- Partial: `src/platform/qt/` has only 4 files: `GPUDrawable.{cpp,h}`, `NativePlatform.{cpp,h}`. There is no font loading, no display link, and no hardware video decoder. +- Consequence: Qt builds require the host app to manually set fallback fonts and drive the animation loop; hardware video will never decode. + +### Web (Emscripten) + +- Partial: Hardware decode on Web (`src/platform/web/HardwareDecoder.cpp`) is a stub that delegates entirely to JavaScript via `PAGWasmBindings.cpp`. The WASM binding file is 676 lines of hand-maintained JS–C++ glue with no automated API sync. +- Risk: Adding new C++ API methods requires manually extending `PAGWasmBindings.cpp`; easy to miss. + +### OpenHarmony (OHOS) Hardware Decode + +- Partial: Hardware texture path (`OH_NativeBuffer`) is disabled (see Known Issues). Only software YUV path is active. +- Consequence: Video playback on HarmonyOS devices runs software decode regardless of hardware availability. + +--- + +## Test Coverage Gaps + +### PAGDecoder / PAGAnimator / PAGImageView + +- What's not tested: The newer high-level APIs (`PAGDecoder`, `PAGAnimator`, `PAGImageView`) have only 13 test references combined across all test files. No screenshot baseline tests for these APIs. +- Files: `src/rendering/PAGDecoder.cpp`, `src/rendering/PAGAnimator.cpp`, `test/src/PAGPlayerTest.cpp` +- Risk: Regressions in async frame delivery or animation timing may go undetected. +- Priority: Medium + +### BitmapSequence and VideoSequence HitTest + +- What's not tested: `test/src/PAGCompositionTest.cpp:530–532` has explicit TODO markers noting that `BitmapSequenceContent` and `VideoSequenceContent` HitTest cases are missing. +- Files: `test/src/PAGCompositionTest.cpp:530` +- Risk: Pixel-level hit testing on sequence layers may return wrong results silently. +- Priority: Medium + +### Filter Edge Cases (SwiftShader, MotionBlur, BulgeFilter) + +- What's not tested: `BulgeFilter` has a known SwiftShader rendering anomaly with no regression test. `MotionBlurFilter` has only 5 test references. No filter tests specifically target SwiftShader output. +- Files: `src/rendering/filters/BulgeFilter.cpp`, `src/rendering/filters/MotionBlurFilter.cpp` +- Risk: Filter visual regressions on CPU-only rendering paths (used in CI via SwiftShader) may produce incorrect baselines. +- Priority: Low + +### Codec Decode Error Paths + +- What's not tested: The `StreamContext::throwException()` error accumulation and early-exit behavior has no dedicated unit tests. Only fuzz corpus replay exercises error paths indirectly. +- Files: `src/codec/utils/StreamContext.h`, `src/codec/utils/DecodeStream.h` +- Risk: New codec tags introduced without testing decode-error propagation may silently produce corrupt object trees. +- Priority: High + +--- + +*Concerns audit: 2025-01-27* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000000..e4b00b661d --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,166 @@ +# Coding Conventions + +**Analysis Date:** 2025-05-27 + +## Naming Conventions + +**Files:** +- Source files: `PascalCase.cpp` / `PascalCase.h` (e.g., `PAGPlayer.cpp`, `RenderCache.h`) +- Test files: `PascalCaseTest.cpp` (e.g., `PAGFileTest.cpp`, `PAGFilterTest.cpp`) +- Utility/helper files: `camelCase.cpp` / `camelCase.h` in `utils/` subdirectories +- PAGX type files in `include/pagx/types/`: `PascalCase.h` per type (e.g., `ScaleMode.h`, `TileMode.h`) + +**Classes and Structs:** +- `PascalCase` — all class and struct names (e.g., `PAGPlayer`, `RenderCache`, `PAGAnimator`) +- Prefix `PAG` for public-facing runtime classes, prefix `PAGX` for XML-format classes + +**Functions and Methods:** +- Global functions and static class methods: `PascalCase` (e.g., `LoadPAGFile()`, `PAGImage::FromPath()`) +- Member methods: `camelCase` (e.g., `pagPlayer->setSurface()`, `pagFile->setCurrentTime()`) +- Getters do not use `get` prefix when name is self-evident: `width()`, `height()`, `duration()` + +**Variables:** +- Local variables: `camelCase` (e.g., `pagFile`, `pagSurface`, `textLayer`) +- Member variables: `camelCase` (e.g., `baselineVersionPath`, `currentVersion`) +- Static constants: `ALL_CAPS_UNDERSCORE` (e.g., `OUT_ROOT`, `PAG_COMPLEX_FILE_PATH`) + +**Macros:** +- `ALL_CAPS_UNDERSCORE` (e.g., `PAG_TEST`, `PAG_SETUP`, `PAG_API`, `GL_VER`) +- No `k` prefix on constants — use `ALL_CAPS_UNDERSCORE` instead + +**Enums:** +- Enum type: `PascalCase`; enum values: `PascalCase` (e.g., `LayerType::PreCompose`, `EncodedFormat::WEBP`) + +## Code Style + +**Formatter:** clang-format, Google style base, configured in `.clang-format` +- Run before every build: `./codeformat.sh 2>/dev/null; true` +- Column limit: 100 characters +- Indent: 2 spaces, no tabs +- Pointer alignment: left (`int* ptr`) +- Brace style: same-line open brace (K&R variant) +- Max empty lines: 1 + +**Language Standard:** C++17 + +**Casting:** +- Use `static_cast()` and `reinterpret_cast()` as needed +- `dynamic_cast` is **forbidden** +- C-style casts are **forbidden** +- Prefer `std::static_pointer_cast()` for shared_ptr downcasts + +**Error Handling:** +- C++ exceptions (`throw`/`try`/`catch`) are **forbidden** +- Return `nullptr`, empty container, or `false` to signal failure +- Callers check return values; no exception propagation + +**Lambda Expressions:** +- Avoid in production code — use explicit named methods or free functions instead +- Short lambdas are allowed inline by clang-format config but should not appear in new code + +**Mutable Members:** +- Avoid `mutable` member variables +- If state must be modified in a conceptually-const context, prefer making the method non-const + +**Smart Pointers:** +- Use `std::shared_ptr` for shared ownership +- Use `std::unique_ptr` for exclusive ownership +- Factory functions return `std::shared_ptr` (e.g., `PAGFile::Load()`, `PAGImage::FromPath()`) + +## File Organization + +**Header Guards:** `#pragma once` in all headers (no traditional include guards) + +**Namespace:** All code lives in namespace `pag`; TGFX types in `tgfx`. Test files use `using namespace tgfx;` locally. + +**Include Order (clang-format merges and sorts includes alphabetically within a block):** +1. Own module header (for `.cpp` files) +2. Internal headers (project-relative paths, no angle brackets) +3. Third-party headers +4. Standard library headers + +Example from `PAGPlayer.cpp`: +```cpp +#include "base/utils/TGFXCast.h" +#include "base/utils/TimeUtil.h" +#include "pag/file.h" +#include "rendering/FileReporter.h" +#include "rendering/caches/RenderCache.h" +#include "tgfx/core/Clock.h" +``` + +**Directory Structure:** +- `include/pag/` — public API headers (detailed doc comments required) +- `include/pagx/` — PAGX format public API headers +- `src/base/` — data model (no rendering dependencies) +- `src/rendering/` — rendering pipeline +- `src/codec/` — binary format codec +- `src/pagx/` — XML format implementation +- `src/platform/{platform}/` — platform-specific implementations +- `src/cli/` — CLI tool +- `test/src/` — test cases +- `test/src/base/` — test framework infrastructure +- `test/src/utils/` — test helpers + +## Patterns to Avoid + +| Pattern | Reason | +|---------|--------| +| `dynamic_cast` | Forbidden — causes RTTI overhead and fragile downcasts | +| `throw` / `try` / `catch` | Forbidden — no exception support in codebase | +| Lambda expressions | Forbidden in production code — reduces readability | +| `mutable` member variables | Avoid — prefer non-const method instead | +| C-style casts `(Type)value` | Use `static_cast(value)` | +| Backward-compatibility shims | Remove all affected code paths when changing API | +| `k` prefix on constants | Use `ALL_CAPS_UNDERSCORE` instead | + +## Comment Standards + +**File Headers:** Every `.cpp` and `.h` file begins with the Tencent Apache 2.0 license banner (97 slashes wide). Copyright year for new files must be the current year (e.g., `Copyright (C) 2026 Tencent`); do not change the year in existing files. + +**Public API Headers (`include/`):** All public methods must have detailed doc comments including parameter descriptions: +```cpp +/** + * Creates a PAGImage object from an array of pixel data, return null if it's not valid pixels. + * @param pixels The pixel data to copy from. + * @param width The width of the pixel data. + * @param height The height of the pixel data. + * @param rowBytes The number of bytes between subsequent rows of the pixel data. + * @param colorType Describes how to interpret the components of a pixel. + * @param alphaType Describes how to interpret the alpha component of a pixel. + */ +static std::shared_ptr FromPixels(const void* pixels, ...); +``` + +**Other Public Methods (non-private, non-public-API):** One-sentence description of main purpose: +```cpp +/** + * PAGAnimator provides a simple timing engine for running animations. + */ +class PAGAnimator { ... }; +``` + +**Private Methods:** No comments. + +**Inline Code Comments:** No line-by-line comments inside function bodies. Only add comments to explain: +- Non-obvious algorithm choices +- Special boundary conditions +- Workarounds for known bugs (include the reason) + +Example of acceptable inline comment: +```cpp +// TODO(kevingpqi): We temporarily disable texture generation through NativeBuffer, as enabling +// asynchronous hardware... [reason for workaround] +``` + +**Test Case Comments:** Test cases have a Chinese description comment directly above the `PAG_TEST` macro: +```cpp +/** + * 用例描述: PAGFile基础信息获取 + */ +PAG_TEST(PAGFileTest, TestPAGFileBase) { ... } +``` + +--- + +*Convention analysis: 2025-05-27* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000000..edf15abc14 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,318 @@ +# External Integrations + +**Analysis Date:** 2026-04-28 + +libpag is an offline C++ rendering library — it has no network APIs, no authentication, no cloud services, and no webhooks. "Integrations" here means **OS-level system frameworks, GPU backends, hardware decoders, and font services** that the library binds to on each supported platform. + +## Third-party Libraries + +| Library | Version | Source | Used For | Condition | +|---------|---------|--------|----------|-----------| +| `tgfx` | 2.1.1 | `https://github.com/libpag/tgfx.git` → `third_party/tgfx/` | 2D GPU rendering engine | Always | +| `lz4` | 1.10.0 | `https://github.com/lz4/lz4.git` → `third_party/lz4/` | Sequence frame disk cache compression | Always (or system `compression.framework` on Apple) | +| `libavc` | — | `https://github.com/libpag/libavc.git` → `third_party/libavc/` | H.264 software decoder fallback | `PAG_USE_LIBAVC` (non-Android/iOS/OHOS) | +| `ffavc` | — | Prebuilt `.so` in `android/libpag/libs/${ANDROID_ABI}/` | H.264 decoder on Android | `PAG_USE_FFAVC` (Android) | +| `harfbuzz` | 10.1.0 | `https://github.com/harfbuzz/harfbuzz.git` → `third_party/harfbuzz/` | Text shaping / glyph layout | `PAG_USE_HARFBUZZ` (auto when PAGX) | +| `expat` | 2.6.3 | `https://github.com/libexpat/libexpat.git` → `third_party/expat/` | XML parsing for PAGX format | `PAG_BUILD_PAGX` | +| `SheenBidi` | 3.0.0 | `https://github.com/Tehreer/SheenBidi.git` → `third_party/SheenBidi/` | Unicode BiDi algorithm | `PAG_BUILD_PAGX` | +| `libxml2` | 2.15.2 | `https://github.com/GNOME/libxml2.git` → `third_party/libxml2/` | XPath + XML for CLI/tests | CLI + Tests only | +| `rttr` | — | `https://github.com/rttrorg/rttr.git` → `third_party/rttr/` | Runtime type reflection | `PAG_USE_RTTR` (opt-in) | +| `vendor_tools` | — | `https://github.com/libpag/vendor_tools.git` → `third_party/vendor_tools/` | CMake vendor build helpers | Always | +| `libpng` | 1.6.47 | TGFX `third_party/libpng/` | PNG decode/encode | `PAG_USE_PNG_*` (via TGFX) | +| `libjpeg-turbo` | 2.1.1 | TGFX `third_party/libjpeg-turbo/` | JPEG decode/encode | `PAG_USE_JPEG_*` (via TGFX) | +| `libwebp` | 1.x | TGFX `third_party/libwebp/` | WebP decode/encode | `PAG_USE_WEBP_*` (via TGFX) | +| `freetype` | 2.13.3 | TGFX `third_party/freetype/` | Vector font rasterization | `PAG_USE_FREETYPE` (via TGFX) | +| `pathkit` | — | TGFX `third_party/pathkit/` | 2D path operations (Skia-derived) | Always (TGFX) | +| `skcms` | — | TGFX `third_party/skcms/` | ICC color profile handling | Always (TGFX) | +| `highway` | — | TGFX `third_party/highway/` | SIMD acceleration | Always (TGFX) | +| `concurrentqueue` | — | TGFX `third_party/concurrentqueue/` | Lock-free task queuing | Always (TGFX) | +| `flatbuffers` | — | TGFX `third_party/flatbuffers/` | Binary serialization in layers module | `TGFX_BUILD_LAYERS` | +| `shaderc` | — | TGFX `third_party/shaderc/` | GLSL → SPIR-V compilation for Metal | TGFX Metal backend | +| `SPIRV-Cross` | — | TGFX `third_party/SPIRV-Cross/` | SPIR-V → MSL for Metal | TGFX Metal backend | +| `zlib` | — | TGFX `third_party/zlib/` | Compression support (shaderc dep) | TGFX | +| `nlohmann/json` | — | TGFX `third_party/json/` | JSON in TGFX test utilities | TGFX tests | +| `googletest` | 1.16.0 | TGFX `third_party/googletest/` | C++ unit test framework | `PAG_BUILD_TESTS` | + +## APIs & External Services + +**External network services:** +- None. libpag does not make HTTP requests, open sockets, or contact any remote service at runtime. `libxml2` is configured with `LIBXML2_WITH_HTTP=OFF` explicitly. + +## Data Storage + +**Databases:** +- None. + +**File Storage:** +- Local filesystem only. The library reads `.pag` binary files and `.pagx` XML files from paths supplied by the caller. +- Disk cache for decoded sequence frames — `PAGDiskCache` API, implemented via `SequenceFile` (`src/rendering/caches/SequenceFile.cpp`), compressed with LZ4. +- Test resources: `resources/` directory (`test/src/` references via `TestConstants::PAG_ROOT`). +- Test output: `test/out/{folder}/{name}.webp` vs baseline `{name}_base.webp`, version-tracked via `test/baseline/version.json` (repo) and `test/baseline/.cache/version.json` (local). + +**Caching:** +- In-memory render caches: `RenderCache`, `RasterCache`, `ShapeCache`, `TextCache` in `src/rendering/caches/`. +- Disk cache wraps the `SequenceFile` LZ4 format. + +## Authentication & Identity + +- None. libpag has no user concept, no auth provider, no credentials. +- `android/libpag/build.gradle` references JReleaser/Sonatype credentials for **publishing** only, not runtime use. + +## Monitoring & Observability + +**Error Tracking:** +- None (no Sentry / Crashlytics / Bugsnag). + +**Logs:** +- Android: `#include ` via linked `log` library (`CMakeLists.txt:407-408`) +- OpenHarmony: `hilog_ndk.z` library (`CMakeLists.txt:485`) +- Trace image debug output: `src/platform/android/JTraceImage.cpp` (optional debug feature) +- Otherwise errors are returned as `DecodingResult` error codes or `nullptr` from factory methods + +## CI/CD & Deployment + +**Hosting:** +- Android SDK → Maven Central via JReleaser (`com.tencent.tav:libpag`, `android/libpag/build.gradle:145-161`) +- Web SDK → NPM (`libpag@4.0.0`, `web/package.json`) +- CLI → NPM (`@libpag/pagx@0.2.16`, `cli/npm/package.json`) +- iOS/macOS → distributed as prebuilt frameworks (no CocoaPods/SPM in this tree) +- OHOS → HAR package (`@tencent/libpag@1.0.1`) + +**CI Pipeline:** +- Build scripts: `autotest.sh`, `build_pag`, `build_vendor`, `codecov.sh` at repo root. +- No `.github/`, `.gitlab-ci.yml`, or `Jenkinsfile` observed. + +## Environment Configuration + +**Runtime env vars:** +- None required by the library itself at runtime. +- `$ENV{CLION_IDE}` inspected at CMake configure time only (`CMakeLists.txt:77-81`). + +**Secrets location:** +- Android publishing: JReleaser GPG keys expected via standard JReleaser env vars (not stored in repo) +- OHOS signing: `ohos/local.signingconfig.sample.json` — sample only, real file gitignored + +## Webhooks & Callbacks + +**Incoming:** None. + +**Outgoing:** None at the network level. + +**In-process callbacks:** +- `PAGAnimator::Listener` — animation lifecycle events +- `PAGPlayer` / `PAGSurface` callbacks delivered to host app via platform bindings (iOS `PAGAnimationCallback.mm`, Android `JPAGAnimator.cpp`) + +--- + +## Platform Integrations (the primary integration surface) + +### iOS + +**System frameworks linked** (`CMakeLists.txt:323-343`): +- `UIKit`, `Foundation`, `QuartzCore`, `CoreGraphics` — UI + 2D primitives +- `CoreText` — font loading / text shaping on Apple +- `VideoToolbox`, `CoreMedia`, `CoreVideo` — hardware H.264 decode +- `ImageIO` — image encode/decode +- `OpenGLES` (when `PAG_USE_OPENGL=ON`) **or** `Metal` (when `PAG_USE_OPENGL=OFF`) +- `iconv` — character encoding conversion + +**GPU context:** +- OpenGL ES via EAGLWindow / EAGLDevice used in `src/platform/ios/private/GPUDrawable.h` +- Metal is the TGFX Metal backend when OpenGL is disabled +- `CAEAGLLayer`-backed `PAGView` (`src/platform/ios/PAGView.mm`) + +**Hardware video decoding** (`src/platform/ios/private/HardwareDecoder.h`): +- `VTDecompressionSessionRef` from `` +- Uses `CMSampleBufferRef` for input bitstream, yields `CVPixelBufferRef` for GPU upload + +**Font loading:** +- Native shaper uses `` via `src/platform/cocoa/private/NativeTextShaper.mm` (shared between iOS and macOS) + +**Display sync:** +- `CADisplayLink` wrapped in `src/platform/ios/private/NativeDisplayLink.mm` + +**Public ObjC headers:** +- Umbrella: `src/platform/ios/libpag.h` +- Module map: `ios/libpag.modulemap` +- Bundle identifier: `com.tencent.libpag` + +### macOS + +**System frameworks linked** (`CMakeLists.txt:367-393`): +- `ApplicationServices`, `QuartzCore`, `Cocoa`, `Foundation` +- `VideoToolbox`, `CoreMedia` — hardware H.264 decode +- `OpenGL` (when `PAG_USE_OPENGL=ON`) **or** `Metal` +- `iconv` + +**GPU context:** +- Desktop OpenGL via `NSOpenGLView`-backed `PAGView` (`src/platform/mac/PAGView.m`) + +**Hardware video decoding:** +- Same VideoToolbox pipeline as iOS; separate impl at `src/platform/mac/private/HardwareDecoder.h/.mm` + +**Font loading:** Shared CoreText path via `src/platform/cocoa/private/NativeTextShaper.mm`. + +**Module map:** `mac/libpag.modulemap`. + +### Android + +**System libraries linked** (`CMakeLists.txt:406-415, 429-434`): +- `log` — `` logging +- `android` — `ANativeWindow` and core NDK +- `jnigraphics` — `AndroidBitmap_*` pixel access +- `mediandk` — ``, `` hardware video decoder +- `GLESv2`, `GLESv3`, `EGL` — OpenGL ES context + +**GPU context:** +- EGL surface via `ANativeWindow` — `src/platform/android/GPUDrawable.cpp` wraps `tgfx::EGLWindow` +- Delivered to Java as `android.view.Surface` through `JPAGSurface.cpp` + +**Hardware video decoding** (`src/platform/android/HardwareDecoder.h`): +- `AMediaCodec` + `AMediaCodecBufferInfo` from `` +- Uses `ANativeWindow` from `` to feed a `SurfaceTexture` +- Fallback software decoder: ffavc (prebuilt `libffavc.so` in `android/libpag/libs/${ANDROID_ABI}/`) + +**Font loading:** `src/platform/android/FontConfigAndroid.cpp` — walks Android system font config XML, registers fallback fonts with TGFX. + +**Display sync:** `src/platform/android/NativeDisplayLink.cpp` — wraps `android.view.Choreographer` via JNI. + +**JNI bridge files:** All `J*.cpp` in `src/platform/android/` — `JPAG.cpp`, `JPAGPlayer.cpp`, `JPAGFile.cpp`, `JPAGImage.cpp`, `JPAGImageLayer.cpp`, `JPAGShapeLayer.cpp`, `JPAGTextLayer.cpp`, `JPAGSolidLayer.cpp`, `JPAGAnimator.cpp`, `JPAGDecoder.cpp`, `JPAGDiskCache.cpp`, `JPAGFont.cpp`, `JPAGImageView.cpp`, `JPAGSurface.cpp`, `JPAGComposition.cpp`, `JPAGLayer.cpp`, `JVideoDecoder.cpp`, `JVideoSurface.cpp`, `JNativeTask.cpp`, `JTraceImage.cpp`. Helpers: `JNIHelper.h/.cpp`. + +**Link-time optimization:** +- Symbol export script `android/libpag/export.def` + `-Wl,--gc-sections --version-script` (`CMakeLists.txt:425`) +- `-fno-exceptions -fno-rtti -Os` for minimum binary size + +**Android artifact:** `com.tencent.tav:libpag:4.1.0` (AAR), min SDK 21, target SDK 33, NDK 28.0.13004108. Depends on `androidx.exifinterface:exifinterface:1.3.3`. + +### OpenHarmony (OHOS) + +**System libraries linked** (`CMakeLists.txt:474-500`): +- GPU: `GLESv3`, `EGL` +- Windowing / graphics: `native_buffer`, `native_window`, `native_image`, `native_display_soloist` +- Pixel buffers: `pixelmap_ndk.z`, `image_source_ndk.z`, `pixelmap`, `image_source` +- NAPI runtime: `ace_ndk.z`, `ace_napi.z` +- Logging: `hilog_ndk.z` +- Raw files: `rawfile.z` +- Hardware video decode: `native_media_codecbase`, `native_media_core`, `native_media_vdec` + +**GPU context:** EGL via `tgfx::EGLWindow` (`src/platform/ohos/GPUDrawable.h`). + +**Hardware video decoding:** `OHOSVideoDecoder.cpp` uses `native_media_vdec` / `native_media_codecbase`. Software fallback: `OHOSSoftwareDecoderWrapper.cpp`. + +**XComponent integration:** `XComponentHandler.cpp` — bridges surface lifecycle from OHOS XComponent. + +**NAPI bridge:** All `JPAG*.cpp` in `src/platform/ohos/` mirror the Android JNI layer via NAPI. Helper: `JsHelper.h/.cpp`. + +**Link-time symbol export:** `ohos/libpag/export.def` + `--gc-sections --version-script` (`CMakeLists.txt:503`). + +**Package:** `@tencent/libpag@1.0.1` HAR. + +### Web (Emscripten / WASM) + +**Runtime environment** (`CMakeLists.txt:639-660`): +- Emscripten-compiled WASM + JS glue +- WebGL 2 (`-sMAX_WEBGL_VERSION=2`) +- ES6 module export with name `PAGInit` (`-sEXPORT_NAME='PAGInit' -sEXPORT_ES6=1 -sMODULARIZE=1`) +- Environments: `web,worker` +- Memory: `-sALLOW_MEMORY_GROWTH=1`; multithreaded build adds `-sUSE_PTHREADS=1 -sINITIAL_MEMORY=32MB -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency` +- `-fno-rtti` (RTTI disabled on web) + +**GPU context:** Canvas-bound WebGL 2. `src/platform/web/GPUDrawable.cpp` calls `emscripten_get_canvas_element_size()` targeting a named canvas element. + +**Hardware video decoding:** Delegated to JavaScript (`src/platform/web/HardwareDecoder.cpp`) via `WebVideoSequenceDemuxer`. `WebSoftwareDecoderFactory` provides JS-implemented software decoders. + +**Text shaping:** `src/platform/web/NativeTextShaper.cpp` delegates to JS callbacks (Canvas/DOM font metrics). Unicode emoji table: `src/platform/web/UnicodeEmojiTable.hh`. + +**JS binding layer:** `src/platform/web/PAGWasmBindings.cpp` uses `emscripten::bind` and `emscripten::val` to expose C++ classes as JS objects (`PAGFile`, `PAGPlayer`, `PAGSurface`, `PAGLayer` hierarchy, etc.). + +**Build tooling:** Rollup + TypeScript in `web/src/`. Variants: `wasm` (ST), `wasm-mt` (MT), `wechat` (WeChat Mini Program with Brotli WASM). + +**Blocked Emscripten version:** `4.0.11` (`CMakeLists.txt:643-648`) — build fatals if detected. + +### Windows + +**System libraries linked** (`CMakeLists.txt:438-459`): +- `opengl32` (native GL) **or** ANGLE static libs from `${TGFX_DIR}/vendor/angle/` when `PAG_USE_ANGLE=ON` +- `Bcrypt` — cryptographic primitives (required transitively) +- `ws2_32` — Winsock (required transitively) + +**GPU context:** WGL on Win32 HWND, or ANGLE-backed GLES3 on D3D11. `src/platform/win/GPUDrawable.cpp`. + +**Hardware video decoding:** None in `src/platform/win/`. Falls back to libavc software decoder. + +**Font loading:** TGFX/FreeType. + +**Preprocessor:** `NOMINMAX`, `_USE_MATH_DEFINES`, 64-bit forced. + +**Demo project:** `win/Win32Demo.sln`. + +### Linux + +**System libraries linked** (`CMakeLists.txt:460-473`): +- `pthread` / `Threads::Threads`, `dl` +- `GLESv2`, `EGL` +- `Fontconfig` (CLI only, `CMakeLists.txt:771-773`) via `find_package(Fontconfig REQUIRED)` + +**GPU context:** EGL + GLESv2 (`src/platform/linux/GPUDrawable.cpp`). + +**Hardware video decoding:** None native; libavc software decoder only. + +**Font loading:** System fontconfig (CLI); TGFX/FreeType otherwise. + +**Compile flags:** `-fPIC -pthread`. + +### Qt + +**Frameworks linked** (`CMakeLists.txt:242-245`): +- `Qt6::Widgets`, `Qt6::OpenGL`, `Qt6::Quick` + +**GPU context:** `QGLWindow` via `src/platform/qt/GPUDrawable.cpp`. + +**Notes:** When Qt is enabled, SwiftShader and ANGLE are force-disabled; OpenGL is force-enabled. On macOS, `HardwareDecoder.mm` is still included for VideoToolbox decoding. + +### SwiftShader (optional CPU backend) + +**Libraries:** Shared libs globbed from `${TGFX_DIR}/vendor/swiftshader/${PLATFORM}/${ARCH}/*` (`CMakeLists.txt:253-258`). Used for headless/CI rendering. + +--- + +## Hardware Video Decoding Summary + +| Platform | Hardware API | Fallback | +|----------|-------------|----------| +| iOS | `VTDecompressionSession` (VideoToolbox) | none | +| macOS | `VTDecompressionSession` (VideoToolbox) | libavc | +| Android | `AMediaCodec` (NdkMediaCodec) | ffavc (`libffavc.so`) | +| OpenHarmony | `native_media_vdec` | OHOS software decoder wrapper | +| Web | JS-side decoders via `WebVideoSequenceDemuxer` | JS software decoder factory | +| Windows | none native | libavc | +| Linux | none native | libavc | + +Software fallback chain controlled at configure time via `PAG_USE_LIBAVC` and `PAG_USE_FFAVC`. The `VideoDecoder` abstract base lives at `src/rendering/video/VideoDecoder.h`. + +## Font Loading Summary + +| Platform | Font source | +|----------|-------------| +| iOS / macOS | CoreText (`src/platform/cocoa/private/NativeTextShaper.mm`) | +| Android | Platform font config XML (`src/platform/android/FontConfigAndroid.cpp`) | +| Linux (CLI) | fontconfig | +| OHOS | TGFX default (FreeType) | +| Web | JS-side text shaping (`src/platform/web/NativeTextShaper.cpp`) | +| Windows | TGFX default (FreeType) | + +## Format Support + +| Format | Codec / Parser | Direction | +|--------|----------------|-----------| +| `.pag` | Tag-based binary codec (`src/codec/`) — 100+ tag types, LZ4-compressed blocks, versioned tags | Read + Write | +| `.pagx` | XML via Expat (`src/pagx/xml/`) + HarfBuzz + SheenBidi for text | Read + Write (`PAGXImporter` / `PAGXExporter`) | +| `.svg` | `SVGImporter.cpp` / `SVGExporter.cpp` (`src/pagx/svg/`), path parsing via `SVGPathParser.cpp` | Read + Write (`PAG_BUILD_SVG`) | +| `.webp` | TGFX/libwebp (baseline screenshots, sequence frames) | Read + Write | +| `.png` | TGFX/libpng | Read + Write | +| `.jpg` | TGFX/libjpeg-turbo | Read + Write | +| `.mp4` / H.264 | Hardware decoder per platform + libavc/ffavc fallback | Read (decode only) | + +--- + +*Integration audit: 2026-04-28* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000000..404a9d62f3 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,172 @@ +# Technology Stack + +**Analysis Date:** 2026-04-28 + +## Languages + +**Primary:** +- C++17 - Core rendering engine, codec, data model (all of `src/base/`, `src/codec/`, `src/rendering/`, `src/pagx/`, `src/renderer/`) +- Objective-C / Objective-C++ - Apple platform integration in `src/platform/cocoa/`, `src/platform/ios/`, `src/platform/mac/` (`.m` / `.mm` files) + +**Secondary:** +- Java / Kotlin - Android SDK layer in `android/libpag/src/main/` (bridged via JNI from `src/platform/android/J*.cpp`) +- TypeScript / JavaScript - Web/WASM SDK in `web/src/`, build tooling in `web/script/` +- ArkTS (ETS) - OpenHarmony SDK in `ohos/libpag/src/main/` (bridged via NAPI from `src/platform/ohos/`) +- CMake - Build configuration (`CMakeLists.txt`, `third_party/vendor_tools/vendor.cmake`) +- Shell - Build/tooling scripts (`sync_deps.sh`, `install_tools.sh`, `codeformat.sh`, `accept_baseline.sh`) + +## Runtime + +**Language standard:** +- `CMAKE_CXX_STANDARD 17` (required) — see `CMakeLists.txt:21-22` +- `CMAKE_CXX_VISIBILITY_PRESET hidden` (symbols hidden by default) + +**Deployment targets** (`CMakeLists.txt:141-153`): +- macOS arm64: `11.0` minimum +- macOS x86_64: `10.15` minimum +- iOS: `9.0` minimum +- Android: `minSdkVersion 21`, `targetSdkVersion 33`, NDK `28.0.13004108` (`android/build.gradle`) +- Web: Emscripten (version `4.0.11` explicitly blocked — `CMakeLists.txt:643-648`) + +**Build tooling required:** +- `node`, `cmake`, `ninja`, `yasm`, `git-lfs` — installed via Homebrew on macOS +- `emscripten` — required when building web target +- `depsync` — npm package for syncing `DEPS` dependencies + +## Frameworks + +**Core build system:** +- CMake 3.13+ with Ninja generator (primary) +- Gradle 8.8.1 + Android Gradle Plugin `8.8.1` (Android wrapper build, `android/build.gradle`) +- Xcode (iOS/macOS framework builds via `mac/gen_mac`, `ios/gen_ios`, `ios/gen_simulator`) +- Hvigor (OpenHarmony build system, `ohos/hvigorfile.ts`) +- Rollup + TypeScript + Babel (web/WASM bundling, `web/package.json`) + +**Testing:** +- Google Test `1.16.0` — pulled from `${TGFX_DIR}/third_party/googletest/`. Test targets: `PAGFullTest`, `PAGUnitTest`, `UpdateBaseline` (`CMakeLists.txt:799-855`). +- Cypress 9.5 — web end-to-end tests (`web/cypress/`) + +**Render backends** (selected at configure time, `CMakeLists.txt:27-31`): +- OpenGL / OpenGL ES (default, `PAG_USE_OPENGL=ON`) +- Metal (Apple, when `PAG_USE_OPENGL=OFF`) +- ANGLE (`PAG_USE_ANGLE=ON`, Windows only, headers from `${TGFX_DIR}/vendor/angle/`) +- SwiftShader (`PAG_USE_SWIFTSHADER=ON`, CPU rendering, libs from `${TGFX_DIR}/vendor/swiftshader/`) +- WebGL 2 (web target via Emscripten `-sMAX_WEBGL_VERSION=2`) +- Qt OpenGL (`PAG_USE_QT=ON`, requires Qt6 Core/Widgets/OpenGL/Quick) + +## Key Dependencies + +**Rendering engine (required):** +- `tgfx` 2.1.1 (Tencent 2D graphics engine, `https://github.com/libpag/tgfx.git`) — pinned commit in `DEPS`, synced to `third_party/tgfx/`. Can be linked three ways (`CMakeLists.txt:525-613`): + 1. Prebuilt external: via `-DTGFX_LIB=... -DTGFX_INCLUDE=...` + 2. Custom source dir: via `-DTGFX_DIR=../tgfx` (used for local TGFX debugging) + 3. Built-in: uses `third_party/tgfx/out/cache` if present, else `add_subdirectory` + +**Binary codec:** +- `lz4` 1.10.0 (`https://github.com/lz4/lz4.git`) — embedded build compiles `third_party/lz4/lib/lz4.c` (`CMakeLists.txt:512-516`). On Apple platforms with `PAG_USE_SYSTEM_LZ4=ON`, links system `compression` framework instead. Used for sequence frame compression in `src/rendering/caches/`. +- `libavc` (H.264 software decoder, `https://github.com/libpag/libavc.git`) — fallback enabled via `PAG_USE_LIBAVC` on non-Android/non-iOS/non-OHOS platforms. Headers: `third_party/libavc/common`, `third_party/libavc/decoder`. +- `ffavc` (prebuilt binary in `vendor/ffavc/` and `android/libpag/libs/${ANDROID_ABI}/libffavc.so`) — alternate H.264 fallback on Android (`PAG_USE_FFAVC=ON`). + +**Text & internationalization** (only when `PAG_BUILD_PAGX=ON` or `PAG_BUILD_TESTS=ON`): +- `harfbuzz` 10.1.0 (`https://github.com/harfbuzz/harfbuzz.git`) — text shaping. Used by `src/renderer/` text layout engine. +- `expat` 2.6.3 (`https://github.com/libexpat/libexpat.git`) — XML parsing for PAGX format. Built static. +- `SheenBidi` 3.0.0 (`https://github.com/Tehreer/SheenBidi.git`) — Unicode bidirectional text (BiDi) algorithm. +- `libxml2` 2.15.2 (`https://github.com/GNOME/libxml2.git`) — XPath + advanced XML (used by CLI/tests only). Configured with all optional features OFF. Platforms: mac, win, linux. + +**Optional:** +- `rttr` (`https://github.com/rttrorg/rttr.git`) — runtime type reflection, enabled only when `PAG_USE_RTTR=ON`. Platforms: mac, win, linux. +- `vendor_tools` (`https://github.com/libpag/vendor_tools.git`) — CMake helpers for third-party targets (`third_party/vendor_tools/vendor.cmake`). + +**Image codecs** (provided by TGFX, controlled via `PAG_USE_PNG_DECODE/ENCODE`, `PAG_USE_JPEG_DECODE/ENCODE`, `PAG_USE_WEBP_DECODE/ENCODE`): +- `libpng` 1.6.47 — PNG encode/decode (TGFX `third_party/libpng/`) +- `libjpeg-turbo` 2.1.1 — JPEG encode/decode (TGFX `third_party/libjpeg-turbo/`) +- `libwebp` 1.x — WebP encode/decode (TGFX `third_party/libwebp/`) +- `freetype` 2.13.3 — Vector font rendering (TGFX `third_party/freetype/`, `PAG_USE_FREETYPE`) + +**Graphics pipeline (TGFX internal):** +- `pathkit` — Skia-derived 2D path library (`third_party/tgfx/third_party/pathkit/`) +- `skcms` — ICC color profile processing (`third_party/tgfx/third_party/skcms/`) +- `highway` — SIMD acceleration (`third_party/tgfx/third_party/highway/`) +- `concurrentqueue` — lock-free task queue (`third_party/tgfx/third_party/concurrentqueue/`) +- `flatbuffers` — binary serialization in TGFX layers module (`third_party/tgfx/third_party/flatbuffers/`) +- `shaderc` + `SPIRV-Cross` — GLSL → MSL shader compilation for Metal backend (`third_party/tgfx/third_party/shaderc/`, `third_party/tgfx/third_party/SPIRV-Cross/`) +- `zlib` — required by TGFX/shaderc on some paths (`third_party/tgfx/third_party/zlib/`) +- `nlohmann/json` — test utilities in TGFX (`third_party/tgfx/third_party/json/`) + +## Configuration + +**CMake options** (defined in `CMakeLists.txt:27-67`): + +| Option | Default | Description | +|--------|---------|-------------| +| `PAG_USE_OPENGL` | ON | OpenGL GPU backend | +| `PAG_USE_SWIFTSHADER` | OFF | CPU rendering via SwiftShader | +| `PAG_USE_ANGLE` | OFF | Windows D3D-backed OpenGL ES via ANGLE | +| `PAG_USE_QT` | OFF | Qt6 integration (disables Metal/SwiftShader/ANGLE) | +| `PAG_USE_RTTR` | OFF | Runtime type reflection | +| `PAG_USE_HARFBUZZ` | OFF | Text shaping (auto-ON with PAGX) | +| `PAG_USE_C` | OFF | C language API bindings (`src/c/`) | +| `PAG_USE_FREETYPE` | ON non-Apple/non-Web, OFF Apple/Web | Embedded FreeType | +| `PAG_USE_LIBAVC` | ON non-Apple-sim/non-OHOS/non-Web | libavc fallback decoder | +| `PAG_USE_FFAVC` | ON Android | ffavc prebuilt decoder | +| `PAG_USE_THREADS` | ON non-Web or EMSCRIPTEN_PTHREADS | Multithreaded rendering | +| `PAG_USE_SYSTEM_LZ4` | ON Apple | Link Apple's `compression.framework` for LZ4 | +| `PAG_BUILD_PAGX` | OFF | PAGX XML format support (auto-ON by TESTS/CLI/SVG) | +| `PAG_BUILD_SVG` | OFF | SVG import/export (auto-ON by CLI/TESTS) | +| `PAG_BUILD_CLI` | OFF | `pagx-cli` command-line tool | +| `PAG_BUILD_TESTS` | OFF | Enable all modules and GoogleTest targets | +| `PAG_BUILD_SHARED` | ON non-Web | Shared vs static library | +| `PAG_BUILD_FRAMEWORK` | ON Apple | Build Apple framework bundle | + +**Dependency option chains** (`CMakeLists.txt:95-125`): +- `PAG_BUILD_CLI` → forces `PAG_BUILD_PAGX` + `PAG_BUILD_SVG` +- `PAG_BUILD_SVG` → forces `PAG_BUILD_PAGX` +- `PAG_BUILD_PAGX` → forces `PAG_USE_HARFBUZZ` +- `PAG_BUILD_TESTS` → forces `PAG_USE_FREETYPE` + `PAG_BUILD_PAGX` + `PAG_BUILD_CLI` + `PAG_BUILD_SVG` + `PAG_USE_HARFBUZZ` + `PAG_USE_SYSTEM_LZ4=OFF` + `PAG_BUILD_SHARED=OFF` +- `PAG_USE_QT|SWIFTSHADER|ANGLE` → forces `PAG_USE_OPENGL=ON` +- `PAG_USE_FFAVC` → overrides `PAG_USE_LIBAVC=OFF` + +**Compiler flags** (`CMakeLists.txt:209-216`): +- Clang: `-Werror -Wall -Wextra -Weffc++ -pedantic -Werror=return-type -Wno-unused-command-line-argument` +- MSVC: `/utf-8 /w44251 /w44275` +- Android release: `-ffunction-sections -fdata-sections -Os -fno-exceptions -fno-rtti` (`CMakeLists.txt:425-426`) +- Web: `-fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0` plus `-Oz` (release) or `-O0 -gsource-map` (debug) + +**Key build defines added by options:** +- `PAG_DLL` when `PAG_BUILD_SHARED` +- `PAG_USE_LIBAVC`, `PAG_USE_FFAVC`, `PAG_USE_RTTR`, `PAG_USE_HARFBUZZ` +- `PAG_BUILD_PAGX`, `PAG_BUILD_SVG` +- `PAG_BUILD_FOR_WEB` on Web +- `LIBXML_STATIC` (tests/CLI), `XML_STATIC` (Windows + PAGX) +- `GL_SILENCE_DEPRECATION` / `GLES_SILENCE_DEPRECATION` on Apple +- `NOMINMAX`, `_USE_MATH_DEFINES` on Windows +- `SKIP_FRAME_COMPARE` in `PAGUnitTest`, `UPDATE_BASELINE`/`GENERATE_BASELINE_IMAGES` in `UpdateBaseline` + +## Platform Requirements + +**Development (macOS, primary dev platform):** +- Xcode command-line tools +- Homebrew-installed: node, cmake, ninja, yasm, git-lfs, emscripten (for web) +- `./sync_deps.sh` must be run before first build — invokes `depsync` which clones repos listed in `DEPS` into `third_party/` +- CLion-friendly: when `$ENV{CLION_IDE}` is set on macOS, `PAG_BUILD_TESTS=ON` by default (`CMakeLists.txt:77-86`) + +**Production build outputs:** +- **iOS/macOS:** Framework bundle (`PAG_BUILD_FRAMEWORK=ON`), bundle identifier `com.tencent.libpag`, version `4.0.0` (`src/rendering/PAG.cpp`) +- **Android:** AAR `com.tencent.tav:libpag:4.1.0`, min SDK 21, target SDK 33, NDK 28.0.13004108 +- **Web:** NPM package `libpag@4.0.0` (`web/package.json`) — ESM/CJS/UMD; targets `wasm` (ST) and `wasm-mt` (MT); WeChat mini-program variant under `web/wechat/` +- **OpenHarmony:** HAR package `@tencent/libpag@1.0.1` +- **CLI:** `pagx-cli` executable, npm `@libpag/pagx@0.2.16` (`cli/npm/package.json`) +- **Windows:** Win32 demo only (`win/Win32Demo.sln`), no packaged SDK +- **Linux:** CLI executable + demo (`linux/CMakeLists.txt`) + +**Versions:** +- C++ SDK: `4.0.0` (`src/rendering/PAG.cpp`) +- Android SDK: `4.1.0` (`android/libpag/build.gradle`) +- Web package: `4.0.0` (`web/package.json`) +- TGFX engine: `2.1.1` (`third_party/tgfx/CMakeLists.txt`) +- PAGX CLI npm: `0.2.16` (`cli/npm/package.json`) +- DEPS manifest: `1.3.12` (`DEPS`) + +--- + +*Stack analysis: 2026-04-28* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000000..d9373333b7 --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,353 @@ +# Codebase Structure + +**Analysis Date:** 2025-07-15 + +## Directory Layout + +``` +libpag/ +├── include/ # Public headers (installed with the library) +│ ├── pag/ # Core C++ API +│ │ ├── pag.h # Master include: PAGPlayer, PAGFile, PAGSurface, PAGImage… +│ │ ├── file.h # PAGFile, PAGComposition, PAGLayer types +│ │ ├── decoder.h # PAGDecoder (frame-by-frame decode to pixels) +│ │ ├── gpu.h # GPU backend types (BackendTexture, BackendSemaphore) +│ │ ├── types.h # Shared value types (Color, Point, Rect, Matrix, etc.) +│ │ ├── defines.h # PAG_API export macros +│ │ └── c/ # C language bindings (pag_player.h, pag_file.h, …) +│ └── pagx/ # PAGX XML format API (~100 headers) +│ ├── PAGXDocument.h # Top-level XML document model +│ ├── PAGXImporter.h # PAGXImporter: XML → PAGFile +│ ├── PAGXExporter.h # PAGXExporter: PAGFile → XML +│ ├── SVGImporter.h # SVGImporter +│ ├── SVGExporter.h # SVGExporter +│ ├── FontConfig.h # Font embedding/lookup config +│ ├── nodes/ # Scene graph node types (Layer, Group, Text, Shape…) +│ └── types/ # PAGX enum/value types (BlendMode, Alignment, etc.) +│ +├── src/ # Implementation sources +│ ├── base/ # PAG data model (no rendering logic) +│ │ ├── Composition.cpp # Base Composition + Vector/Bitmap/VideoComposition +│ │ ├── Layer.cpp # Base Layer + Image/Shape/Solid/Text/PreCompose/Camera +│ │ ├── File.cpp # File-level metadata +│ │ ├── shapes/ # Shape primitives (Rectangle, Ellipse, ShapePath, Fill…) +│ │ ├── effects/ # Effect data (blur, drop shadow, glow, etc.) +│ │ ├── keyframes/ # Keyframe interpolation types +│ │ ├── layerStyles/ # Layer style data (stroke, outer glow, etc.) +│ │ ├── text/ # Text document, animator, selector data +│ │ └── utils/ # TGFXCast, TimeUtil, math helpers +│ │ +│ ├── codec/ # Binary PAG format encode/decode +│ │ ├── Codec.cpp # Entry point: Codec::Decode / Codec::Encode +│ │ ├── CodecContext.cpp # Decode/encode state context +│ │ ├── AttributeHelper.cpp # Typed attribute read/write helpers +│ │ ├── DataTypes.cpp # Primitive type serialization +│ │ ├── tags/ # Per-feature tag handlers +│ │ │ ├── effects/ # Effect tag readers/writers +│ │ │ ├── shapes/ # Shape tag readers/writers +│ │ │ ├── layerStyles/ # Layer style tag readers/writers +│ │ │ └── text/ # Text tag readers/writers +│ │ ├── mp4/ # MP4/H.264 muxing for video sequences +│ │ └── utils/ # Codec utility helpers +│ │ +│ ├── rendering/ # Real-time rendering pipeline +│ │ ├── PAGPlayer.cpp # PAGPlayer: playback control, owns PAGStage + RenderCache +│ │ ├── PAGSurface.cpp # PAGSurface: rendering target, wraps Drawable +│ │ ├── PAGSurfaceFactory.cpp # Platform-agnostic surface creation helpers +│ │ ├── PAGAnimator.cpp # Animation frame timing +│ │ ├── PAGDecoder.cpp # Frame-by-frame offline decoder +│ │ ├── PAGFont.cpp # Font registration/lookup +│ │ ├── PAG.cpp # Global PAG init / version info +│ │ ├── FontManager.cpp # System/embedded font manager +│ │ ├── Performance.cpp # Render performance metrics +│ │ ├── FileReporter.cpp # Telemetry/error reporting +│ │ ├── layers/ # PAGLayer runtime wrappers +│ │ │ ├── PAGStage.cpp # Root scene graph node; owns RenderCache attachment +│ │ │ ├── PAGLayer.cpp # Base PAGLayer (wraps data-model Layer) +│ │ │ ├── PAGComposition.cpp # PAGComposition (wraps Composition) +│ │ │ ├── PAGFile.cpp # PAGFile (top-level file runtime wrapper) +│ │ │ ├── PAGImageLayer.cpp +│ │ │ ├── PAGShapeLayer.cpp +│ │ │ ├── PAGSolidLayer.cpp +│ │ │ ├── PAGTextLayer.cpp +│ │ │ └── ContentVersion.cpp # Dirty-version counter for cache invalidation +│ │ ├── caches/ # Frame and content caches +│ │ │ ├── RenderCache.cpp # Master cache; manages GPU resource lifecycle +│ │ │ ├── LayerCache.cpp # Per-layer snapshot cache +│ │ │ ├── ShapeContentCache.cpp +│ │ │ ├── TextContentCache.cpp +│ │ │ ├── ImageContentCache.cpp +│ │ │ ├── CompositionCache.cpp +│ │ │ ├── DiskCache.cpp # Disk-backed cache +│ │ │ ├── DiskIOWorker.cpp # Background I/O thread +│ │ │ ├── SequenceFile.cpp # Video/bitmap sequence disk cache +│ │ │ ├── TextBlock.cpp # Shaped text block cache +│ │ │ └── GraphicContent.cpp +│ │ ├── renderers/ # Per-content-type rendering logic +│ │ │ ├── LayerRenderer.cpp # Dispatch + transform/effect chain +│ │ │ ├── ShapeRenderer.cpp +│ │ │ ├── TextRenderer.cpp +│ │ │ ├── FilterRenderer.cpp # Effect/filter application +│ │ │ ├── CompositionRenderer.cpp +│ │ │ ├── MaskRenderer.cpp +│ │ │ ├── TrackMatteRenderer.cpp +│ │ │ ├── TransformRenderer.cpp +│ │ │ ├── TextAnimatorRenderer.cpp +│ │ │ └── TextSelectorRenderer.cpp +│ │ ├── filters/ # GPU filter implementations (32+ types) +│ │ │ ├── gaussianblur/ # Gaussian blur passes +│ │ │ ├── glow/ # Glow/outer glow filters +│ │ │ ├── layerstyle/ # Drop shadow, stroke, etc. +│ │ │ ├── utils/ # Filter utilities +│ │ │ ├── LayerStylesFilter.cpp +│ │ │ ├── MotionBlurFilter.cpp +│ │ │ ├── DisplacementMapFilter.cpp +│ │ │ └── … (20+ more filter files) +│ │ ├── graphics/ # Low-level graphic primitives +│ │ │ ├── Recorder.h # Draw command recorder +│ │ │ ├── Picture.h # Raster/GPU picture wrapper +│ │ │ ├── Shape.h # Vector path wrapper +│ │ │ ├── Snapshot.h # GPU texture snapshot +│ │ │ └── ImageProxy.h # Lazy image decode proxy +│ │ ├── sequences/ # Async sequence decode pipeline +│ │ │ ├── SequenceInfo.h # Sequence metadata +│ │ │ └── SequenceImageQueue.h # Async frame queue +│ │ ├── drawables/ # Platform Drawable implementations +│ │ ├── editing/ # Runtime layer editing helpers +│ │ ├── video/ # Video sequence decode coordination +│ │ └── utils/ # Render utilities (ScaleMode, LockGuard, shaper/) +│ │ +│ ├── renderer/ # Text layout engine (HarfBuzz/BiDi integration) +│ │ ├── TextShaper.cpp # HarfBuzz shaping +│ │ ├── LineBreaker.cpp # Unicode line breaking +│ │ ├── BidiResolver.cpp # Bidirectional text +│ │ ├── GlyphRunRenderer.cpp # Glyph run draw calls +│ │ ├── LayerBuilder.cpp # Text layer build from shaped glyphs +│ │ ├── FontEmbedder.cpp # Font data embedding +│ │ ├── ImageEmbedder.cpp # Inline image embedding +│ │ ├── PunctuationSquash.cpp # CJK punctuation compression +│ │ └── ToTGFX.cpp # Convert to TGFX draw calls +│ │ +│ ├── pagx/ # PAGX XML format support +│ │ ├── PAGXImporter.cpp # XML → PAGFile conversion (Expat parser) +│ │ ├── PAGXExporter.cpp # PAGFile → XML serialization +│ │ ├── PAGXDocument.cpp # PAGX document model +│ │ ├── FontConfig.cpp # Font config for PAGX +│ │ ├── SystemFonts.cpp # OS font enumeration (silent fail) +│ │ ├── TextLayout.cpp # PAGX text layout engine +│ │ ├── LayoutContext.cpp # Layout pass context +│ │ ├── LayoutNode.cpp # Node layout calculations +│ │ ├── PathData.cpp # Path data handling +│ │ ├── nodes/ # PAGX node implementations +│ │ ├── svg/ # SVG import/export +│ │ ├── utils/ # PAGX utility helpers +│ │ └── xml/ # Expat XML wrapper +│ │ +│ ├── cli/ # pagx-cli command-line tool +│ │ ├── main.cpp # CLI entry point, command dispatch +│ │ ├── CliUtils.cpp # Shared CLI helpers +│ │ ├── CommandBounds.cpp # `bounds` command +│ │ ├── CommandEmbed.cpp # `embed` command (embed fonts/images into PAGX) +│ │ ├── CommandExport.cpp # `export` command (PAGX → PAG) +│ │ ├── CommandFont.cpp # `font` command (list/verify fonts) +│ │ ├── CommandFormat.cpp # `format` command +│ │ ├── CommandImport.cpp # `import` command (PAG → PAGX) +│ │ ├── CommandLayout.cpp # `layout` command +│ │ ├── CommandRender.cpp # `render` command (render to image) +│ │ ├── CommandResolve.cpp # `resolve` command +│ │ ├── CommandVerify.cpp # `verify` command +│ │ ├── FormatUtils.cpp # Output formatting helpers +│ │ ├── LayoutUtils.cpp # Layout computation helpers +│ │ └── XPathQuery.cpp # XPath-like XML query helper +│ │ +│ ├── platform/ # Platform-specific implementations +│ │ ├── Platform.cpp / .h # Platform abstraction interface +│ │ ├── android/ # JNI bindings, hardware video, EGL context +│ │ ├── cocoa/ # Shared iOS+macOS: font loading, Metal context +│ │ │ └── private/ +│ │ ├── ios/ # iOS-specific: UIKit integration, VideoToolbox +│ │ │ └── private/ +│ │ ├── mac/ # macOS-specific: AppKit, CVDisplayLink +│ │ │ └── private/ +│ │ ├── win/ # Windows: DXGI/OpenGL context, DirectWrite fonts +│ │ ├── linux/ # Linux: EGL/GLX context, Fontconfig +│ │ ├── ohos/ # OpenHarmony +│ │ ├── web/ # Emscripten/WASM, WebGL +│ │ ├── qt/ # Qt platform abstraction +│ │ └── swiftshader/ # CPU rendering via SwiftShader +│ │ +│ └── c/ # C language API bindings +│ ├── pag_player.cpp +│ ├── pag_file.cpp +│ ├── pag_surface.cpp +│ ├── pag_image.cpp +│ ├── pag_layer.cpp +│ ├── pag_composition.cpp +│ ├── pag_text_document.cpp +│ ├── pag_font.cpp +│ ├── pag_decoder.cpp +│ ├── pag_animator.cpp +│ ├── pag_types.cpp +│ └── ext/ # EGL extension bindings +│ +├── test/ +│ ├── src/ # Google Test cases +│ │ ├── PAGFileTest.cpp # Core file loading and rendering +│ │ ├── PAGPlayerTest.cpp # Player control and lifecycle +│ │ ├── PAGSurfaceTest.cpp # Surface and GPU backend +│ │ ├── PAGTextLayerTest.cpp # Text rendering +│ │ ├── PAGFilterTest.cpp # Filter effects +│ │ ├── PAGXTest.cpp # PAGX format round-trip +│ │ ├── PAGXCliTest.cpp # CLI command tests +│ │ ├── PAGXSVGTest.cpp # SVG import/export +│ │ ├── PAGFontTest.cpp # Font loading/embedding +│ │ ├── PAGImageLayerTest.cpp +│ │ ├── PAGShapeLayerTest.cpp +│ │ ├── PAGSequenceTest.cpp +│ │ ├── PAGBlendTest.cpp +│ │ ├── PAGDiskCacheTest.cpp +│ │ ├── AsyncDecodeTest.cpp +│ │ ├── base/ # Base/utility test helpers +│ │ └── utils/ # Test utility code +│ ├── baseline/ # Screenshot baseline version tracking +│ │ └── version.json # Baseline version registry (do not modify manually) +│ └── out/ # Generated test output screenshots (gitignored) +│ +├── resources/ # Test fixture files +│ ├── AE/ # After Effects exported PAG/PAGX files +│ ├── cli/ # CLI test inputs +│ ├── font/ # Test font files +│ ├── svg/ # SVG test files +│ ├── text/ # Text test resources +│ └── … # Other fixture categories +│ +├── third_party/ # Vendored dependencies (not committed in full) +│ ├── tgfx/ # TGFX 2D GPU engine (core dependency) +│ ├── harfbuzz/ # Text shaping +│ ├── expat/ # XML parsing (used by PAGX) +│ ├── sheenbidi/ # Unicode BiDi algorithm +│ ├── lz4/ # LZ4 compression (codec layer) +│ └── libavc/ # H.264 software decoder +│ +├── android/ # Android Studio project / AAR build +├── ios/ # Xcode project / iOS framework build +├── mac/ # macOS framework build +├── win/ # Windows build helpers +├── linux/ # Linux build helpers +├── web/ # Web/WASM build +├── ohos/ # OpenHarmony build +├── viewer/ # Desktop PAG viewer application +├── exporter/ # After Effects exporter plugin +├── playground/ # Development scratch area +├── spec/ # PAG format specification +├── assets/ # Asset lists and metadata +├── CMakeLists.txt # Root CMake build definition +├── DEPS # Dependency version pins +├── vendor.json # Third-party vendor manifest +├── sync_deps.sh # Dependency sync script (run before first build) +├── codeformat.sh # clang-format + header guard formatter +└── accept_baseline.sh # Screenshot baseline acceptance (run via /accept-baseline) +``` + +## Key Files + +| File | Purpose | +|------|---------| +| `include/pag/pag.h` | Master public C++ API header | +| `include/pag/file.h` | PAGFile, PAGComposition, layer type declarations | +| `include/pagx/PAGXImporter.h` | PAGX XML import entry point | +| `include/pagx/PAGXExporter.h` | PAGX XML export entry point | +| `src/rendering/PAGPlayer.cpp` | Animation playback engine | +| `src/rendering/PAGSurface.cpp` | GPU rendering surface | +| `src/rendering/layers/PAGStage.cpp` | Root render scene graph | +| `src/rendering/caches/RenderCache.cpp` | GPU resource / frame cache | +| `src/rendering/renderers/LayerRenderer.cpp` | Per-layer render dispatch | +| `src/codec/Codec.cpp` | Binary PAG encode/decode entry | +| `src/pagx/PAGXImporter.cpp` | PAGX XML parser | +| `src/pagx/SystemFonts.cpp` | OS font enumeration (silent on failure) | +| `src/cli/main.cpp` | `pagx-cli` entry point | +| `src/platform/Platform.h` | Platform abstraction interface | +| `CMakeLists.txt` | Build system root | +| `test/baseline/version.json` | Screenshot baseline version registry | + +## Module Boundaries + +| Module | Allowed Dependencies | Forbidden | +|--------|---------------------|-----------| +| `src/base/` | Standard library, `include/pag/types.h` | rendering, codec, pagx, platform | +| `src/codec/` | `src/base/`, LZ4, MP4 utils | rendering, pagx, platform | +| `src/rendering/` | `src/base/`, `src/renderer/`, TGFX, platform `Drawable` | codec (via `PAGFile::Load` result), pagx | +| `src/renderer/` | `src/base/`, HarfBuzz, TGFX | rendering (no upward deps) | +| `src/pagx/` | `src/base/`, `src/codec/`, Expat, `src/renderer/` | rendering (uses codec output) | +| `src/cli/` | `src/pagx/`, `src/rendering/`, `include/pagx/` | platform internals | +| `src/platform/` | `src/rendering/` drawables, TGFX | `src/base/` data model directly | +| `src/c/` | `include/pag/` public API only | internal rendering headers | + +## Public API Surface + +### C++ API (`include/pag/`) + +- **`PAGPlayer`** — playback control: `setComposition`, `setProgress`, `flush`, `setSurface` +- **`PAGFile`** — file loading: `PAGFile::Load(path)`, `PAGFile::Load(bytes, size)` +- **`PAGSurface`** — rendering target: `PAGSurface::MakeFromWindow(...)`, `PAGSurface::MakeOffscreen(...)` +- **`PAGImage`** — image replacement: `PAGImage::FromPath(...)`, `PAGImage::FromPixels(...)` +- **`PAGFont`** — font registration: `PAGFont::RegisterFont(...)` +- **`PAGDecoder`** — frame-by-frame decode: `PAGDecoder::Make(file, ...)`, `copyFrameTo(pixels, ...)` +- **`PAGAnimator`** — animation timing helper +- **`PAGComposition`** / **`PAGLayer`** / typed sublayers — scene graph manipulation + +### PAGX API (`include/pagx/`) + +- **`PAGXImporter`** — `PAGXImporter::Import(path)` → `std::shared_ptr` +- **`PAGXExporter`** — `PAGXExporter::Export(file, path)` +- **`PAGXDocument`** — XML document model +- **`SVGImporter`** / **`SVGExporter`** — SVG conversion +- **`FontConfig`** — font embedding/lookup configuration +- **Node types** in `include/pagx/nodes/` — Layer, Group, Text, Image, Shape primitives +- **Value types** in `include/pagx/types/` — enums and structs used by node properties + +### C API (`include/pag/c/`) + +Thin C wrappers over the C++ API. All types are opaque pointers (`pag_player_t*`, etc.). Intended for FFI use from non-C++ languages. + +## Naming Conventions + +**Files:** +- C++ class files: `ClassName.cpp` / `ClassName.h` (PascalCase) +- CLI command files: `CommandVerb.cpp` / `CommandVerb.h` +- C API files: `pag_noun.cpp` / `pag_noun.h` (snake_case) + +**Directories:** +- Lowercase, plural for categories (`renderers/`, `caches/`, `filters/`, `layers/`) +- PascalCase for platform names (`android/`, `cocoa/`, `ios/`) + +## Where to Add New Code + +**New PAG data type / shape / effect:** +- Data model: `src/base/shapes/` or `src/base/effects/` +- Codec tag: `src/codec/tags/shapes/` or `src/codec/tags/effects/` +- Renderer: `src/rendering/renderers/` +- Test: `test/src/PAGShapeLayerTest.cpp` or new `test/src/PAG{Feature}Test.cpp` + +**New PAGX node type:** +- Public header: `include/pagx/nodes/NewNode.h` +- Implementation: `src/pagx/nodes/` +- Import/export support: `src/pagx/PAGXImporter.cpp` + `src/pagx/PAGXExporter.cpp` + +**New CLI command:** +- `src/cli/CommandNewVerb.cpp` + `src/cli/CommandNewVerb.h` +- Register in `src/cli/main.cpp` +- Test: `test/src/PAGXCliTest.cpp` + +**New filter effect:** +- `src/rendering/filters/NewFilter.cpp` + `.h` +- Wire into `LayerStylesFilter` or `FilterRenderer` as appropriate + +**New platform:** +- `src/platform/{platform}/` directory +- Implement `Drawable` interface for GPU context + surface +- Implement font loading + video decode hooks + +--- + +*Structure analysis: 2025-07-15* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000000..782d1d0a4b --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,214 @@ +# Testing Patterns + +**Analysis Date:** 2025-05-27 + +## Test Framework + +**Runner:** Google Test (gtest) +- Config: CMake target `PAGFullTest` (all tests + screenshot comparison) / `PAGUnitTest` (no screenshot comparison, `SKIP_FRAME_COMPARE` defined) +- Base header: `test/src/base/PAGTest.h` + +**Assertion Library:** Google Test built-in (`ASSERT_*`, `EXPECT_*`) + +**Run Commands:** +```bash +# Build (required before running) +./codeformat.sh 2>/dev/null; true +cmake -G Ninja -DPAG_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -B cmake-build-debug +cmake --build cmake-build-debug --target PAGFullTest + +# Run all tests +./cmake-build-debug/PAGFullTest + +# Run a specific test case +./cmake-build-debug/PAGFullTest --gtest_filter="PAGFileTest.TestPAGFileBase" + +# Run all tests in a suite +./cmake-build-debug/PAGFullTest --gtest_filter="PAGFileTest.*" +``` + +## Test File Organization + +**Location:** `test/src/` — all test `.cpp` files co-located in one directory + +**Naming:** `{SuiteName}Test.cpp` (e.g., `PAGFileTest.cpp`, `PAGFilterTest.cpp`, `PAGSurfaceTest.cpp`) + +**Utilities:** `test/src/utils/` — shared helpers for all test files: +- `Baseline.h` / `Baseline.cpp` — screenshot comparison logic +- `TestUtils.h` / `TestUtils.cpp` — `LoadPAGFile()`, `MakeSnapshot()`, `GetLayer()`, `MakeImage()`, etc. +- `OffscreenSurface.h` / `OffscreenSurface.cpp` — off-screen GPU surface factory +- `DevicePool.h` / `DevicePool.cpp` — GL device management +- `ProjectPath.h` / `ProjectPath.cpp` — absolute path resolution +- `Semaphore.h` / `Semaphore.cpp` — synchronization primitive for multi-thread tests +- `TestDir.h` / `TestDir.cpp` — test output directory management + +**Framework infrastructure:** `test/src/base/PAGTest.h` / `PAGTest.cpp` + +## Test Structure + +**Test Base Classes:** + +| Class | Defined In | Purpose | +|-------|------------|---------| +| `pag::PAGTest` | `test/src/base/PAGTest.h` | Base for all PAG rendering tests; tracks `hasFailure` global | +| `pag::PAGXTest` | `test/src/base/PAGTest.h` | Extends PAGTest; creates `GLDevice` + `Context` in `SetUp()`, unlocks in `TearDown()` | +| `pag::CLITest` | `test/src/base/PAGTest.h` | Base for CLI-only tests; no GPU setup | + +**Test Macros:** + +```cpp +// Standard rendering test (uses PAGTest base) +PAG_TEST(SuiteName, TestName) { + // test body +} + +// PAGX XML test (uses PAGXTest base — GLDevice already set up) +PAGX_TEST(SuiteName, TestName) { + // this->device and this->context are available +} + +// CLI test (uses CLITest base — no GPU context) +CLI_TEST(SuiteName, TestName) { + // test body +} +``` + +**Standard Setup Macros:** + +```cpp +// Default setup: loads resources/apitest/test.pag, creates shared OffscreenSurface +PAG_SETUP(pagSurface, pagPlayer, pagFile); +// Expands to: +// auto pagFile = LoadPAGFile("resources/apitest/test.pag"); +// auto pagSurface = OffscreenSurface::Make(pagFile->width(), pagFile->height()); +// auto pagPlayer = std::make_shared(); +// pagPlayer->setSurface(pagSurface); pagPlayer->setComposition(pagFile); + +// Isolated setup: creates PAGSurface::MakeOffscreen() instead of shared pool +PAG_SETUP_ISOLATED(pagSurface, pagPlayer, pagFile); + +// Custom path setup +PAG_SETUP_WITH_PATH(pagSurface, pagPlayer, pagFile, "resources/apitest/custom.pag"); +``` + +**Typical Test Pattern:** +```cpp +/** + * 用例描述: CornerPin用例 + */ +PAG_TEST(PAGFilterTest, CornerPin) { + auto pagFile = LoadPAGFile("resources/filter/cornerpin.pag"); + ASSERT_NE(pagFile, nullptr); + auto pagSurface = OffscreenSurface::Make(pagFile->width(), pagFile->height()); + ASSERT_NE(pagSurface, nullptr); + auto pagPlayer = std::make_shared(); + pagPlayer->setSurface(pagSurface); + pagPlayer->setComposition(pagFile); + + pagFile->setCurrentTime(1000000); + pagPlayer->flush(); + EXPECT_TRUE(Baseline::Compare(pagSurface, "PAGFilterTest/CornerPin")); +} +``` + +## Screenshot Testing + +**Mechanism:** `Baseline::Compare(surface, key)` compares rendered output against stored baseline images. + +**Key format:** `"{Folder}/{Name}"` — e.g., `"PAGFilterTest/CornerPin"`, `"PAGSurfaceTest/Mask"` + +**Output path:** `test/out/{Folder}/{Name}.webp` + +**Baseline path:** `test/out/{Folder}/{Name}_base.webp` + +**Version files:** +- `test/baseline/version.json` — committed repository baselines +- `test/baseline/.cache/version.json` — local cache of accepted versions + +**Comparison logic:** +- Both repo and cache versions exist AND differ → skip comparison, return `true` (change accepted) +- Otherwise → compare rendered output against `_base.webp`; fail if missing or pixels differ + +**Accepting baseline changes:** +- **Never** manually run `accept_baseline.sh` or modify `version.json` directly +- The only permitted workflow is the user running the `/accept-baseline` slash command + +**Baseline.Compare overloads** (`test/src/utils/Baseline.h`): +```cpp +Baseline::Compare(std::shared_ptr surface, const std::string& key); +Baseline::Compare(const tgfx::Bitmap& bitmap, const std::string& key); +Baseline::Compare(const tgfx::Pixmap& pixmap, const std::string& key); +Baseline::Compare(const std::shared_ptr& surface, const std::string& key); +Baseline::Compare(const std::shared_ptr& byteData, const std::string& key); +``` + +**Test output images** are written as WebP at 100% quality via `tgfx::ImageCodec::Encode()`. + +## Test Coverage + +**Test Suites** (`test/src/`): + +| File | Suite | Coverage Area | +|------|-------|---------------| +| `PAGFileTest.cpp` | `PAGFileTest` | PAGFile load, metadata, text/image layer access | +| `PAGPlayerTest.cpp` | `PAGPlayerTest` | PAGPlayer composition, flush, surface switching | +| `PAGSurfaceTest.cpp` | `PAGSurfaceTest` | Surface from texture, pixel readback | +| `PAGFilterTest.cpp` | `PAGFilterTest` | All 30+ filter effects (CornerPin, Bulge, blur, etc.) | +| `PAGLayerTest.cpp` | `PAGLayerTest` | Layer properties and manipulation | +| `PAGImageTest.cpp` | `PAGImageTest` | PAGImage creation and replacement | +| `PAGImageLayerTest.cpp` | `PAGImageLayerTest` | Image layer behavior | +| `PAGTextLayerTest.cpp` | `PAGTextLayerTest` | Text layer editing and rendering | +| `PAGShapeLayerTest.cpp` | `PAGShapeLayerTest` | Shape layer rendering | +| `PAGSolidLayerTest.cpp` | `PAGSolidLayerTest` | Solid layer rendering | +| `PAGCompositionTest.cpp` | `PAGCompositionTest` | Composition nesting and playback | +| `PAGBlendTest.cpp` | `PAGBlendTest` | Blend mode rendering | +| `PAGFontTest.cpp` | `PAGFontTest` | Font loading and fallback | +| `PAGDiskCacheTest.cpp` | `PAGDiskCacheTest` | Disk cache behavior | +| `PAGSequenceTest.cpp` | `PAGSequenceTest` | Bitmap/video sequence decoding | +| `PAGTimeStretchTest.cpp` | `PAGTimeStretchTest` | Time remapping | +| `PAGTimeUtilsTest.cpp` | `PAGTimeUtilsTest` | Time utility functions | +| `PAGGradientColorTest.cpp` | `PAGGradientColorTest` | Gradient color rendering | +| `PAGSimplePathTest.cpp` | `PAGSimplePathTest` | Path simplification | +| `PAGCompareFrameTest.cpp` | `PAGCompareFrameTest` | Frame-by-frame comparison | +| `PAGFuzzTest.cpp` | `PAGFuzzTest` | Fuzz / malformed input handling | +| `AsyncDecodeTest.cpp` | `AsyncDecode` | Async video/bitmap decoding | +| `MultiThreadCase.cpp` | `SimpleMultiThreadCase` | Concurrent multi-PAGPlayer rendering | +| `PAGXTest.cpp` | Various PAGX suites | PAGX import/export, layout, CLI operations | +| `PAGXSVGTest.cpp` | `PAGXSVGTest` | SVG export correctness | +| `PAGXCliTest.cpp` | Various CLI suites | CLI command execution (embed, export, verify, etc.) | + +## Running Tests + +**Build requirement:** Must pass `-DPAG_BUILD_TESTS=ON` to enable all modules. + +```bash +# Standard build + run +./codeformat.sh 2>/dev/null; true +cmake -G Ninja -DPAG_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -B cmake-build-debug +cmake --build cmake-build-debug --target PAGFullTest +./cmake-build-debug/PAGFullTest + +# Filter to one suite +./cmake-build-debug/PAGFullTest --gtest_filter="PAGFilterTest.*" + +# Filter to one case +./cmake-build-debug/PAGFullTest --gtest_filter="PAGFileTest.TestPAGFileBase" + +# Build with local TGFX source (when debugging rendering engine) +cmake -G Ninja -DPAG_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DTGFX_DIR=../tgfx -B cmake-build-debuglocal +cmake --build cmake-build-debuglocal --target PAGFullTest +``` + +**Non-zero exit code from PAGFullTest is normal when tests fail** — do not re-run the same command. + +**Test resources** live in `resources/` directory. Access via: +```cpp +LoadPAGFile("resources/apitest/test.pag"); // resolved to absolute path internally +MakePAGImage("resources/apitest/rotation.jpg"); +``` + +**Numeric test values** (font sizes, coordinates, matrices) must use integers — no floating-point literals. + +--- + +*Testing analysis: 2025-05-27* From ebb7ce49f6a9b5e8f4d831e0fbd54c02ae76e2fd Mon Sep 17 00:00:00 2001 From: codywwang Date: Tue, 28 Apr 2026 17:25:31 +0800 Subject: [PATCH 49/87] Add SystemFonts::FindFont fallback for CLI font resolution on FreeType backend. --- src/cli/CliUtils.h | 18 +++++-- src/pagx/SystemFonts.cpp | 109 +++++++++++++++++++++++++++++++++++++++ src/pagx/SystemFonts.h | 7 +++ 3 files changed, 131 insertions(+), 3 deletions(-) diff --git a/src/cli/CliUtils.h b/src/cli/CliUtils.h index 321773153c..1a70edd5b0 100644 --- a/src/cli/CliUtils.h +++ b/src/cli/CliUtils.h @@ -27,6 +27,7 @@ #include #include "pagx/FontConfig.h" #include "pagx/PAGXDocument.h" +#include "pagx/SystemFonts.h" #include "pagx/nodes/Layer.h" #include "pagx/utils/VerifyUtils.h" #include "tgfx/core/Typeface.h" @@ -47,9 +48,9 @@ static inline bool FontFamilyMatch(const std::string& requested, const std::stri } /** - * Resolves a system font by family and style with fallback. First attempts an exact match with the - * given style. If the result's fontFamily does not match the requested family (case-insensitive), - * or the style is not found, falls back to the family's default style. + * Resolves a system font by family and style with fallback. First attempts MakeFromName for an + * exact match. If MakeFromName is unavailable (e.g. FreeType backend on macOS), falls back to + * SystemFonts::FindFont to locate the font file path and loads via MakeFromPath. */ static inline std::shared_ptr ResolveSystemTypeface(const std::string& family, const std::string& style) { @@ -63,6 +64,17 @@ static inline std::shared_ptr ResolveSystemTypeface(const std::s return typeface; } } + // Fallback: locate the font file via platform APIs and load by path. + auto location = pagx::SystemFonts::FindFont(family, style); + if (!location.path.empty()) { + return tgfx::Typeface::MakeFromPath(location.path, location.ttcIndex); + } + if (!style.empty()) { + location = pagx::SystemFonts::FindFont(family, ""); + if (!location.path.empty()) { + return tgfx::Typeface::MakeFromPath(location.path, location.ttcIndex); + } + } return nullptr; } diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index d2707eb771..19f3bc564a 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -244,6 +244,57 @@ std::vector SystemFonts::AllFontFamilies() { return entries; } +FontLocation SystemFonts::FindFont(const std::string& family, const std::string& style) { + if (family.empty()) { + return {}; + } + auto cfFamily = + CFStringCreateWithCString(kCFAllocatorDefault, family.c_str(), kCFStringEncodingUTF8); + if (cfFamily == nullptr) { + return {}; + } + CFMutableDictionaryRef attributes = CFDictionaryCreateMutable( + kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionaryAddValue(attributes, kCTFontFamilyNameAttribute, cfFamily); + CFRelease(cfFamily); + if (!style.empty()) { + auto cfStyle = + CFStringCreateWithCString(kCFAllocatorDefault, style.c_str(), kCFStringEncodingUTF8); + if (cfStyle != nullptr) { + CFDictionaryAddValue(attributes, kCTFontStyleNameAttribute, cfStyle); + CFRelease(cfStyle); + } + } + const void* mandatoryKeys[] = {kCTFontFamilyNameAttribute, kCTFontStyleNameAttribute}; + int mandatoryCount = style.empty() ? 1 : 2; + CFSetRef mandatoryAttributes = + CFSetCreate(kCFAllocatorDefault, mandatoryKeys, mandatoryCount, &kCFTypeSetCallBacks); + CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes(attributes); + CFRelease(attributes); + if (descriptor == nullptr) { + if (mandatoryAttributes != nullptr) { + CFRelease(mandatoryAttributes); + } + return {}; + } + CFArrayRef matches = + CTFontDescriptorCreateMatchingFontDescriptors(descriptor, mandatoryAttributes); + CFRelease(descriptor); + if (mandatoryAttributes != nullptr) { + CFRelease(mandatoryAttributes); + } + if (matches == nullptr || CFArrayGetCount(matches) == 0) { + if (matches != nullptr) { + CFRelease(matches); + } + return {}; + } + auto matched = static_cast(CFArrayGetValueAtIndex(matches, 0)); + auto location = GetFontLocationFromDescriptor(matched); + CFRelease(matches); + return location; +} + } // namespace pagx #elif defined(_WIN32) @@ -446,6 +497,11 @@ std::vector SystemFonts::AllFontFamilies() { return entries; } +FontLocation SystemFonts::FindFont(const std::string&, const std::string&) { + // Windows FreeType backend already implements MakeFromName via DirectWrite. + return {}; +} + } // namespace pagx #elif defined(__linux__) @@ -582,6 +638,55 @@ std::vector SystemFonts::AllFontFamilies() { return entries; } +FontLocation SystemFonts::FindFont(const std::string& family, const std::string& style) { + if (family.empty()) { + return {}; + } + FcPattern* pattern = FcPatternCreate(); + if (pattern == nullptr) { + return {}; + } + FcPatternAddString(pattern, FC_FAMILY, reinterpret_cast(family.c_str())); + if (!style.empty()) { + FcPatternAddString(pattern, FC_STYLE, reinterpret_cast(style.c_str())); + } + FcPatternAddBool(pattern, FC_SCALABLE, FcTrue); + FcConfigSubstitute(nullptr, pattern, FcMatchPattern); + FcDefaultSubstitute(pattern); + + FcResult result = FcResultMatch; + FcPattern* matched = FcFontMatch(nullptr, pattern, &result); + FcPatternDestroy(pattern); + if (matched == nullptr) { + return {}; + } + FcChar8* filePath = nullptr; + if (FcPatternGetString(matched, FC_FILE, 0, &filePath) != FcResultMatch || filePath == nullptr) { + FcPatternDestroy(matched); + return {}; + } + FcChar8* matchedFamily = nullptr; + FcPatternGetString(matched, FC_FAMILY, 0, &matchedFamily); + if (matchedFamily == nullptr || + strcasecmp(reinterpret_cast(matchedFamily), family.c_str()) != 0) { + FcPatternDestroy(matched); + return {}; + } + FontLocation location = {}; + location.path = std::string(reinterpret_cast(filePath)); + location.fontFamily = std::string(reinterpret_cast(matchedFamily)); + int ttcIndex = 0; + FcPatternGetInteger(matched, FC_INDEX, 0, &ttcIndex); + location.ttcIndex = ttcIndex; + FcChar8* matchedStyle = nullptr; + if (FcPatternGetString(matched, FC_STYLE, 0, &matchedStyle) == FcResultMatch && + matchedStyle != nullptr) { + location.fontStyle = std::string(reinterpret_cast(matchedStyle)); + } + FcPatternDestroy(matched); + return location; +} + } // namespace pagx #else @@ -596,6 +701,10 @@ std::vector SystemFonts::AllFontFamilies() { return {}; } +FontLocation SystemFonts::FindFont(const std::string&, const std::string&) { + return {}; +} + } // namespace pagx #endif diff --git a/src/pagx/SystemFonts.h b/src/pagx/SystemFonts.h index cdee6f6a40..229ba98d14 100644 --- a/src/pagx/SystemFonts.h +++ b/src/pagx/SystemFonts.h @@ -66,6 +66,13 @@ class SystemFonts { * guaranteed (callers should sort as needed). */ static std::vector AllFontFamilies(); + + /** + * Finds a single system font by family and style name. Returns a FontLocation with the file path + * and TTC index. If style is empty, returns the family's default style. Returns an empty + * FontLocation (empty path) if no match is found. + */ + static FontLocation FindFont(const std::string& family, const std::string& style); }; } // namespace pagx From 14d20215eaa860e1c1bc9e3af3c167743cb5571f Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 30 Apr 2026 16:49:36 +0800 Subject: [PATCH 50/87] Fix use-after-free in FontEmbedder nodeMap cleanup and harden embed CLI against oversized files. Co-Authored-By: Claude Opus 4.7 --- include/pagx/PAGXDocument.h | 1 + src/cli/CommandEmbed.cpp | 21 ++++++++++++++++++--- src/cli/CommandFont.cpp | 9 +-------- src/pagx/PAGXDocument.cpp | 10 +++++++++- src/pagx/SystemFonts.h | 4 ++++ src/renderer/FontEmbedder.cpp | 8 ++++++++ src/renderer/ImageEmbedder.cpp | 11 ++++++++--- 7 files changed, 49 insertions(+), 15 deletions(-) diff --git a/include/pagx/PAGXDocument.h b/include/pagx/PAGXDocument.h index 968e795fb8..635cb91892 100644 --- a/include/pagx/PAGXDocument.h +++ b/include/pagx/PAGXDocument.h @@ -178,6 +178,7 @@ class PAGXDocument : public Node { friend class PAGXImporter; friend class PAGXExporter; friend class TextLayoutContext; + friend class FontEmbedder; }; } // namespace pagx diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp index 5689c6b6ee..1bddd291f4 100644 --- a/src/cli/CommandEmbed.cpp +++ b/src/cli/CommandEmbed.cpp @@ -17,6 +17,7 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "cli/CommandEmbed.h" +#include #include #include #include @@ -45,8 +46,8 @@ static void PrintEmbedUsage() { << "\n" << "Options:\n" << " -o, --output Output file path (default: overwrite input)\n" - << " --file Register a font file (can be specified multiple\n" - << " times)\n" + << " --font-file, --file Register a font file for glyph embedding\n" + << " (can be specified multiple times)\n" << " --fallback Add a fallback font file or system font name (can\n" << " be specified multiple times)\n" << " --skip-fonts Skip font embedding\n" @@ -60,7 +61,7 @@ static int ParseEmbedOptions(int argc, char* argv[], EmbedOptions* options) { std::string arg = argv[i]; if ((arg == "-o" || arg == "--output") && i + 1 < argc) { options->outputFile = argv[++i]; - } else if (arg == "--file" && i + 1 < argc) { + } else if ((arg == "--file" || arg == "--font-file") && i + 1 < argc) { options->fontFiles.push_back(argv[++i]); } else if (arg == "--fallback" && i + 1 < argc) { options->fallbacks.push_back(argv[++i]); @@ -136,6 +137,20 @@ int RunEmbed(int argc, char* argv[]) { } auto xml = PAGXExporter::ToXML(*document); + if (options.outputFile == options.inputFile) { + auto tempPath = options.outputFile + ".tmp"; + if (!WriteStringToFile(xml, tempPath, "pagx embed")) { + std::remove(tempPath.c_str()); + return 1; + } + if (std::rename(tempPath.c_str(), options.outputFile.c_str()) != 0) { + std::cerr << "pagx embed: failed to replace '" << options.outputFile << "'\n"; + std::remove(tempPath.c_str()); + return 1; + } + std::cout << "pagx embed: wrote " << options.outputFile << "\n"; + return 0; + } if (!WriteStringToFile(xml, options.outputFile, "pagx embed")) { return 1; } diff --git a/src/cli/CommandFont.cpp b/src/cli/CommandFont.cpp index 46b1f1a83d..9c6335b705 100644 --- a/src/cli/CommandFont.cpp +++ b/src/cli/CommandFont.cpp @@ -56,10 +56,6 @@ static void PrintFontUsage() { << "Exactly one of --file, --name, or --list must be specified.\n"; } -static bool FontFamilyLess(const pagx::FontFamilyEntry& lhs, const pagx::FontFamilyEntry& rhs) { - return lhs.family < rhs.family; -} - static void FormatFontListText(const std::vector& entries) { for (const auto& entry : entries) { if (entry.styles.empty()) { @@ -165,11 +161,8 @@ int RunFont(int argc, char* argv[]) { } if (options.listMode) { - if (options.sizeSpecified) { - std::cerr << "pagx font: warning: --size is ignored in --list mode\n"; - } auto entries = pagx::SystemFonts::AllFontFamilies(); - std::sort(entries.begin(), entries.end(), FontFamilyLess); + std::sort(entries.begin(), entries.end()); if (options.jsonOutput) { FormatFontListJson(entries); } else { diff --git a/src/pagx/PAGXDocument.cpp b/src/pagx/PAGXDocument.cpp index 19cc0bb112..1c3ce444fb 100644 --- a/src/pagx/PAGXDocument.cpp +++ b/src/pagx/PAGXDocument.cpp @@ -102,6 +102,14 @@ bool PAGXDocument::hasUnresolvedImports() const { return false; } +static bool IsUrlPath(const std::string& path) { + if (path.find("data:") == 0) { + return true; + } + auto schemePos = path.find("://"); + return schemePos != std::string::npos && path.find('/') > schemePos; +} + std::vector PAGXDocument::getExternalFilePaths() const { std::vector paths = {}; for (auto& node : nodes) { @@ -112,7 +120,7 @@ std::vector PAGXDocument::getExternalFilePaths() const { if (image->data != nullptr || image->filePath.empty()) { continue; } - if (image->filePath.find("data:") == 0) { + if (IsUrlPath(image->filePath)) { continue; } paths.push_back(image->filePath); diff --git a/src/pagx/SystemFonts.h b/src/pagx/SystemFonts.h index 229ba98d14..84abb8d2d1 100644 --- a/src/pagx/SystemFonts.h +++ b/src/pagx/SystemFonts.h @@ -41,6 +41,10 @@ struct FontLocation { struct FontFamilyEntry { std::string family = {}; std::vector styles = {}; + + bool operator<(const FontFamilyEntry& other) const { + return family < other.family; + } }; /** diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index f742acc29a..b0f4be4e9b 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -420,6 +420,14 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { } } nodes.resize(writeIdx); + + for (auto it = document->nodeMap.begin(); it != document->nodeMap.end();) { + if (toRemove.count(it->second) > 0) { + it = document->nodeMap.erase(it); + } else { + ++it; + } + } } bool FontEmbedder::embed(PAGXDocument* document) { diff --git a/src/renderer/ImageEmbedder.cpp b/src/renderer/ImageEmbedder.cpp index 11fa429292..a52afdec0e 100644 --- a/src/renderer/ImageEmbedder.cpp +++ b/src/renderer/ImageEmbedder.cpp @@ -19,16 +19,24 @@ #include "renderer/ImageEmbedder.h" #include #include +#include "base/utils/Log.h" #include "pagx/types/Data.h" namespace pagx { +static constexpr size_t MaxFileSize = 256 * 1024 * 1024; + static std::shared_ptr ReadFileToData(const std::string& path) { std::ifstream in(path, std::ios::binary | std::ios::ate); if (!in.is_open()) return nullptr; std::streampos end = in.tellg(); if (end <= 0) return nullptr; // covers tellg failure and empty file auto size = static_cast(end); + if (size > MaxFileSize) { + LOGE("ReadFileToData: file '%s' exceeds the maximum size limit (%zu bytes).", path.c_str(), + MaxFileSize); + return nullptr; + } in.seekg(0, std::ios::beg); auto* buffer = new (std::nothrow) uint8_t[size]; if (buffer == nullptr) return nullptr; @@ -45,9 +53,6 @@ bool ImageEmbedder::embed(PAGXDocument* document) { auto paths = document->getExternalFilePaths(); std::unordered_set loaded; for (const auto& path : paths) { - if (path.find("://") != std::string::npos) { - continue; // URL per D1.3 — silently skip - } if (!loaded.insert(path).second) { continue; } From f437e0dd8a52e619adec1fbbf773e4c1862a58b2 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 10:44:10 +0800 Subject: [PATCH 51/87] Reorder nodeMap cleanup before node compaction to avoid use-after-free of dangling pointers. --- src/renderer/FontEmbedder.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index b0f4be4e9b..2d4c7f1ef0 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -412,6 +412,15 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { toRemove.insert(node.get()); } } + // Clean up nodeMap before compacting nodes so the pointers in nodeMap remain valid. + for (auto it = document->nodeMap.begin(); it != document->nodeMap.end();) { + if (toRemove.count(it->second) > 0) { + it = document->nodeMap.erase(it); + } else { + ++it; + } + } + auto& nodes = document->nodes; size_t writeIdx = 0; for (size_t readIdx = 0; readIdx < nodes.size(); readIdx++) { @@ -420,14 +429,6 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { } } nodes.resize(writeIdx); - - for (auto it = document->nodeMap.begin(); it != document->nodeMap.end();) { - if (toRemove.count(it->second) > 0) { - it = document->nodeMap.erase(it); - } else { - ++it; - } - } } bool FontEmbedder::embed(PAGXDocument* document) { From e5c1618658a59ce3cb88aeab6c6ffd4f915045d3 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 11:17:40 +0800 Subject: [PATCH 52/87] Use known URL scheme prefixes in IsUrlPath to avoid false positives on Windows paths. --- src/pagx/PAGXDocument.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pagx/PAGXDocument.cpp b/src/pagx/PAGXDocument.cpp index 1c3ce444fb..d52742546f 100644 --- a/src/pagx/PAGXDocument.cpp +++ b/src/pagx/PAGXDocument.cpp @@ -106,8 +106,9 @@ static bool IsUrlPath(const std::string& path) { if (path.find("data:") == 0) { return true; } - auto schemePos = path.find("://"); - return schemePos != std::string::npos && path.find('/') > schemePos; + // Match known URL schemes rather than generic :// to avoid false positives on Windows paths + // like C://Users/file.png. + return path.find("http://") == 0 || path.find("https://") == 0; } std::vector PAGXDocument::getExternalFilePaths() const { From e61ee9b76d914d74e2a231e72efb89419f871174 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 11:25:32 +0800 Subject: [PATCH 53/87] Remove unused sizeSpecified field from FontOptions after warning removal. --- src/cli/CommandFont.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cli/CommandFont.cpp b/src/cli/CommandFont.cpp index 9c6335b705..162b23b956 100644 --- a/src/cli/CommandFont.cpp +++ b/src/cli/CommandFont.cpp @@ -35,7 +35,6 @@ struct FontOptions { std::string fontFile = {}; std::string fontName = {}; float fontSize = 12.0f; - bool sizeSpecified = false; bool jsonOutput = false; bool listMode = false; }; @@ -129,7 +128,6 @@ int RunFont(int argc, char* argv[]) { std::cerr << "pagx font: invalid font size '" << argv[i] << "'\n"; return 1; } - options.sizeSpecified = true; } else if (arg == "--json") { options.jsonOutput = true; } else if (arg == "--list") { From d38388fcdf54d51a115a2465b8d01c7d1ada7287 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 11:27:00 +0800 Subject: [PATCH 54/87] Support Windows backslash path separators in CliUtils path functions. --- src/cli/CliUtils.h | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/cli/CliUtils.h b/src/cli/CliUtils.h index 1a70edd5b0..d3e07274a3 100644 --- a/src/cli/CliUtils.h +++ b/src/cli/CliUtils.h @@ -78,13 +78,21 @@ static inline std::shared_ptr ResolveSystemTypeface(const std::s return nullptr; } +inline size_t FindLastPathSeparator(const std::string& path) { + auto slash = path.rfind('/'); + auto backslash = path.rfind('\\'); + if (slash == std::string::npos) return backslash; + if (backslash == std::string::npos) return slash; + return std::max(slash, backslash); +} + /** * Resolves a fallback font specifier to a Typeface. Accepts either a font file path (containing - * '/' or ending with a known font extension) or a font name in "family[,style]" format. + * a path separator or ending with a known font extension) or a font name in "family[,style]" format. */ inline std::shared_ptr ResolveFallbackTypeface(const std::string& specifier) { - // Treat as file path if it contains '/' or ends with a known font extension. - bool isFilePath = specifier.find('/') != std::string::npos; + bool isFilePath = + specifier.find('/') != std::string::npos || specifier.find('\\') != std::string::npos; if (!isFilePath) { auto dot = specifier.rfind('.'); if (dot != std::string::npos) { @@ -134,7 +142,7 @@ inline std::string ReplaceExtension(const std::string& path, const std::string& * Extracts the directory part of a path (including trailing slash), or returns "./" if none. */ inline std::string GetDirectory(const std::string& path) { - auto slash = path.rfind('/'); + auto slash = FindLastPathSeparator(path); if (slash != std::string::npos) { return path.substr(0, slash + 1); } @@ -145,7 +153,7 @@ inline std::string GetDirectory(const std::string& path) { * Extracts the base name from a path (filename without directory and extension). */ inline std::string GetBaseName(const std::string& path) { - auto slash = path.rfind('/'); + auto slash = FindLastPathSeparator(path); auto base = (slash != std::string::npos) ? path.substr(slash + 1) : path; auto dot = base.rfind('.'); if (dot != std::string::npos) { From 75a4b6ff3db19c2a5cdf0dea3b8f165bee0f5bce Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 11:28:44 +0800 Subject: [PATCH 55/87] Lowercase font extension before comparison to make case-insensitive matching effective. --- src/cli/CliUtils.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cli/CliUtils.h b/src/cli/CliUtils.h index d3e07274a3..d0d79fde09 100644 --- a/src/cli/CliUtils.h +++ b/src/cli/CliUtils.h @@ -97,8 +97,11 @@ inline std::shared_ptr ResolveFallbackTypeface(const std::string auto dot = specifier.rfind('.'); if (dot != std::string::npos) { auto ext = specifier.substr(dot); - isFilePath = ext == ".ttf" || ext == ".otf" || ext == ".ttc" || ext == ".woff" || - ext == ".woff2" || ext == ".TTF" || ext == ".OTF" || ext == ".TTC"; + for (auto& ch : ext) { + ch = static_cast(std::tolower(static_cast(ch))); + } + isFilePath = + ext == ".ttf" || ext == ".otf" || ext == ".ttc" || ext == ".woff" || ext == ".woff2"; } } if (isFilePath) { From 6e32c4c8c51b98e861cdffa2f62c65ad3c8d0453 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 11:30:05 +0800 Subject: [PATCH 56/87] Check return value of loadFileData in ImageEmbedder to detect missing file paths. --- src/renderer/ImageEmbedder.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/ImageEmbedder.cpp b/src/renderer/ImageEmbedder.cpp index a52afdec0e..df01acccca 100644 --- a/src/renderer/ImageEmbedder.cpp +++ b/src/renderer/ImageEmbedder.cpp @@ -61,7 +61,10 @@ bool ImageEmbedder::embed(PAGXDocument* document) { lastErrorPath_ = path; return false; } - document->loadFileData(path, data); + if (!document->loadFileData(path, data)) { + lastErrorPath_ = path; + return false; + } } return true; } From 6e7ad22b26dbf6e4c6f6d52f6cdc21f95a9ffaa5 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 11:33:12 +0800 Subject: [PATCH 57/87] Show concrete input path in embed usage line instead of placeholder syntax. --- src/cli/CommandEmbed.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp index 1bddd291f4..567976740a 100644 --- a/src/cli/CommandEmbed.cpp +++ b/src/cli/CommandEmbed.cpp @@ -40,7 +40,7 @@ struct EmbedOptions { static void PrintEmbedUsage() { std::cout - << "Usage: pagx embed [options] \n" + << "Usage: pagx embed [options] input.pagx\n" << "\n" << "Embed font glyphs and images into a PAGX file for self-contained output.\n" << "\n" From 565fb300c309e219334359f65a9c5bf7e06b0c58 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 11:34:17 +0800 Subject: [PATCH 58/87] Remove .planning/codebase/ from repository and add .planning/ to gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1c2e2b27e5..9bcee3817f 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ local.properties # CodeBuddy .codebuddy/designs/ +# AI agent planning artifacts +.planning/ + # Local config *.local.json *.local.md From 8396456bd5c077042fb5b36b62b60e715b61ce66 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 11:34:54 +0800 Subject: [PATCH 59/87] Stop tracking .planning/codebase/ files from repository index. --- .planning/codebase/ARCHITECTURE.md | 204 ----------------- .planning/codebase/CONCERNS.md | 236 ------------------- .planning/codebase/CONVENTIONS.md | 166 -------------- .planning/codebase/INTEGRATIONS.md | 318 -------------------------- .planning/codebase/STACK.md | 172 -------------- .planning/codebase/STRUCTURE.md | 353 ----------------------------- .planning/codebase/TESTING.md | 214 ----------------- 7 files changed, 1663 deletions(-) delete mode 100644 .planning/codebase/ARCHITECTURE.md delete mode 100644 .planning/codebase/CONCERNS.md delete mode 100644 .planning/codebase/CONVENTIONS.md delete mode 100644 .planning/codebase/INTEGRATIONS.md delete mode 100644 .planning/codebase/STACK.md delete mode 100644 .planning/codebase/STRUCTURE.md delete mode 100644 .planning/codebase/TESTING.md diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md deleted file mode 100644 index bb8b1f87c6..0000000000 --- a/.planning/codebase/ARCHITECTURE.md +++ /dev/null @@ -1,204 +0,0 @@ - -# Architecture - -**Analysis Date:** 2025-07-15 - -## System Overview - -```text -┌──────────────────────────────────────────────────────────────────────────┐ -│ Public API Layer │ -│ include/pag/pag.h include/pag/file.h include/pag/decoder.h │ -│ C API: include/pag/c/ │ PAGX API: include/pagx/ │ -└──────────┬──────────────┬───────────────────────┬────────────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌──────────────┐ ┌──────────────────┐ ┌──────────────────────────────┐ -│ Codec Layer │ │ Rendering Layer │ │ PAGX / CLI Tools │ -│ src/codec/ │ │ src/rendering/ │ │ src/pagx/ src/cli/ │ -│ (decode / │ │ (playback + │ │ (XML import/export, │ -│ encode │ │ GPU submit) │ │ SVG, command-line tool) │ -│ PAG binary)│ │ │ │ │ -└──────┬───────┘ └────────┬─────────┘ └──────────────────────────────┘ - │ │ - ▼ ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ Data Model src/base/ │ -│ Composition, Layer, Shape, Effect, Keyframe, Transform, Sequence │ -└──────────────────────────────────────┬──────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ Text Layout Engine src/renderer/ │ -│ HarfBuzz shaping, line breaking, BiDi, font embedding │ -└──────────────────────────────────────┬──────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ TGFX 2D Graphics Engine (third_party/tgfx) │ -│ GPU backend (OpenGL/Metal/Vulkan), path rendering, image decoding │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ Platform Abstraction src/platform/ │ -│ android/ ios/ mac/ cocoa/ win/ linux/ ohos/ web/ qt/ swiftshader/ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -## Core Subsystems - -| Subsystem | Location | Responsibility | -|-----------|----------|----------------| -| Data Model | `src/base/` | PAG scene graph: Compositions, Layers, Shapes, Effects, Keyframes | -| Codec | `src/codec/` | Tag-based binary PAG format encode/decode; LZ4 compression; MP4 muxing | -| Rendering | `src/rendering/` | PAGPlayer/PAGSurface/PAGStage, frame caches, GPU draw submission | -| Text Layout | `src/renderer/` | HarfBuzz text shaping, BiDi, line breaking, font embedding | -| PAGX | `src/pagx/` | XML format import/export, SVG support, layout engine | -| CLI | `src/cli/` | `pagx-cli` commands: embed, export, import, font, render, verify, etc. | -| Platform | `src/platform/` | GPU context init, hardware video decode, system font loading, event loop | -| C Bindings | `src/c/` | C language API wrapping the C++ public API | -| Public C++ API | `include/pag/` | `PAGPlayer`, `PAGFile`, `PAGSurface`, `PAGImage`, `PAGFont`, etc. | -| PAGX Public API | `include/pagx/` | `PAGXImporter`, `PAGXExporter`, `PAGXDocument`, node/type headers | - -## Data Flow - -### Primary Playback Path - -1. **Load PAG file** — `PAGFile::Load(path)` (`include/pag/file.h`, `src/rendering/layers/PAGFile.cpp`) -2. **Binary decode** — `Codec::Decode()` reads tag stream (`src/codec/Codec.cpp`) -3. **Build object tree** — `Composition`, `Layer`, `Shape` objects in `src/base/` -4. **Create player** — `PAGPlayer` owns `PAGStage` + `RenderCache` (`src/rendering/PAGPlayer.cpp`) -5. **Attach surface** — `PAGPlayer::setSurface(PAGSurface)` wraps platform `Drawable` -6. **Set progress** — `PAGPlayer::setProgress(t)` updates animation state on `PAGStage` -7. **Render** — `PAGPlayer::flush()` → `PAGStage::draw()` → per-layer renderers → TGFX GPU submit - -### PAGX Conversion Path - -1. **Import** — `PAGXImporter::Import(path)` parses XML via Expat (`src/pagx/PAGXImporter.cpp`) -2. **Build PAGX scene graph** — `PAGXDocument` + `pagx::Node` tree (`include/pagx/`) -3. **Export to PAG** — `PAGXExporter::Export()` serializes binary PAG (`src/pagx/PAGXExporter.cpp`) -4. **CLI command** — `CommandImport` / `CommandExport` wrappers in `src/cli/` - -### Render Frame Flow (detail) - -``` -PAGPlayer::flush() - └─ RenderCache::beginFrame() [src/rendering/caches/RenderCache.cpp] - └─ PAGStage::draw(recorder) [src/rendering/layers/PAGStage.cpp] - └─ PAGLayer::draw() [src/rendering/layers/PAGLayer.cpp] - └─ LayerRenderer::draw() [src/rendering/renderers/LayerRenderer.cpp] - ├─ ContentCache hit? [src/rendering/caches/] - ├─ ShapeRenderer / TextRenderer / FilterRenderer - └─ TrackMatteRenderer / MaskRenderer - └─ RenderCache::attachToContext() → TGFX draw calls → GPU submit -``` - -### State Management - -- All mutations guarded by `rootLocker` (a shared mutex on `PAGStage`) -- `LockGuard` / `ScopedLock` used consistently in `PAGPlayer` public methods -- Cache invalidation via `ContentVersion` dirty-tracking (`src/rendering/layers/ContentVersion.h`) - -## Layer Hierarchy - -### Composition Classes (`src/base/`) - -``` -Composition (abstract) -├─ VectorComposition — vector layers, shapes, text -├─ BitmapComposition — raster bitmap sequence -└─ VideoComposition — H.264/H.265 video sequence -``` - -### Layer Classes (`src/base/`) - -``` -Layer (abstract) -├─ ImageLayer — bitmap image content -├─ ShapeLayer — vector shape groups -├─ SolidLayer — solid color fill -├─ TextLayer — text with animator -├─ PreComposeLayer — nested composition reference -└─ CameraLayer — 3D camera -``` - -### PAG Runtime Layer Classes (`src/rendering/layers/`) - -``` -PAGLayer (public API wrapper around data-model Layer) -├─ PAGComposition — wraps Composition; supports child layers -│ ├─ PAGFile — top-level file wrapper -│ └─ PAGStage — root render graph (owns RenderCache) -├─ PAGImageLayer -├─ PAGShapeLayer -├─ PAGSolidLayer -└─ PAGTextLayer -``` - -### Renderer Classes (`src/rendering/renderers/`) - -| Renderer | Handles | -|----------|---------| -| `LayerRenderer` | Dispatch to typed renderers; apply transforms/effects | -| `ShapeRenderer` | Vector shape drawing via TGFX paths | -| `TextRenderer` | Text layout integration; glyph drawing | -| `FilterRenderer` | Effect/filter chain application | -| `CompositionRenderer` | Nested composition flattening | -| `MaskRenderer` | Alpha/luma mask application | -| `TrackMatteRenderer` | Track matte compositing | -| `TransformRenderer` | Transform/opacity application | -| `TextAnimatorRenderer` | Per-character text animation | - -### Cache Classes (`src/rendering/caches/`) - -| Cache | Stores | -|-------|--------| -| `RenderCache` | Master cache coordinator; owns GPU context attachment | -| `LayerCache` | Per-layer transform/content snapshots | -| `ShapeContentCache` | Pre-rasterized shape geometry | -| `TextContentCache` | Shaped text glyph runs | -| `ImageContentCache` | Decoded image bitmaps | -| `CompositionCache` | Flattened composition snapshots | -| `SequenceFile` | Disk-backed video/bitmap sequence cache | -| `DiskCache` | General disk I/O cache with worker thread | - -## Key Design Patterns - -### 1. Data Model / Runtime Wrapper Split -`src/base/` contains pure data (`Layer`, `Composition`, `Shape`, etc.) with no rendering logic. `src/rendering/layers/` wraps these in `PAGLayer`/`PAGComposition` runtime objects that hold cache state and expose the public API. Data objects are decoded once; runtime wrappers are created per `PAGPlayer` instance. - -### 2. Tag-Based Codec -The PAG binary format uses a tag-stream design (`src/codec/tags/`). Each feature (effect, shape type, layer style) maps to one or more numeric tag IDs. New features add new tags without breaking backward compatibility. Tags above a known version are skipped by older decoders. - -### 3. Dirty-Region Cache Invalidation -`ContentVersion` counters (`src/rendering/layers/ContentVersion.h`) track when layer content changes. Renderers check version numbers before re-rasterizing, enabling frame-to-frame content reuse without explicit cache invalidation calls. - -### 4. Platform Abstraction via `Drawable` -`PAGSurface` holds a `Drawable` interface (`src/rendering/drawables/`). Each platform implements `Drawable` to provide a TGFX `Surface` backed by the platform's GPU context (EGL on Android, CAMetalLayer on iOS/Mac, etc.). - -### 5. Sequence Decode Pipeline -Video/bitmap sequences have a dedicated async pipeline: `SequenceInfo` → `SequenceImageQueue` → hardware decoder (via platform layer) → `RenderCache` frame promotion. `DiskIOWorker` handles background I/O for disk-cached sequences. - -### 6. No Exceptions / No RTTI -The codebase prohibits `throw`/`try`/`catch` and `dynamic_cast`. Error propagation uses return values, `nullptr`, or output parameters. - -## Error Handling - -**Strategy:** Return `nullptr` / empty objects on failure; no exceptions. - -**Patterns:** -- `PAGFile::Load()` returns `nullptr` on decode failure -- `std::shared_ptr` used for all heap-allocated public objects (automatic lifetime) -- Platform implementations return empty on silent failure (e.g., `src/cli/`, `src/pagx/SystemFonts.cpp`) - -## Cross-Cutting Concerns - -**Logging:** No `LOG` macros in `src/cli/` or `src/pagx/SystemFonts.cpp`; silent empty-return convention. -**Thread safety:** `rootLocker` mutex on `PAGStage`; `LockGuard`/`ScopedLock` wrappers. -**Memory:** `std::shared_ptr` ownership throughout public API; `RenderCache` manages GPU resource lifetime. -**GPU:** All GPU calls go through TGFX (`third_party/tgfx`); libpag never calls GPU APIs directly. - ---- - -*Architecture analysis: 2025-07-15* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md deleted file mode 100644 index e7b095f799..0000000000 --- a/.planning/codebase/CONCERNS.md +++ /dev/null @@ -1,236 +0,0 @@ -# Codebase Concerns - -**Analysis Date:** 2025-01-27 - ---- - -## Tech Debt - -### Raw Memory Management in Codec Layer - -- Issue: `src/codec/` and `src/codec/tags/` extensively use raw `new[]`/`delete[]` and manual `delete` for object lifetimes, instead of `std::unique_ptr` or `std::vector`. This includes `DecodeStream`, `EncodeStream`, `CodecContext`, `LayerTag`, `Attributes.h`, and `AttributeHelper.h`. -- Files: `src/codec/utils/EncodeStream.cpp`, `src/codec/utils/DecodeStream.h`, `src/codec/utils/EncodeStream.h`, `src/codec/tags/LayerTag.cpp`, `src/codec/AttributeHelper.h`, `src/codec/Attributes.h`, `src/codec/CodecContext.cpp`, `src/codec/DataTypes.cpp` -- Impact: Memory leak risk on early-return error paths; manual ownership tracking required when extending the codec. -- Fix approach: Migrate raw arrays to `std::vector` or `std::unique_ptr`; use RAII wrappers for C-style allocations. - -### `mutable` Members in Stream Classes (Convention Violation) - -- Issue: `DecodeStream::context` and `EncodeStream::context` are declared `mutable StreamContext*`, used to record errors from `const` read/write methods. This violates the project convention to avoid `mutable`. -- Files: `src/codec/utils/DecodeStream.h:233`, `src/codec/utils/EncodeStream.h:228` -- Impact: Misleads callers about const-correctness; mutating error state through const methods obscures whether a read produced side effects. -- Fix approach: Make all error-producing read/write methods non-const, passing context explicitly or storing it non-mutably. - -### Lambda Usage in MP4Generator and SVGExporter - -- Issue: `src/codec/mp4/MP4Generator.cpp` contains ~17 lambdas (`[&]`, `[this]`) for all write-function callbacks. `src/pagx/svg/SVGExporter.cpp` and `src/platform/web/NativeTextShaper.cpp` also use lambdas. This violates the project convention to avoid lambdas in favor of explicit methods. -- Files: `src/codec/mp4/MP4Generator.cpp`, `src/pagx/svg/SVGExporter.cpp`, `src/platform/web/NativeTextShaper.cpp` -- Impact: Harder to read call stacks in debuggers; inconsistency with the rest of the codebase. -- Fix approach: Refactor each lambda into a named private method or static function. - -### `std::function` Usage in Codec Attribute Helpers - -- Issue: `src/codec/AttributeHelper.h` stores `std::function` and `std::function` as fields in `CustomAttribute`, and `src/codec/tags/ShapeTag.cpp` uses `std::unordered_map>`. `std::function` carries heap allocation and type-erasure overhead. -- Files: `src/codec/AttributeHelper.h`, `src/codec/tags/ShapeTag.cpp` -- Impact: Performance cost in hot codec paths; indirection obscures static dispatch. -- Fix approach: Replace with function pointer templates or virtual dispatch where appropriate. - -### Dead Code: OHOS Hardware Texture Path Disabled with `if (false)` - -- Issue: `OHOSVideoDecoder::onRenderFrame()` has the hardware NativeBuffer texture path guarded by `if (false && codecCategory == HARDWARE)`. The code exists but is permanently bypassed, requiring asynchronous HarmonyOS hardware decoding to be re-enabled once the platform fixes a jitter bug. -- Files: `src/platform/ohos/OHOSVideoDecoder.cpp:264` -- Impact: HarmonyOS hardware video decoding silently falls back to software YUV path, reducing performance on real devices. The dead code path may bitrot. -- Fix approach: Re-enable when HarmonyOS fixes the jitter issue; track with an upstream HarmonyOS issue link. - ---- - -## Complexity Hotspots - -### CommandVerify.cpp — 2841 Lines - -- Files: `src/cli/CommandVerify.cpp` -- Problem: Single file handles the entire `pagx verify` command: layer validation, painter deduplication, geometry analysis, heuristic rule engine. No sub-module decomposition. -- Why it's a concern: Every new verification rule adds to an already large file; hard to unit-test individual rules in isolation. -- Improvement path: Extract each verification check into a separate `Verifier` class or function file under `src/cli/verify/`. - -### SVGImporter.cpp — 2761 Lines - -- Files: `src/pagx/svg/SVGImporter.cpp` -- Problem: Handles the complete SVG-to-PAGX translation: element parsing, geometry import, gradient import, text layout, group merging, unknown-node preservation. Monolithic single-file implementation. -- Why it's a concern: SVG spec coverage is incomplete; adding new SVG features requires navigating 2700+ lines. -- Improvement path: Split by SVG element category (shapes, text, gradients, groups) into sub-files. - -### PAGXImporter.cpp — 2202 Lines - -- Files: `src/pagx/PAGXImporter.cpp` -- Problem: Parses every PAGX XML node type in a single file with deeply nested `if`/`switch` logic per attribute. -- Why it's a concern: Adding new PAGX node attributes or tag versions requires searching through 2200 lines; error-prone when tag versions accumulate. -- Improvement path: Generate per-type parse functions from a schema or split into per-node-category files. - -### TagCode Enum — 94 Versioned Tags in Public API - -- Files: `include/pag/file.h:51–152` -- Problem: The `TagCode` enum has 94 defined codes with version suffixes (`V2`, `V3`, `Extra`, `ExtraV2`). Reserved ranges (`34~44`) indicate past breakage. The public header exposes the full enum to all consumers and pulls in RTTR headers when `PAG_USE_RTTR` is defined. -- Why it's a concern: Every new format feature requires a new `V(N+1)` tag, and decoders must handle all historical versions forever. Version proliferation makes the tag table hard to audit. -- Improvement path: Document a tag retirement policy; consider a single extensible tag with a version field for new additions. - -### TextLayout.cpp — 1585 Lines - -- Files: `src/pagx/TextLayout.cpp` -- Problem: Handles HarfBuzz shaping, line breaking, SheenBidi BiDi resolution, and glyph metric computation all in one file. -- Why it's a concern: Complex bidirectional text + shaping logic is notoriously hard to test; changes risk regressions across scripts. -- Improvement path: Unit-test individual shaping cases; split BiDi logic into `BidiLayout.cpp`. - ---- - -## Missing Abstractions - -### No Linux Platform Implementation - -- Problem: `CMakeLists.txt` references `src/platform/linux/` (line 471) for native Linux builds, but the directory does not exist. Linux is listed as a supported platform in documentation, but there is no `NativePlatform`, font loading, or GPU context code for it. -- Impact: Linux builds without `USE_NATIVE_PLATFORM` fall through to the Qt backend (`src/platform/qt/`), which only provides a GPU drawable stub without font loading or display link. -- Fix approach: Implement a `src/platform/linux/` with at minimum `NativePlatform.cpp` (fontconfig font loading) and `GPUDrawable.cpp` (EGL context). - -### No Notification Mechanism for PAGImage Scale Factor Invalidation - -- Problem: `ImageReplacement::getScaleFactor()` recomputes the content matrix on every call but has no mechanism to notify upper layers when the `PAGImage` scale mode or matrix changes, requiring callers to re-query. -- Files: `src/rendering/editing/ImageReplacement.cpp:52–57` -- Impact: Callers may cache a stale scale factor after a PAGImage replacement update. -- Fix approach: Implement a dirty/observer notification from `PAGImage` to `ImageReplacement` when scale mode or matrix changes. - -### Platform Abstraction Has No Default Display Link on Most Platforms - -- Problem: `Platform::createDisplayLink()` returns `nullptr` by default; only iOS/macOS/Android/OHOS implement it. The Qt and Win platforms do not, limiting animation-loop integration on desktop. -- Files: `src/platform/Platform.h:92`, `src/platform/qt/NativePlatform.cpp`, `src/platform/win/NativePlatform.cpp` -- Impact: `PAGAnimator` cannot drive itself automatically on Win/Qt — callers must implement their own timer loop. -- Fix approach: Provide a generic timer-based `DisplayLink` fallback in `src/platform/Platform.cpp`. - ---- - -## Risk Areas - -### Codec Error Handling: `throwException` Is Not a Real Exception - -- Problem: `StreamContext::throwException()` (named misleadingly) merely appends to an `errorMessages` vector and returns a bool. Callers use the `PAGThrowError` macro to log and record errors, but decoding continues unless the caller explicitly checks `hasException()`. Corrupted data can cause reads past the end of a stream before the error is noticed. -- Files: `src/codec/utils/StreamContext.h` -- Impact: Malformed or fuzzer-generated PAG files may partially decode into undefined state before errors surface; the 116 fuzz corpus files (`resources/fuzz/`) suggest this is a known concern. -- Fix approach: Make `checkEndOfFile` abort reads immediately by returning early (it already exists in `DecodeStream`); audit all callers of `PAGThrowError` to ensure they propagate the error upward promptly. - -### Fuzz Corpus Is Static (116 Files) - -- Problem: `test/src/PAGFuzzTest.cpp` loads every file in `resources/fuzz/` and decodes them. The corpus is manually curated (116 files) with no continuous fuzzing infrastructure. -- Files: `test/src/PAGFuzzTest.cpp`, `resources/fuzz/` -- Impact: Coverage-guided fuzzing (libFuzzer/AFL) is not integrated; new codec paths added for future tags may introduce vulnerabilities that the static corpus won't discover. -- Fix approach: Add a `PAGFuzzTarget` build target for libFuzzer; seed with the existing corpus. - -### OHOS Async Decode Condition Variable Deadlock Risk - -- Problem: `OHOSVideoDecoder::onSendBytes()` and `onRenderFrame()` use `condition_variable::wait()` with a lambda capturing `this`. If the codec callback thread fails to push to the queue (codec error, shutdown race), `onSendBytes` will block indefinitely. -- Files: `src/platform/ohos/OHOSVideoDecoder.cpp:165–168`, `src/platform/ohos/OHOSVideoDecoder.cpp:208` -- Impact: Potential hang on OHOS when hardware codec enters an error state. -- Fix approach: Add a timeout or a cancellation flag to the `wait()` calls to allow graceful teardown. - -### RTTR Macro Pollution in Public Header - -- Problem: `include/pag/file.h` conditionally includes 9 RTTR headers and defines `RTTR_AUTO_REGISTER_CLASS` on all public types when `PAG_USE_RTTR` is defined. This couples the public API to a reflection library that most consumers do not need. -- Files: `include/pag/file.h:25–44` -- Impact: Increases compile time for all consumers; RTTR headers pull in `clang diagnostic` suppressions globally. -- Fix approach: Move RTTR registration to a separate `include/pag/file_rttr.h` that consumers opt in to explicitly. - -### `reinterpret_cast` Across HarfBuzz C Callbacks - -- Problem: `src/renderer/TextShaper.cpp` uses `reinterpret_cast` to store and retrieve typed pointers (`DataPointer*`, `DataPointer*`) in HarfBuzz `void*` user-data slots, then `delete`s them in destroy callbacks. -- Files: `src/renderer/TextShaper.cpp:41–49` -- Impact: Type confusion if a wrong destroy callback is called; manual ownership requires careful pairing of create/destroy across C API boundaries. -- Fix approach: Wrap user-data in a typed destructor struct; consider `std::unique_ptr` with a custom deleter where the C API allows. - ---- - -## Known Issues (TODO/FIXME Analysis) - -### [OHOS] Hardware Video Decode Disabled - -- Location: `src/platform/ohos/OHOSVideoDecoder.cpp:262` -- Owner: kevingpqi -- Issue: Asynchronous hardware decoding on HarmonyOS causes video jitter. Hardware texture path is disabled with `if (false && ...)` until HarmonyOS platform fixes the issue. -- Risk: HarmonyOS users always run software decode; no automatic re-enablement when platform is fixed. - -### [Rendering] PAGPlayer `renderingTime()` Metric Stale - -- Location: `src/rendering/PAGPlayer.cpp:395` -- Owner: domrjchen -- Issue: Performance monitoring panel of PAGViewer has not been updated to display new timing properties added to `RenderCache`. -- Risk: Developers using `renderingTime()` may miss new breakdown metrics; documentation mismatch. - -### [Editing] PAGImage Scale Factor Notification Missing - -- Location: `src/rendering/editing/ImageReplacement.cpp:53` -- Owner: domrjchen -- Issue: No notification mechanism exists to invalidate/reset `scaleFactor` when `PAGImage` scale mode or matrix changes. -- Risk: Callers may receive stale scale factors after image replacement updates. - -### [Rendering] BulgeFilter SwiftShader Anomaly - -- Location: `src/rendering/filters/BulgeFilter.cpp:130` -- Issue: Using `mix()` to eliminate branch statements in the BulgeFilter GLSL shader causes visual artifacts on SwiftShader. Workaround is an explicit `if (distance <= 1.0)` branch. Root cause unresolved. -- Risk: If SwiftShader is used for CI screenshot testing, BulgeFilter output may differ from GPU results. - ---- - -## Platform Gaps - -### Linux - -- Missing: `src/platform/linux/` directory referenced in `CMakeLists.txt:471` does not exist. -- Consequence: Linux native platform builds (`USE_NATIVE_PLATFORM`) will fail at cmake configuration. Linux builds fall back to Qt platform with no font loading. -- Coverage: No Linux-specific tests exist. - -### Qt / Desktop Windows - -- Partial: `src/platform/qt/` has only 4 files: `GPUDrawable.{cpp,h}`, `NativePlatform.{cpp,h}`. There is no font loading, no display link, and no hardware video decoder. -- Consequence: Qt builds require the host app to manually set fallback fonts and drive the animation loop; hardware video will never decode. - -### Web (Emscripten) - -- Partial: Hardware decode on Web (`src/platform/web/HardwareDecoder.cpp`) is a stub that delegates entirely to JavaScript via `PAGWasmBindings.cpp`. The WASM binding file is 676 lines of hand-maintained JS–C++ glue with no automated API sync. -- Risk: Adding new C++ API methods requires manually extending `PAGWasmBindings.cpp`; easy to miss. - -### OpenHarmony (OHOS) Hardware Decode - -- Partial: Hardware texture path (`OH_NativeBuffer`) is disabled (see Known Issues). Only software YUV path is active. -- Consequence: Video playback on HarmonyOS devices runs software decode regardless of hardware availability. - ---- - -## Test Coverage Gaps - -### PAGDecoder / PAGAnimator / PAGImageView - -- What's not tested: The newer high-level APIs (`PAGDecoder`, `PAGAnimator`, `PAGImageView`) have only 13 test references combined across all test files. No screenshot baseline tests for these APIs. -- Files: `src/rendering/PAGDecoder.cpp`, `src/rendering/PAGAnimator.cpp`, `test/src/PAGPlayerTest.cpp` -- Risk: Regressions in async frame delivery or animation timing may go undetected. -- Priority: Medium - -### BitmapSequence and VideoSequence HitTest - -- What's not tested: `test/src/PAGCompositionTest.cpp:530–532` has explicit TODO markers noting that `BitmapSequenceContent` and `VideoSequenceContent` HitTest cases are missing. -- Files: `test/src/PAGCompositionTest.cpp:530` -- Risk: Pixel-level hit testing on sequence layers may return wrong results silently. -- Priority: Medium - -### Filter Edge Cases (SwiftShader, MotionBlur, BulgeFilter) - -- What's not tested: `BulgeFilter` has a known SwiftShader rendering anomaly with no regression test. `MotionBlurFilter` has only 5 test references. No filter tests specifically target SwiftShader output. -- Files: `src/rendering/filters/BulgeFilter.cpp`, `src/rendering/filters/MotionBlurFilter.cpp` -- Risk: Filter visual regressions on CPU-only rendering paths (used in CI via SwiftShader) may produce incorrect baselines. -- Priority: Low - -### Codec Decode Error Paths - -- What's not tested: The `StreamContext::throwException()` error accumulation and early-exit behavior has no dedicated unit tests. Only fuzz corpus replay exercises error paths indirectly. -- Files: `src/codec/utils/StreamContext.h`, `src/codec/utils/DecodeStream.h` -- Risk: New codec tags introduced without testing decode-error propagation may silently produce corrupt object trees. -- Priority: High - ---- - -*Concerns audit: 2025-01-27* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md deleted file mode 100644 index e4b00b661d..0000000000 --- a/.planning/codebase/CONVENTIONS.md +++ /dev/null @@ -1,166 +0,0 @@ -# Coding Conventions - -**Analysis Date:** 2025-05-27 - -## Naming Conventions - -**Files:** -- Source files: `PascalCase.cpp` / `PascalCase.h` (e.g., `PAGPlayer.cpp`, `RenderCache.h`) -- Test files: `PascalCaseTest.cpp` (e.g., `PAGFileTest.cpp`, `PAGFilterTest.cpp`) -- Utility/helper files: `camelCase.cpp` / `camelCase.h` in `utils/` subdirectories -- PAGX type files in `include/pagx/types/`: `PascalCase.h` per type (e.g., `ScaleMode.h`, `TileMode.h`) - -**Classes and Structs:** -- `PascalCase` — all class and struct names (e.g., `PAGPlayer`, `RenderCache`, `PAGAnimator`) -- Prefix `PAG` for public-facing runtime classes, prefix `PAGX` for XML-format classes - -**Functions and Methods:** -- Global functions and static class methods: `PascalCase` (e.g., `LoadPAGFile()`, `PAGImage::FromPath()`) -- Member methods: `camelCase` (e.g., `pagPlayer->setSurface()`, `pagFile->setCurrentTime()`) -- Getters do not use `get` prefix when name is self-evident: `width()`, `height()`, `duration()` - -**Variables:** -- Local variables: `camelCase` (e.g., `pagFile`, `pagSurface`, `textLayer`) -- Member variables: `camelCase` (e.g., `baselineVersionPath`, `currentVersion`) -- Static constants: `ALL_CAPS_UNDERSCORE` (e.g., `OUT_ROOT`, `PAG_COMPLEX_FILE_PATH`) - -**Macros:** -- `ALL_CAPS_UNDERSCORE` (e.g., `PAG_TEST`, `PAG_SETUP`, `PAG_API`, `GL_VER`) -- No `k` prefix on constants — use `ALL_CAPS_UNDERSCORE` instead - -**Enums:** -- Enum type: `PascalCase`; enum values: `PascalCase` (e.g., `LayerType::PreCompose`, `EncodedFormat::WEBP`) - -## Code Style - -**Formatter:** clang-format, Google style base, configured in `.clang-format` -- Run before every build: `./codeformat.sh 2>/dev/null; true` -- Column limit: 100 characters -- Indent: 2 spaces, no tabs -- Pointer alignment: left (`int* ptr`) -- Brace style: same-line open brace (K&R variant) -- Max empty lines: 1 - -**Language Standard:** C++17 - -**Casting:** -- Use `static_cast()` and `reinterpret_cast()` as needed -- `dynamic_cast` is **forbidden** -- C-style casts are **forbidden** -- Prefer `std::static_pointer_cast()` for shared_ptr downcasts - -**Error Handling:** -- C++ exceptions (`throw`/`try`/`catch`) are **forbidden** -- Return `nullptr`, empty container, or `false` to signal failure -- Callers check return values; no exception propagation - -**Lambda Expressions:** -- Avoid in production code — use explicit named methods or free functions instead -- Short lambdas are allowed inline by clang-format config but should not appear in new code - -**Mutable Members:** -- Avoid `mutable` member variables -- If state must be modified in a conceptually-const context, prefer making the method non-const - -**Smart Pointers:** -- Use `std::shared_ptr` for shared ownership -- Use `std::unique_ptr` for exclusive ownership -- Factory functions return `std::shared_ptr` (e.g., `PAGFile::Load()`, `PAGImage::FromPath()`) - -## File Organization - -**Header Guards:** `#pragma once` in all headers (no traditional include guards) - -**Namespace:** All code lives in namespace `pag`; TGFX types in `tgfx`. Test files use `using namespace tgfx;` locally. - -**Include Order (clang-format merges and sorts includes alphabetically within a block):** -1. Own module header (for `.cpp` files) -2. Internal headers (project-relative paths, no angle brackets) -3. Third-party headers -4. Standard library headers - -Example from `PAGPlayer.cpp`: -```cpp -#include "base/utils/TGFXCast.h" -#include "base/utils/TimeUtil.h" -#include "pag/file.h" -#include "rendering/FileReporter.h" -#include "rendering/caches/RenderCache.h" -#include "tgfx/core/Clock.h" -``` - -**Directory Structure:** -- `include/pag/` — public API headers (detailed doc comments required) -- `include/pagx/` — PAGX format public API headers -- `src/base/` — data model (no rendering dependencies) -- `src/rendering/` — rendering pipeline -- `src/codec/` — binary format codec -- `src/pagx/` — XML format implementation -- `src/platform/{platform}/` — platform-specific implementations -- `src/cli/` — CLI tool -- `test/src/` — test cases -- `test/src/base/` — test framework infrastructure -- `test/src/utils/` — test helpers - -## Patterns to Avoid - -| Pattern | Reason | -|---------|--------| -| `dynamic_cast` | Forbidden — causes RTTI overhead and fragile downcasts | -| `throw` / `try` / `catch` | Forbidden — no exception support in codebase | -| Lambda expressions | Forbidden in production code — reduces readability | -| `mutable` member variables | Avoid — prefer non-const method instead | -| C-style casts `(Type)value` | Use `static_cast(value)` | -| Backward-compatibility shims | Remove all affected code paths when changing API | -| `k` prefix on constants | Use `ALL_CAPS_UNDERSCORE` instead | - -## Comment Standards - -**File Headers:** Every `.cpp` and `.h` file begins with the Tencent Apache 2.0 license banner (97 slashes wide). Copyright year for new files must be the current year (e.g., `Copyright (C) 2026 Tencent`); do not change the year in existing files. - -**Public API Headers (`include/`):** All public methods must have detailed doc comments including parameter descriptions: -```cpp -/** - * Creates a PAGImage object from an array of pixel data, return null if it's not valid pixels. - * @param pixels The pixel data to copy from. - * @param width The width of the pixel data. - * @param height The height of the pixel data. - * @param rowBytes The number of bytes between subsequent rows of the pixel data. - * @param colorType Describes how to interpret the components of a pixel. - * @param alphaType Describes how to interpret the alpha component of a pixel. - */ -static std::shared_ptr FromPixels(const void* pixels, ...); -``` - -**Other Public Methods (non-private, non-public-API):** One-sentence description of main purpose: -```cpp -/** - * PAGAnimator provides a simple timing engine for running animations. - */ -class PAGAnimator { ... }; -``` - -**Private Methods:** No comments. - -**Inline Code Comments:** No line-by-line comments inside function bodies. Only add comments to explain: -- Non-obvious algorithm choices -- Special boundary conditions -- Workarounds for known bugs (include the reason) - -Example of acceptable inline comment: -```cpp -// TODO(kevingpqi): We temporarily disable texture generation through NativeBuffer, as enabling -// asynchronous hardware... [reason for workaround] -``` - -**Test Case Comments:** Test cases have a Chinese description comment directly above the `PAG_TEST` macro: -```cpp -/** - * 用例描述: PAGFile基础信息获取 - */ -PAG_TEST(PAGFileTest, TestPAGFileBase) { ... } -``` - ---- - -*Convention analysis: 2025-05-27* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md deleted file mode 100644 index edf15abc14..0000000000 --- a/.planning/codebase/INTEGRATIONS.md +++ /dev/null @@ -1,318 +0,0 @@ -# External Integrations - -**Analysis Date:** 2026-04-28 - -libpag is an offline C++ rendering library — it has no network APIs, no authentication, no cloud services, and no webhooks. "Integrations" here means **OS-level system frameworks, GPU backends, hardware decoders, and font services** that the library binds to on each supported platform. - -## Third-party Libraries - -| Library | Version | Source | Used For | Condition | -|---------|---------|--------|----------|-----------| -| `tgfx` | 2.1.1 | `https://github.com/libpag/tgfx.git` → `third_party/tgfx/` | 2D GPU rendering engine | Always | -| `lz4` | 1.10.0 | `https://github.com/lz4/lz4.git` → `third_party/lz4/` | Sequence frame disk cache compression | Always (or system `compression.framework` on Apple) | -| `libavc` | — | `https://github.com/libpag/libavc.git` → `third_party/libavc/` | H.264 software decoder fallback | `PAG_USE_LIBAVC` (non-Android/iOS/OHOS) | -| `ffavc` | — | Prebuilt `.so` in `android/libpag/libs/${ANDROID_ABI}/` | H.264 decoder on Android | `PAG_USE_FFAVC` (Android) | -| `harfbuzz` | 10.1.0 | `https://github.com/harfbuzz/harfbuzz.git` → `third_party/harfbuzz/` | Text shaping / glyph layout | `PAG_USE_HARFBUZZ` (auto when PAGX) | -| `expat` | 2.6.3 | `https://github.com/libexpat/libexpat.git` → `third_party/expat/` | XML parsing for PAGX format | `PAG_BUILD_PAGX` | -| `SheenBidi` | 3.0.0 | `https://github.com/Tehreer/SheenBidi.git` → `third_party/SheenBidi/` | Unicode BiDi algorithm | `PAG_BUILD_PAGX` | -| `libxml2` | 2.15.2 | `https://github.com/GNOME/libxml2.git` → `third_party/libxml2/` | XPath + XML for CLI/tests | CLI + Tests only | -| `rttr` | — | `https://github.com/rttrorg/rttr.git` → `third_party/rttr/` | Runtime type reflection | `PAG_USE_RTTR` (opt-in) | -| `vendor_tools` | — | `https://github.com/libpag/vendor_tools.git` → `third_party/vendor_tools/` | CMake vendor build helpers | Always | -| `libpng` | 1.6.47 | TGFX `third_party/libpng/` | PNG decode/encode | `PAG_USE_PNG_*` (via TGFX) | -| `libjpeg-turbo` | 2.1.1 | TGFX `third_party/libjpeg-turbo/` | JPEG decode/encode | `PAG_USE_JPEG_*` (via TGFX) | -| `libwebp` | 1.x | TGFX `third_party/libwebp/` | WebP decode/encode | `PAG_USE_WEBP_*` (via TGFX) | -| `freetype` | 2.13.3 | TGFX `third_party/freetype/` | Vector font rasterization | `PAG_USE_FREETYPE` (via TGFX) | -| `pathkit` | — | TGFX `third_party/pathkit/` | 2D path operations (Skia-derived) | Always (TGFX) | -| `skcms` | — | TGFX `third_party/skcms/` | ICC color profile handling | Always (TGFX) | -| `highway` | — | TGFX `third_party/highway/` | SIMD acceleration | Always (TGFX) | -| `concurrentqueue` | — | TGFX `third_party/concurrentqueue/` | Lock-free task queuing | Always (TGFX) | -| `flatbuffers` | — | TGFX `third_party/flatbuffers/` | Binary serialization in layers module | `TGFX_BUILD_LAYERS` | -| `shaderc` | — | TGFX `third_party/shaderc/` | GLSL → SPIR-V compilation for Metal | TGFX Metal backend | -| `SPIRV-Cross` | — | TGFX `third_party/SPIRV-Cross/` | SPIR-V → MSL for Metal | TGFX Metal backend | -| `zlib` | — | TGFX `third_party/zlib/` | Compression support (shaderc dep) | TGFX | -| `nlohmann/json` | — | TGFX `third_party/json/` | JSON in TGFX test utilities | TGFX tests | -| `googletest` | 1.16.0 | TGFX `third_party/googletest/` | C++ unit test framework | `PAG_BUILD_TESTS` | - -## APIs & External Services - -**External network services:** -- None. libpag does not make HTTP requests, open sockets, or contact any remote service at runtime. `libxml2` is configured with `LIBXML2_WITH_HTTP=OFF` explicitly. - -## Data Storage - -**Databases:** -- None. - -**File Storage:** -- Local filesystem only. The library reads `.pag` binary files and `.pagx` XML files from paths supplied by the caller. -- Disk cache for decoded sequence frames — `PAGDiskCache` API, implemented via `SequenceFile` (`src/rendering/caches/SequenceFile.cpp`), compressed with LZ4. -- Test resources: `resources/` directory (`test/src/` references via `TestConstants::PAG_ROOT`). -- Test output: `test/out/{folder}/{name}.webp` vs baseline `{name}_base.webp`, version-tracked via `test/baseline/version.json` (repo) and `test/baseline/.cache/version.json` (local). - -**Caching:** -- In-memory render caches: `RenderCache`, `RasterCache`, `ShapeCache`, `TextCache` in `src/rendering/caches/`. -- Disk cache wraps the `SequenceFile` LZ4 format. - -## Authentication & Identity - -- None. libpag has no user concept, no auth provider, no credentials. -- `android/libpag/build.gradle` references JReleaser/Sonatype credentials for **publishing** only, not runtime use. - -## Monitoring & Observability - -**Error Tracking:** -- None (no Sentry / Crashlytics / Bugsnag). - -**Logs:** -- Android: `#include ` via linked `log` library (`CMakeLists.txt:407-408`) -- OpenHarmony: `hilog_ndk.z` library (`CMakeLists.txt:485`) -- Trace image debug output: `src/platform/android/JTraceImage.cpp` (optional debug feature) -- Otherwise errors are returned as `DecodingResult` error codes or `nullptr` from factory methods - -## CI/CD & Deployment - -**Hosting:** -- Android SDK → Maven Central via JReleaser (`com.tencent.tav:libpag`, `android/libpag/build.gradle:145-161`) -- Web SDK → NPM (`libpag@4.0.0`, `web/package.json`) -- CLI → NPM (`@libpag/pagx@0.2.16`, `cli/npm/package.json`) -- iOS/macOS → distributed as prebuilt frameworks (no CocoaPods/SPM in this tree) -- OHOS → HAR package (`@tencent/libpag@1.0.1`) - -**CI Pipeline:** -- Build scripts: `autotest.sh`, `build_pag`, `build_vendor`, `codecov.sh` at repo root. -- No `.github/`, `.gitlab-ci.yml`, or `Jenkinsfile` observed. - -## Environment Configuration - -**Runtime env vars:** -- None required by the library itself at runtime. -- `$ENV{CLION_IDE}` inspected at CMake configure time only (`CMakeLists.txt:77-81`). - -**Secrets location:** -- Android publishing: JReleaser GPG keys expected via standard JReleaser env vars (not stored in repo) -- OHOS signing: `ohos/local.signingconfig.sample.json` — sample only, real file gitignored - -## Webhooks & Callbacks - -**Incoming:** None. - -**Outgoing:** None at the network level. - -**In-process callbacks:** -- `PAGAnimator::Listener` — animation lifecycle events -- `PAGPlayer` / `PAGSurface` callbacks delivered to host app via platform bindings (iOS `PAGAnimationCallback.mm`, Android `JPAGAnimator.cpp`) - ---- - -## Platform Integrations (the primary integration surface) - -### iOS - -**System frameworks linked** (`CMakeLists.txt:323-343`): -- `UIKit`, `Foundation`, `QuartzCore`, `CoreGraphics` — UI + 2D primitives -- `CoreText` — font loading / text shaping on Apple -- `VideoToolbox`, `CoreMedia`, `CoreVideo` — hardware H.264 decode -- `ImageIO` — image encode/decode -- `OpenGLES` (when `PAG_USE_OPENGL=ON`) **or** `Metal` (when `PAG_USE_OPENGL=OFF`) -- `iconv` — character encoding conversion - -**GPU context:** -- OpenGL ES via EAGLWindow / EAGLDevice used in `src/platform/ios/private/GPUDrawable.h` -- Metal is the TGFX Metal backend when OpenGL is disabled -- `CAEAGLLayer`-backed `PAGView` (`src/platform/ios/PAGView.mm`) - -**Hardware video decoding** (`src/platform/ios/private/HardwareDecoder.h`): -- `VTDecompressionSessionRef` from `` -- Uses `CMSampleBufferRef` for input bitstream, yields `CVPixelBufferRef` for GPU upload - -**Font loading:** -- Native shaper uses `` via `src/platform/cocoa/private/NativeTextShaper.mm` (shared between iOS and macOS) - -**Display sync:** -- `CADisplayLink` wrapped in `src/platform/ios/private/NativeDisplayLink.mm` - -**Public ObjC headers:** -- Umbrella: `src/platform/ios/libpag.h` -- Module map: `ios/libpag.modulemap` -- Bundle identifier: `com.tencent.libpag` - -### macOS - -**System frameworks linked** (`CMakeLists.txt:367-393`): -- `ApplicationServices`, `QuartzCore`, `Cocoa`, `Foundation` -- `VideoToolbox`, `CoreMedia` — hardware H.264 decode -- `OpenGL` (when `PAG_USE_OPENGL=ON`) **or** `Metal` -- `iconv` - -**GPU context:** -- Desktop OpenGL via `NSOpenGLView`-backed `PAGView` (`src/platform/mac/PAGView.m`) - -**Hardware video decoding:** -- Same VideoToolbox pipeline as iOS; separate impl at `src/platform/mac/private/HardwareDecoder.h/.mm` - -**Font loading:** Shared CoreText path via `src/platform/cocoa/private/NativeTextShaper.mm`. - -**Module map:** `mac/libpag.modulemap`. - -### Android - -**System libraries linked** (`CMakeLists.txt:406-415, 429-434`): -- `log` — `` logging -- `android` — `ANativeWindow` and core NDK -- `jnigraphics` — `AndroidBitmap_*` pixel access -- `mediandk` — ``, `` hardware video decoder -- `GLESv2`, `GLESv3`, `EGL` — OpenGL ES context - -**GPU context:** -- EGL surface via `ANativeWindow` — `src/platform/android/GPUDrawable.cpp` wraps `tgfx::EGLWindow` -- Delivered to Java as `android.view.Surface` through `JPAGSurface.cpp` - -**Hardware video decoding** (`src/platform/android/HardwareDecoder.h`): -- `AMediaCodec` + `AMediaCodecBufferInfo` from `` -- Uses `ANativeWindow` from `` to feed a `SurfaceTexture` -- Fallback software decoder: ffavc (prebuilt `libffavc.so` in `android/libpag/libs/${ANDROID_ABI}/`) - -**Font loading:** `src/platform/android/FontConfigAndroid.cpp` — walks Android system font config XML, registers fallback fonts with TGFX. - -**Display sync:** `src/platform/android/NativeDisplayLink.cpp` — wraps `android.view.Choreographer` via JNI. - -**JNI bridge files:** All `J*.cpp` in `src/platform/android/` — `JPAG.cpp`, `JPAGPlayer.cpp`, `JPAGFile.cpp`, `JPAGImage.cpp`, `JPAGImageLayer.cpp`, `JPAGShapeLayer.cpp`, `JPAGTextLayer.cpp`, `JPAGSolidLayer.cpp`, `JPAGAnimator.cpp`, `JPAGDecoder.cpp`, `JPAGDiskCache.cpp`, `JPAGFont.cpp`, `JPAGImageView.cpp`, `JPAGSurface.cpp`, `JPAGComposition.cpp`, `JPAGLayer.cpp`, `JVideoDecoder.cpp`, `JVideoSurface.cpp`, `JNativeTask.cpp`, `JTraceImage.cpp`. Helpers: `JNIHelper.h/.cpp`. - -**Link-time optimization:** -- Symbol export script `android/libpag/export.def` + `-Wl,--gc-sections --version-script` (`CMakeLists.txt:425`) -- `-fno-exceptions -fno-rtti -Os` for minimum binary size - -**Android artifact:** `com.tencent.tav:libpag:4.1.0` (AAR), min SDK 21, target SDK 33, NDK 28.0.13004108. Depends on `androidx.exifinterface:exifinterface:1.3.3`. - -### OpenHarmony (OHOS) - -**System libraries linked** (`CMakeLists.txt:474-500`): -- GPU: `GLESv3`, `EGL` -- Windowing / graphics: `native_buffer`, `native_window`, `native_image`, `native_display_soloist` -- Pixel buffers: `pixelmap_ndk.z`, `image_source_ndk.z`, `pixelmap`, `image_source` -- NAPI runtime: `ace_ndk.z`, `ace_napi.z` -- Logging: `hilog_ndk.z` -- Raw files: `rawfile.z` -- Hardware video decode: `native_media_codecbase`, `native_media_core`, `native_media_vdec` - -**GPU context:** EGL via `tgfx::EGLWindow` (`src/platform/ohos/GPUDrawable.h`). - -**Hardware video decoding:** `OHOSVideoDecoder.cpp` uses `native_media_vdec` / `native_media_codecbase`. Software fallback: `OHOSSoftwareDecoderWrapper.cpp`. - -**XComponent integration:** `XComponentHandler.cpp` — bridges surface lifecycle from OHOS XComponent. - -**NAPI bridge:** All `JPAG*.cpp` in `src/platform/ohos/` mirror the Android JNI layer via NAPI. Helper: `JsHelper.h/.cpp`. - -**Link-time symbol export:** `ohos/libpag/export.def` + `--gc-sections --version-script` (`CMakeLists.txt:503`). - -**Package:** `@tencent/libpag@1.0.1` HAR. - -### Web (Emscripten / WASM) - -**Runtime environment** (`CMakeLists.txt:639-660`): -- Emscripten-compiled WASM + JS glue -- WebGL 2 (`-sMAX_WEBGL_VERSION=2`) -- ES6 module export with name `PAGInit` (`-sEXPORT_NAME='PAGInit' -sEXPORT_ES6=1 -sMODULARIZE=1`) -- Environments: `web,worker` -- Memory: `-sALLOW_MEMORY_GROWTH=1`; multithreaded build adds `-sUSE_PTHREADS=1 -sINITIAL_MEMORY=32MB -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency` -- `-fno-rtti` (RTTI disabled on web) - -**GPU context:** Canvas-bound WebGL 2. `src/platform/web/GPUDrawable.cpp` calls `emscripten_get_canvas_element_size()` targeting a named canvas element. - -**Hardware video decoding:** Delegated to JavaScript (`src/platform/web/HardwareDecoder.cpp`) via `WebVideoSequenceDemuxer`. `WebSoftwareDecoderFactory` provides JS-implemented software decoders. - -**Text shaping:** `src/platform/web/NativeTextShaper.cpp` delegates to JS callbacks (Canvas/DOM font metrics). Unicode emoji table: `src/platform/web/UnicodeEmojiTable.hh`. - -**JS binding layer:** `src/platform/web/PAGWasmBindings.cpp` uses `emscripten::bind` and `emscripten::val` to expose C++ classes as JS objects (`PAGFile`, `PAGPlayer`, `PAGSurface`, `PAGLayer` hierarchy, etc.). - -**Build tooling:** Rollup + TypeScript in `web/src/`. Variants: `wasm` (ST), `wasm-mt` (MT), `wechat` (WeChat Mini Program with Brotli WASM). - -**Blocked Emscripten version:** `4.0.11` (`CMakeLists.txt:643-648`) — build fatals if detected. - -### Windows - -**System libraries linked** (`CMakeLists.txt:438-459`): -- `opengl32` (native GL) **or** ANGLE static libs from `${TGFX_DIR}/vendor/angle/` when `PAG_USE_ANGLE=ON` -- `Bcrypt` — cryptographic primitives (required transitively) -- `ws2_32` — Winsock (required transitively) - -**GPU context:** WGL on Win32 HWND, or ANGLE-backed GLES3 on D3D11. `src/platform/win/GPUDrawable.cpp`. - -**Hardware video decoding:** None in `src/platform/win/`. Falls back to libavc software decoder. - -**Font loading:** TGFX/FreeType. - -**Preprocessor:** `NOMINMAX`, `_USE_MATH_DEFINES`, 64-bit forced. - -**Demo project:** `win/Win32Demo.sln`. - -### Linux - -**System libraries linked** (`CMakeLists.txt:460-473`): -- `pthread` / `Threads::Threads`, `dl` -- `GLESv2`, `EGL` -- `Fontconfig` (CLI only, `CMakeLists.txt:771-773`) via `find_package(Fontconfig REQUIRED)` - -**GPU context:** EGL + GLESv2 (`src/platform/linux/GPUDrawable.cpp`). - -**Hardware video decoding:** None native; libavc software decoder only. - -**Font loading:** System fontconfig (CLI); TGFX/FreeType otherwise. - -**Compile flags:** `-fPIC -pthread`. - -### Qt - -**Frameworks linked** (`CMakeLists.txt:242-245`): -- `Qt6::Widgets`, `Qt6::OpenGL`, `Qt6::Quick` - -**GPU context:** `QGLWindow` via `src/platform/qt/GPUDrawable.cpp`. - -**Notes:** When Qt is enabled, SwiftShader and ANGLE are force-disabled; OpenGL is force-enabled. On macOS, `HardwareDecoder.mm` is still included for VideoToolbox decoding. - -### SwiftShader (optional CPU backend) - -**Libraries:** Shared libs globbed from `${TGFX_DIR}/vendor/swiftshader/${PLATFORM}/${ARCH}/*` (`CMakeLists.txt:253-258`). Used for headless/CI rendering. - ---- - -## Hardware Video Decoding Summary - -| Platform | Hardware API | Fallback | -|----------|-------------|----------| -| iOS | `VTDecompressionSession` (VideoToolbox) | none | -| macOS | `VTDecompressionSession` (VideoToolbox) | libavc | -| Android | `AMediaCodec` (NdkMediaCodec) | ffavc (`libffavc.so`) | -| OpenHarmony | `native_media_vdec` | OHOS software decoder wrapper | -| Web | JS-side decoders via `WebVideoSequenceDemuxer` | JS software decoder factory | -| Windows | none native | libavc | -| Linux | none native | libavc | - -Software fallback chain controlled at configure time via `PAG_USE_LIBAVC` and `PAG_USE_FFAVC`. The `VideoDecoder` abstract base lives at `src/rendering/video/VideoDecoder.h`. - -## Font Loading Summary - -| Platform | Font source | -|----------|-------------| -| iOS / macOS | CoreText (`src/platform/cocoa/private/NativeTextShaper.mm`) | -| Android | Platform font config XML (`src/platform/android/FontConfigAndroid.cpp`) | -| Linux (CLI) | fontconfig | -| OHOS | TGFX default (FreeType) | -| Web | JS-side text shaping (`src/platform/web/NativeTextShaper.cpp`) | -| Windows | TGFX default (FreeType) | - -## Format Support - -| Format | Codec / Parser | Direction | -|--------|----------------|-----------| -| `.pag` | Tag-based binary codec (`src/codec/`) — 100+ tag types, LZ4-compressed blocks, versioned tags | Read + Write | -| `.pagx` | XML via Expat (`src/pagx/xml/`) + HarfBuzz + SheenBidi for text | Read + Write (`PAGXImporter` / `PAGXExporter`) | -| `.svg` | `SVGImporter.cpp` / `SVGExporter.cpp` (`src/pagx/svg/`), path parsing via `SVGPathParser.cpp` | Read + Write (`PAG_BUILD_SVG`) | -| `.webp` | TGFX/libwebp (baseline screenshots, sequence frames) | Read + Write | -| `.png` | TGFX/libpng | Read + Write | -| `.jpg` | TGFX/libjpeg-turbo | Read + Write | -| `.mp4` / H.264 | Hardware decoder per platform + libavc/ffavc fallback | Read (decode only) | - ---- - -*Integration audit: 2026-04-28* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md deleted file mode 100644 index 404a9d62f3..0000000000 --- a/.planning/codebase/STACK.md +++ /dev/null @@ -1,172 +0,0 @@ -# Technology Stack - -**Analysis Date:** 2026-04-28 - -## Languages - -**Primary:** -- C++17 - Core rendering engine, codec, data model (all of `src/base/`, `src/codec/`, `src/rendering/`, `src/pagx/`, `src/renderer/`) -- Objective-C / Objective-C++ - Apple platform integration in `src/platform/cocoa/`, `src/platform/ios/`, `src/platform/mac/` (`.m` / `.mm` files) - -**Secondary:** -- Java / Kotlin - Android SDK layer in `android/libpag/src/main/` (bridged via JNI from `src/platform/android/J*.cpp`) -- TypeScript / JavaScript - Web/WASM SDK in `web/src/`, build tooling in `web/script/` -- ArkTS (ETS) - OpenHarmony SDK in `ohos/libpag/src/main/` (bridged via NAPI from `src/platform/ohos/`) -- CMake - Build configuration (`CMakeLists.txt`, `third_party/vendor_tools/vendor.cmake`) -- Shell - Build/tooling scripts (`sync_deps.sh`, `install_tools.sh`, `codeformat.sh`, `accept_baseline.sh`) - -## Runtime - -**Language standard:** -- `CMAKE_CXX_STANDARD 17` (required) — see `CMakeLists.txt:21-22` -- `CMAKE_CXX_VISIBILITY_PRESET hidden` (symbols hidden by default) - -**Deployment targets** (`CMakeLists.txt:141-153`): -- macOS arm64: `11.0` minimum -- macOS x86_64: `10.15` minimum -- iOS: `9.0` minimum -- Android: `minSdkVersion 21`, `targetSdkVersion 33`, NDK `28.0.13004108` (`android/build.gradle`) -- Web: Emscripten (version `4.0.11` explicitly blocked — `CMakeLists.txt:643-648`) - -**Build tooling required:** -- `node`, `cmake`, `ninja`, `yasm`, `git-lfs` — installed via Homebrew on macOS -- `emscripten` — required when building web target -- `depsync` — npm package for syncing `DEPS` dependencies - -## Frameworks - -**Core build system:** -- CMake 3.13+ with Ninja generator (primary) -- Gradle 8.8.1 + Android Gradle Plugin `8.8.1` (Android wrapper build, `android/build.gradle`) -- Xcode (iOS/macOS framework builds via `mac/gen_mac`, `ios/gen_ios`, `ios/gen_simulator`) -- Hvigor (OpenHarmony build system, `ohos/hvigorfile.ts`) -- Rollup + TypeScript + Babel (web/WASM bundling, `web/package.json`) - -**Testing:** -- Google Test `1.16.0` — pulled from `${TGFX_DIR}/third_party/googletest/`. Test targets: `PAGFullTest`, `PAGUnitTest`, `UpdateBaseline` (`CMakeLists.txt:799-855`). -- Cypress 9.5 — web end-to-end tests (`web/cypress/`) - -**Render backends** (selected at configure time, `CMakeLists.txt:27-31`): -- OpenGL / OpenGL ES (default, `PAG_USE_OPENGL=ON`) -- Metal (Apple, when `PAG_USE_OPENGL=OFF`) -- ANGLE (`PAG_USE_ANGLE=ON`, Windows only, headers from `${TGFX_DIR}/vendor/angle/`) -- SwiftShader (`PAG_USE_SWIFTSHADER=ON`, CPU rendering, libs from `${TGFX_DIR}/vendor/swiftshader/`) -- WebGL 2 (web target via Emscripten `-sMAX_WEBGL_VERSION=2`) -- Qt OpenGL (`PAG_USE_QT=ON`, requires Qt6 Core/Widgets/OpenGL/Quick) - -## Key Dependencies - -**Rendering engine (required):** -- `tgfx` 2.1.1 (Tencent 2D graphics engine, `https://github.com/libpag/tgfx.git`) — pinned commit in `DEPS`, synced to `third_party/tgfx/`. Can be linked three ways (`CMakeLists.txt:525-613`): - 1. Prebuilt external: via `-DTGFX_LIB=... -DTGFX_INCLUDE=...` - 2. Custom source dir: via `-DTGFX_DIR=../tgfx` (used for local TGFX debugging) - 3. Built-in: uses `third_party/tgfx/out/cache` if present, else `add_subdirectory` - -**Binary codec:** -- `lz4` 1.10.0 (`https://github.com/lz4/lz4.git`) — embedded build compiles `third_party/lz4/lib/lz4.c` (`CMakeLists.txt:512-516`). On Apple platforms with `PAG_USE_SYSTEM_LZ4=ON`, links system `compression` framework instead. Used for sequence frame compression in `src/rendering/caches/`. -- `libavc` (H.264 software decoder, `https://github.com/libpag/libavc.git`) — fallback enabled via `PAG_USE_LIBAVC` on non-Android/non-iOS/non-OHOS platforms. Headers: `third_party/libavc/common`, `third_party/libavc/decoder`. -- `ffavc` (prebuilt binary in `vendor/ffavc/` and `android/libpag/libs/${ANDROID_ABI}/libffavc.so`) — alternate H.264 fallback on Android (`PAG_USE_FFAVC=ON`). - -**Text & internationalization** (only when `PAG_BUILD_PAGX=ON` or `PAG_BUILD_TESTS=ON`): -- `harfbuzz` 10.1.0 (`https://github.com/harfbuzz/harfbuzz.git`) — text shaping. Used by `src/renderer/` text layout engine. -- `expat` 2.6.3 (`https://github.com/libexpat/libexpat.git`) — XML parsing for PAGX format. Built static. -- `SheenBidi` 3.0.0 (`https://github.com/Tehreer/SheenBidi.git`) — Unicode bidirectional text (BiDi) algorithm. -- `libxml2` 2.15.2 (`https://github.com/GNOME/libxml2.git`) — XPath + advanced XML (used by CLI/tests only). Configured with all optional features OFF. Platforms: mac, win, linux. - -**Optional:** -- `rttr` (`https://github.com/rttrorg/rttr.git`) — runtime type reflection, enabled only when `PAG_USE_RTTR=ON`. Platforms: mac, win, linux. -- `vendor_tools` (`https://github.com/libpag/vendor_tools.git`) — CMake helpers for third-party targets (`third_party/vendor_tools/vendor.cmake`). - -**Image codecs** (provided by TGFX, controlled via `PAG_USE_PNG_DECODE/ENCODE`, `PAG_USE_JPEG_DECODE/ENCODE`, `PAG_USE_WEBP_DECODE/ENCODE`): -- `libpng` 1.6.47 — PNG encode/decode (TGFX `third_party/libpng/`) -- `libjpeg-turbo` 2.1.1 — JPEG encode/decode (TGFX `third_party/libjpeg-turbo/`) -- `libwebp` 1.x — WebP encode/decode (TGFX `third_party/libwebp/`) -- `freetype` 2.13.3 — Vector font rendering (TGFX `third_party/freetype/`, `PAG_USE_FREETYPE`) - -**Graphics pipeline (TGFX internal):** -- `pathkit` — Skia-derived 2D path library (`third_party/tgfx/third_party/pathkit/`) -- `skcms` — ICC color profile processing (`third_party/tgfx/third_party/skcms/`) -- `highway` — SIMD acceleration (`third_party/tgfx/third_party/highway/`) -- `concurrentqueue` — lock-free task queue (`third_party/tgfx/third_party/concurrentqueue/`) -- `flatbuffers` — binary serialization in TGFX layers module (`third_party/tgfx/third_party/flatbuffers/`) -- `shaderc` + `SPIRV-Cross` — GLSL → MSL shader compilation for Metal backend (`third_party/tgfx/third_party/shaderc/`, `third_party/tgfx/third_party/SPIRV-Cross/`) -- `zlib` — required by TGFX/shaderc on some paths (`third_party/tgfx/third_party/zlib/`) -- `nlohmann/json` — test utilities in TGFX (`third_party/tgfx/third_party/json/`) - -## Configuration - -**CMake options** (defined in `CMakeLists.txt:27-67`): - -| Option | Default | Description | -|--------|---------|-------------| -| `PAG_USE_OPENGL` | ON | OpenGL GPU backend | -| `PAG_USE_SWIFTSHADER` | OFF | CPU rendering via SwiftShader | -| `PAG_USE_ANGLE` | OFF | Windows D3D-backed OpenGL ES via ANGLE | -| `PAG_USE_QT` | OFF | Qt6 integration (disables Metal/SwiftShader/ANGLE) | -| `PAG_USE_RTTR` | OFF | Runtime type reflection | -| `PAG_USE_HARFBUZZ` | OFF | Text shaping (auto-ON with PAGX) | -| `PAG_USE_C` | OFF | C language API bindings (`src/c/`) | -| `PAG_USE_FREETYPE` | ON non-Apple/non-Web, OFF Apple/Web | Embedded FreeType | -| `PAG_USE_LIBAVC` | ON non-Apple-sim/non-OHOS/non-Web | libavc fallback decoder | -| `PAG_USE_FFAVC` | ON Android | ffavc prebuilt decoder | -| `PAG_USE_THREADS` | ON non-Web or EMSCRIPTEN_PTHREADS | Multithreaded rendering | -| `PAG_USE_SYSTEM_LZ4` | ON Apple | Link Apple's `compression.framework` for LZ4 | -| `PAG_BUILD_PAGX` | OFF | PAGX XML format support (auto-ON by TESTS/CLI/SVG) | -| `PAG_BUILD_SVG` | OFF | SVG import/export (auto-ON by CLI/TESTS) | -| `PAG_BUILD_CLI` | OFF | `pagx-cli` command-line tool | -| `PAG_BUILD_TESTS` | OFF | Enable all modules and GoogleTest targets | -| `PAG_BUILD_SHARED` | ON non-Web | Shared vs static library | -| `PAG_BUILD_FRAMEWORK` | ON Apple | Build Apple framework bundle | - -**Dependency option chains** (`CMakeLists.txt:95-125`): -- `PAG_BUILD_CLI` → forces `PAG_BUILD_PAGX` + `PAG_BUILD_SVG` -- `PAG_BUILD_SVG` → forces `PAG_BUILD_PAGX` -- `PAG_BUILD_PAGX` → forces `PAG_USE_HARFBUZZ` -- `PAG_BUILD_TESTS` → forces `PAG_USE_FREETYPE` + `PAG_BUILD_PAGX` + `PAG_BUILD_CLI` + `PAG_BUILD_SVG` + `PAG_USE_HARFBUZZ` + `PAG_USE_SYSTEM_LZ4=OFF` + `PAG_BUILD_SHARED=OFF` -- `PAG_USE_QT|SWIFTSHADER|ANGLE` → forces `PAG_USE_OPENGL=ON` -- `PAG_USE_FFAVC` → overrides `PAG_USE_LIBAVC=OFF` - -**Compiler flags** (`CMakeLists.txt:209-216`): -- Clang: `-Werror -Wall -Wextra -Weffc++ -pedantic -Werror=return-type -Wno-unused-command-line-argument` -- MSVC: `/utf-8 /w44251 /w44275` -- Android release: `-ffunction-sections -fdata-sections -Os -fno-exceptions -fno-rtti` (`CMakeLists.txt:425-426`) -- Web: `-fno-rtti -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0` plus `-Oz` (release) or `-O0 -gsource-map` (debug) - -**Key build defines added by options:** -- `PAG_DLL` when `PAG_BUILD_SHARED` -- `PAG_USE_LIBAVC`, `PAG_USE_FFAVC`, `PAG_USE_RTTR`, `PAG_USE_HARFBUZZ` -- `PAG_BUILD_PAGX`, `PAG_BUILD_SVG` -- `PAG_BUILD_FOR_WEB` on Web -- `LIBXML_STATIC` (tests/CLI), `XML_STATIC` (Windows + PAGX) -- `GL_SILENCE_DEPRECATION` / `GLES_SILENCE_DEPRECATION` on Apple -- `NOMINMAX`, `_USE_MATH_DEFINES` on Windows -- `SKIP_FRAME_COMPARE` in `PAGUnitTest`, `UPDATE_BASELINE`/`GENERATE_BASELINE_IMAGES` in `UpdateBaseline` - -## Platform Requirements - -**Development (macOS, primary dev platform):** -- Xcode command-line tools -- Homebrew-installed: node, cmake, ninja, yasm, git-lfs, emscripten (for web) -- `./sync_deps.sh` must be run before first build — invokes `depsync` which clones repos listed in `DEPS` into `third_party/` -- CLion-friendly: when `$ENV{CLION_IDE}` is set on macOS, `PAG_BUILD_TESTS=ON` by default (`CMakeLists.txt:77-86`) - -**Production build outputs:** -- **iOS/macOS:** Framework bundle (`PAG_BUILD_FRAMEWORK=ON`), bundle identifier `com.tencent.libpag`, version `4.0.0` (`src/rendering/PAG.cpp`) -- **Android:** AAR `com.tencent.tav:libpag:4.1.0`, min SDK 21, target SDK 33, NDK 28.0.13004108 -- **Web:** NPM package `libpag@4.0.0` (`web/package.json`) — ESM/CJS/UMD; targets `wasm` (ST) and `wasm-mt` (MT); WeChat mini-program variant under `web/wechat/` -- **OpenHarmony:** HAR package `@tencent/libpag@1.0.1` -- **CLI:** `pagx-cli` executable, npm `@libpag/pagx@0.2.16` (`cli/npm/package.json`) -- **Windows:** Win32 demo only (`win/Win32Demo.sln`), no packaged SDK -- **Linux:** CLI executable + demo (`linux/CMakeLists.txt`) - -**Versions:** -- C++ SDK: `4.0.0` (`src/rendering/PAG.cpp`) -- Android SDK: `4.1.0` (`android/libpag/build.gradle`) -- Web package: `4.0.0` (`web/package.json`) -- TGFX engine: `2.1.1` (`third_party/tgfx/CMakeLists.txt`) -- PAGX CLI npm: `0.2.16` (`cli/npm/package.json`) -- DEPS manifest: `1.3.12` (`DEPS`) - ---- - -*Stack analysis: 2026-04-28* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md deleted file mode 100644 index d9373333b7..0000000000 --- a/.planning/codebase/STRUCTURE.md +++ /dev/null @@ -1,353 +0,0 @@ -# Codebase Structure - -**Analysis Date:** 2025-07-15 - -## Directory Layout - -``` -libpag/ -├── include/ # Public headers (installed with the library) -│ ├── pag/ # Core C++ API -│ │ ├── pag.h # Master include: PAGPlayer, PAGFile, PAGSurface, PAGImage… -│ │ ├── file.h # PAGFile, PAGComposition, PAGLayer types -│ │ ├── decoder.h # PAGDecoder (frame-by-frame decode to pixels) -│ │ ├── gpu.h # GPU backend types (BackendTexture, BackendSemaphore) -│ │ ├── types.h # Shared value types (Color, Point, Rect, Matrix, etc.) -│ │ ├── defines.h # PAG_API export macros -│ │ └── c/ # C language bindings (pag_player.h, pag_file.h, …) -│ └── pagx/ # PAGX XML format API (~100 headers) -│ ├── PAGXDocument.h # Top-level XML document model -│ ├── PAGXImporter.h # PAGXImporter: XML → PAGFile -│ ├── PAGXExporter.h # PAGXExporter: PAGFile → XML -│ ├── SVGImporter.h # SVGImporter -│ ├── SVGExporter.h # SVGExporter -│ ├── FontConfig.h # Font embedding/lookup config -│ ├── nodes/ # Scene graph node types (Layer, Group, Text, Shape…) -│ └── types/ # PAGX enum/value types (BlendMode, Alignment, etc.) -│ -├── src/ # Implementation sources -│ ├── base/ # PAG data model (no rendering logic) -│ │ ├── Composition.cpp # Base Composition + Vector/Bitmap/VideoComposition -│ │ ├── Layer.cpp # Base Layer + Image/Shape/Solid/Text/PreCompose/Camera -│ │ ├── File.cpp # File-level metadata -│ │ ├── shapes/ # Shape primitives (Rectangle, Ellipse, ShapePath, Fill…) -│ │ ├── effects/ # Effect data (blur, drop shadow, glow, etc.) -│ │ ├── keyframes/ # Keyframe interpolation types -│ │ ├── layerStyles/ # Layer style data (stroke, outer glow, etc.) -│ │ ├── text/ # Text document, animator, selector data -│ │ └── utils/ # TGFXCast, TimeUtil, math helpers -│ │ -│ ├── codec/ # Binary PAG format encode/decode -│ │ ├── Codec.cpp # Entry point: Codec::Decode / Codec::Encode -│ │ ├── CodecContext.cpp # Decode/encode state context -│ │ ├── AttributeHelper.cpp # Typed attribute read/write helpers -│ │ ├── DataTypes.cpp # Primitive type serialization -│ │ ├── tags/ # Per-feature tag handlers -│ │ │ ├── effects/ # Effect tag readers/writers -│ │ │ ├── shapes/ # Shape tag readers/writers -│ │ │ ├── layerStyles/ # Layer style tag readers/writers -│ │ │ └── text/ # Text tag readers/writers -│ │ ├── mp4/ # MP4/H.264 muxing for video sequences -│ │ └── utils/ # Codec utility helpers -│ │ -│ ├── rendering/ # Real-time rendering pipeline -│ │ ├── PAGPlayer.cpp # PAGPlayer: playback control, owns PAGStage + RenderCache -│ │ ├── PAGSurface.cpp # PAGSurface: rendering target, wraps Drawable -│ │ ├── PAGSurfaceFactory.cpp # Platform-agnostic surface creation helpers -│ │ ├── PAGAnimator.cpp # Animation frame timing -│ │ ├── PAGDecoder.cpp # Frame-by-frame offline decoder -│ │ ├── PAGFont.cpp # Font registration/lookup -│ │ ├── PAG.cpp # Global PAG init / version info -│ │ ├── FontManager.cpp # System/embedded font manager -│ │ ├── Performance.cpp # Render performance metrics -│ │ ├── FileReporter.cpp # Telemetry/error reporting -│ │ ├── layers/ # PAGLayer runtime wrappers -│ │ │ ├── PAGStage.cpp # Root scene graph node; owns RenderCache attachment -│ │ │ ├── PAGLayer.cpp # Base PAGLayer (wraps data-model Layer) -│ │ │ ├── PAGComposition.cpp # PAGComposition (wraps Composition) -│ │ │ ├── PAGFile.cpp # PAGFile (top-level file runtime wrapper) -│ │ │ ├── PAGImageLayer.cpp -│ │ │ ├── PAGShapeLayer.cpp -│ │ │ ├── PAGSolidLayer.cpp -│ │ │ ├── PAGTextLayer.cpp -│ │ │ └── ContentVersion.cpp # Dirty-version counter for cache invalidation -│ │ ├── caches/ # Frame and content caches -│ │ │ ├── RenderCache.cpp # Master cache; manages GPU resource lifecycle -│ │ │ ├── LayerCache.cpp # Per-layer snapshot cache -│ │ │ ├── ShapeContentCache.cpp -│ │ │ ├── TextContentCache.cpp -│ │ │ ├── ImageContentCache.cpp -│ │ │ ├── CompositionCache.cpp -│ │ │ ├── DiskCache.cpp # Disk-backed cache -│ │ │ ├── DiskIOWorker.cpp # Background I/O thread -│ │ │ ├── SequenceFile.cpp # Video/bitmap sequence disk cache -│ │ │ ├── TextBlock.cpp # Shaped text block cache -│ │ │ └── GraphicContent.cpp -│ │ ├── renderers/ # Per-content-type rendering logic -│ │ │ ├── LayerRenderer.cpp # Dispatch + transform/effect chain -│ │ │ ├── ShapeRenderer.cpp -│ │ │ ├── TextRenderer.cpp -│ │ │ ├── FilterRenderer.cpp # Effect/filter application -│ │ │ ├── CompositionRenderer.cpp -│ │ │ ├── MaskRenderer.cpp -│ │ │ ├── TrackMatteRenderer.cpp -│ │ │ ├── TransformRenderer.cpp -│ │ │ ├── TextAnimatorRenderer.cpp -│ │ │ └── TextSelectorRenderer.cpp -│ │ ├── filters/ # GPU filter implementations (32+ types) -│ │ │ ├── gaussianblur/ # Gaussian blur passes -│ │ │ ├── glow/ # Glow/outer glow filters -│ │ │ ├── layerstyle/ # Drop shadow, stroke, etc. -│ │ │ ├── utils/ # Filter utilities -│ │ │ ├── LayerStylesFilter.cpp -│ │ │ ├── MotionBlurFilter.cpp -│ │ │ ├── DisplacementMapFilter.cpp -│ │ │ └── … (20+ more filter files) -│ │ ├── graphics/ # Low-level graphic primitives -│ │ │ ├── Recorder.h # Draw command recorder -│ │ │ ├── Picture.h # Raster/GPU picture wrapper -│ │ │ ├── Shape.h # Vector path wrapper -│ │ │ ├── Snapshot.h # GPU texture snapshot -│ │ │ └── ImageProxy.h # Lazy image decode proxy -│ │ ├── sequences/ # Async sequence decode pipeline -│ │ │ ├── SequenceInfo.h # Sequence metadata -│ │ │ └── SequenceImageQueue.h # Async frame queue -│ │ ├── drawables/ # Platform Drawable implementations -│ │ ├── editing/ # Runtime layer editing helpers -│ │ ├── video/ # Video sequence decode coordination -│ │ └── utils/ # Render utilities (ScaleMode, LockGuard, shaper/) -│ │ -│ ├── renderer/ # Text layout engine (HarfBuzz/BiDi integration) -│ │ ├── TextShaper.cpp # HarfBuzz shaping -│ │ ├── LineBreaker.cpp # Unicode line breaking -│ │ ├── BidiResolver.cpp # Bidirectional text -│ │ ├── GlyphRunRenderer.cpp # Glyph run draw calls -│ │ ├── LayerBuilder.cpp # Text layer build from shaped glyphs -│ │ ├── FontEmbedder.cpp # Font data embedding -│ │ ├── ImageEmbedder.cpp # Inline image embedding -│ │ ├── PunctuationSquash.cpp # CJK punctuation compression -│ │ └── ToTGFX.cpp # Convert to TGFX draw calls -│ │ -│ ├── pagx/ # PAGX XML format support -│ │ ├── PAGXImporter.cpp # XML → PAGFile conversion (Expat parser) -│ │ ├── PAGXExporter.cpp # PAGFile → XML serialization -│ │ ├── PAGXDocument.cpp # PAGX document model -│ │ ├── FontConfig.cpp # Font config for PAGX -│ │ ├── SystemFonts.cpp # OS font enumeration (silent fail) -│ │ ├── TextLayout.cpp # PAGX text layout engine -│ │ ├── LayoutContext.cpp # Layout pass context -│ │ ├── LayoutNode.cpp # Node layout calculations -│ │ ├── PathData.cpp # Path data handling -│ │ ├── nodes/ # PAGX node implementations -│ │ ├── svg/ # SVG import/export -│ │ ├── utils/ # PAGX utility helpers -│ │ └── xml/ # Expat XML wrapper -│ │ -│ ├── cli/ # pagx-cli command-line tool -│ │ ├── main.cpp # CLI entry point, command dispatch -│ │ ├── CliUtils.cpp # Shared CLI helpers -│ │ ├── CommandBounds.cpp # `bounds` command -│ │ ├── CommandEmbed.cpp # `embed` command (embed fonts/images into PAGX) -│ │ ├── CommandExport.cpp # `export` command (PAGX → PAG) -│ │ ├── CommandFont.cpp # `font` command (list/verify fonts) -│ │ ├── CommandFormat.cpp # `format` command -│ │ ├── CommandImport.cpp # `import` command (PAG → PAGX) -│ │ ├── CommandLayout.cpp # `layout` command -│ │ ├── CommandRender.cpp # `render` command (render to image) -│ │ ├── CommandResolve.cpp # `resolve` command -│ │ ├── CommandVerify.cpp # `verify` command -│ │ ├── FormatUtils.cpp # Output formatting helpers -│ │ ├── LayoutUtils.cpp # Layout computation helpers -│ │ └── XPathQuery.cpp # XPath-like XML query helper -│ │ -│ ├── platform/ # Platform-specific implementations -│ │ ├── Platform.cpp / .h # Platform abstraction interface -│ │ ├── android/ # JNI bindings, hardware video, EGL context -│ │ ├── cocoa/ # Shared iOS+macOS: font loading, Metal context -│ │ │ └── private/ -│ │ ├── ios/ # iOS-specific: UIKit integration, VideoToolbox -│ │ │ └── private/ -│ │ ├── mac/ # macOS-specific: AppKit, CVDisplayLink -│ │ │ └── private/ -│ │ ├── win/ # Windows: DXGI/OpenGL context, DirectWrite fonts -│ │ ├── linux/ # Linux: EGL/GLX context, Fontconfig -│ │ ├── ohos/ # OpenHarmony -│ │ ├── web/ # Emscripten/WASM, WebGL -│ │ ├── qt/ # Qt platform abstraction -│ │ └── swiftshader/ # CPU rendering via SwiftShader -│ │ -│ └── c/ # C language API bindings -│ ├── pag_player.cpp -│ ├── pag_file.cpp -│ ├── pag_surface.cpp -│ ├── pag_image.cpp -│ ├── pag_layer.cpp -│ ├── pag_composition.cpp -│ ├── pag_text_document.cpp -│ ├── pag_font.cpp -│ ├── pag_decoder.cpp -│ ├── pag_animator.cpp -│ ├── pag_types.cpp -│ └── ext/ # EGL extension bindings -│ -├── test/ -│ ├── src/ # Google Test cases -│ │ ├── PAGFileTest.cpp # Core file loading and rendering -│ │ ├── PAGPlayerTest.cpp # Player control and lifecycle -│ │ ├── PAGSurfaceTest.cpp # Surface and GPU backend -│ │ ├── PAGTextLayerTest.cpp # Text rendering -│ │ ├── PAGFilterTest.cpp # Filter effects -│ │ ├── PAGXTest.cpp # PAGX format round-trip -│ │ ├── PAGXCliTest.cpp # CLI command tests -│ │ ├── PAGXSVGTest.cpp # SVG import/export -│ │ ├── PAGFontTest.cpp # Font loading/embedding -│ │ ├── PAGImageLayerTest.cpp -│ │ ├── PAGShapeLayerTest.cpp -│ │ ├── PAGSequenceTest.cpp -│ │ ├── PAGBlendTest.cpp -│ │ ├── PAGDiskCacheTest.cpp -│ │ ├── AsyncDecodeTest.cpp -│ │ ├── base/ # Base/utility test helpers -│ │ └── utils/ # Test utility code -│ ├── baseline/ # Screenshot baseline version tracking -│ │ └── version.json # Baseline version registry (do not modify manually) -│ └── out/ # Generated test output screenshots (gitignored) -│ -├── resources/ # Test fixture files -│ ├── AE/ # After Effects exported PAG/PAGX files -│ ├── cli/ # CLI test inputs -│ ├── font/ # Test font files -│ ├── svg/ # SVG test files -│ ├── text/ # Text test resources -│ └── … # Other fixture categories -│ -├── third_party/ # Vendored dependencies (not committed in full) -│ ├── tgfx/ # TGFX 2D GPU engine (core dependency) -│ ├── harfbuzz/ # Text shaping -│ ├── expat/ # XML parsing (used by PAGX) -│ ├── sheenbidi/ # Unicode BiDi algorithm -│ ├── lz4/ # LZ4 compression (codec layer) -│ └── libavc/ # H.264 software decoder -│ -├── android/ # Android Studio project / AAR build -├── ios/ # Xcode project / iOS framework build -├── mac/ # macOS framework build -├── win/ # Windows build helpers -├── linux/ # Linux build helpers -├── web/ # Web/WASM build -├── ohos/ # OpenHarmony build -├── viewer/ # Desktop PAG viewer application -├── exporter/ # After Effects exporter plugin -├── playground/ # Development scratch area -├── spec/ # PAG format specification -├── assets/ # Asset lists and metadata -├── CMakeLists.txt # Root CMake build definition -├── DEPS # Dependency version pins -├── vendor.json # Third-party vendor manifest -├── sync_deps.sh # Dependency sync script (run before first build) -├── codeformat.sh # clang-format + header guard formatter -└── accept_baseline.sh # Screenshot baseline acceptance (run via /accept-baseline) -``` - -## Key Files - -| File | Purpose | -|------|---------| -| `include/pag/pag.h` | Master public C++ API header | -| `include/pag/file.h` | PAGFile, PAGComposition, layer type declarations | -| `include/pagx/PAGXImporter.h` | PAGX XML import entry point | -| `include/pagx/PAGXExporter.h` | PAGX XML export entry point | -| `src/rendering/PAGPlayer.cpp` | Animation playback engine | -| `src/rendering/PAGSurface.cpp` | GPU rendering surface | -| `src/rendering/layers/PAGStage.cpp` | Root render scene graph | -| `src/rendering/caches/RenderCache.cpp` | GPU resource / frame cache | -| `src/rendering/renderers/LayerRenderer.cpp` | Per-layer render dispatch | -| `src/codec/Codec.cpp` | Binary PAG encode/decode entry | -| `src/pagx/PAGXImporter.cpp` | PAGX XML parser | -| `src/pagx/SystemFonts.cpp` | OS font enumeration (silent on failure) | -| `src/cli/main.cpp` | `pagx-cli` entry point | -| `src/platform/Platform.h` | Platform abstraction interface | -| `CMakeLists.txt` | Build system root | -| `test/baseline/version.json` | Screenshot baseline version registry | - -## Module Boundaries - -| Module | Allowed Dependencies | Forbidden | -|--------|---------------------|-----------| -| `src/base/` | Standard library, `include/pag/types.h` | rendering, codec, pagx, platform | -| `src/codec/` | `src/base/`, LZ4, MP4 utils | rendering, pagx, platform | -| `src/rendering/` | `src/base/`, `src/renderer/`, TGFX, platform `Drawable` | codec (via `PAGFile::Load` result), pagx | -| `src/renderer/` | `src/base/`, HarfBuzz, TGFX | rendering (no upward deps) | -| `src/pagx/` | `src/base/`, `src/codec/`, Expat, `src/renderer/` | rendering (uses codec output) | -| `src/cli/` | `src/pagx/`, `src/rendering/`, `include/pagx/` | platform internals | -| `src/platform/` | `src/rendering/` drawables, TGFX | `src/base/` data model directly | -| `src/c/` | `include/pag/` public API only | internal rendering headers | - -## Public API Surface - -### C++ API (`include/pag/`) - -- **`PAGPlayer`** — playback control: `setComposition`, `setProgress`, `flush`, `setSurface` -- **`PAGFile`** — file loading: `PAGFile::Load(path)`, `PAGFile::Load(bytes, size)` -- **`PAGSurface`** — rendering target: `PAGSurface::MakeFromWindow(...)`, `PAGSurface::MakeOffscreen(...)` -- **`PAGImage`** — image replacement: `PAGImage::FromPath(...)`, `PAGImage::FromPixels(...)` -- **`PAGFont`** — font registration: `PAGFont::RegisterFont(...)` -- **`PAGDecoder`** — frame-by-frame decode: `PAGDecoder::Make(file, ...)`, `copyFrameTo(pixels, ...)` -- **`PAGAnimator`** — animation timing helper -- **`PAGComposition`** / **`PAGLayer`** / typed sublayers — scene graph manipulation - -### PAGX API (`include/pagx/`) - -- **`PAGXImporter`** — `PAGXImporter::Import(path)` → `std::shared_ptr` -- **`PAGXExporter`** — `PAGXExporter::Export(file, path)` -- **`PAGXDocument`** — XML document model -- **`SVGImporter`** / **`SVGExporter`** — SVG conversion -- **`FontConfig`** — font embedding/lookup configuration -- **Node types** in `include/pagx/nodes/` — Layer, Group, Text, Image, Shape primitives -- **Value types** in `include/pagx/types/` — enums and structs used by node properties - -### C API (`include/pag/c/`) - -Thin C wrappers over the C++ API. All types are opaque pointers (`pag_player_t*`, etc.). Intended for FFI use from non-C++ languages. - -## Naming Conventions - -**Files:** -- C++ class files: `ClassName.cpp` / `ClassName.h` (PascalCase) -- CLI command files: `CommandVerb.cpp` / `CommandVerb.h` -- C API files: `pag_noun.cpp` / `pag_noun.h` (snake_case) - -**Directories:** -- Lowercase, plural for categories (`renderers/`, `caches/`, `filters/`, `layers/`) -- PascalCase for platform names (`android/`, `cocoa/`, `ios/`) - -## Where to Add New Code - -**New PAG data type / shape / effect:** -- Data model: `src/base/shapes/` or `src/base/effects/` -- Codec tag: `src/codec/tags/shapes/` or `src/codec/tags/effects/` -- Renderer: `src/rendering/renderers/` -- Test: `test/src/PAGShapeLayerTest.cpp` or new `test/src/PAG{Feature}Test.cpp` - -**New PAGX node type:** -- Public header: `include/pagx/nodes/NewNode.h` -- Implementation: `src/pagx/nodes/` -- Import/export support: `src/pagx/PAGXImporter.cpp` + `src/pagx/PAGXExporter.cpp` - -**New CLI command:** -- `src/cli/CommandNewVerb.cpp` + `src/cli/CommandNewVerb.h` -- Register in `src/cli/main.cpp` -- Test: `test/src/PAGXCliTest.cpp` - -**New filter effect:** -- `src/rendering/filters/NewFilter.cpp` + `.h` -- Wire into `LayerStylesFilter` or `FilterRenderer` as appropriate - -**New platform:** -- `src/platform/{platform}/` directory -- Implement `Drawable` interface for GPU context + surface -- Implement font loading + video decode hooks - ---- - -*Structure analysis: 2025-07-15* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md deleted file mode 100644 index 782d1d0a4b..0000000000 --- a/.planning/codebase/TESTING.md +++ /dev/null @@ -1,214 +0,0 @@ -# Testing Patterns - -**Analysis Date:** 2025-05-27 - -## Test Framework - -**Runner:** Google Test (gtest) -- Config: CMake target `PAGFullTest` (all tests + screenshot comparison) / `PAGUnitTest` (no screenshot comparison, `SKIP_FRAME_COMPARE` defined) -- Base header: `test/src/base/PAGTest.h` - -**Assertion Library:** Google Test built-in (`ASSERT_*`, `EXPECT_*`) - -**Run Commands:** -```bash -# Build (required before running) -./codeformat.sh 2>/dev/null; true -cmake -G Ninja -DPAG_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -B cmake-build-debug -cmake --build cmake-build-debug --target PAGFullTest - -# Run all tests -./cmake-build-debug/PAGFullTest - -# Run a specific test case -./cmake-build-debug/PAGFullTest --gtest_filter="PAGFileTest.TestPAGFileBase" - -# Run all tests in a suite -./cmake-build-debug/PAGFullTest --gtest_filter="PAGFileTest.*" -``` - -## Test File Organization - -**Location:** `test/src/` — all test `.cpp` files co-located in one directory - -**Naming:** `{SuiteName}Test.cpp` (e.g., `PAGFileTest.cpp`, `PAGFilterTest.cpp`, `PAGSurfaceTest.cpp`) - -**Utilities:** `test/src/utils/` — shared helpers for all test files: -- `Baseline.h` / `Baseline.cpp` — screenshot comparison logic -- `TestUtils.h` / `TestUtils.cpp` — `LoadPAGFile()`, `MakeSnapshot()`, `GetLayer()`, `MakeImage()`, etc. -- `OffscreenSurface.h` / `OffscreenSurface.cpp` — off-screen GPU surface factory -- `DevicePool.h` / `DevicePool.cpp` — GL device management -- `ProjectPath.h` / `ProjectPath.cpp` — absolute path resolution -- `Semaphore.h` / `Semaphore.cpp` — synchronization primitive for multi-thread tests -- `TestDir.h` / `TestDir.cpp` — test output directory management - -**Framework infrastructure:** `test/src/base/PAGTest.h` / `PAGTest.cpp` - -## Test Structure - -**Test Base Classes:** - -| Class | Defined In | Purpose | -|-------|------------|---------| -| `pag::PAGTest` | `test/src/base/PAGTest.h` | Base for all PAG rendering tests; tracks `hasFailure` global | -| `pag::PAGXTest` | `test/src/base/PAGTest.h` | Extends PAGTest; creates `GLDevice` + `Context` in `SetUp()`, unlocks in `TearDown()` | -| `pag::CLITest` | `test/src/base/PAGTest.h` | Base for CLI-only tests; no GPU setup | - -**Test Macros:** - -```cpp -// Standard rendering test (uses PAGTest base) -PAG_TEST(SuiteName, TestName) { - // test body -} - -// PAGX XML test (uses PAGXTest base — GLDevice already set up) -PAGX_TEST(SuiteName, TestName) { - // this->device and this->context are available -} - -// CLI test (uses CLITest base — no GPU context) -CLI_TEST(SuiteName, TestName) { - // test body -} -``` - -**Standard Setup Macros:** - -```cpp -// Default setup: loads resources/apitest/test.pag, creates shared OffscreenSurface -PAG_SETUP(pagSurface, pagPlayer, pagFile); -// Expands to: -// auto pagFile = LoadPAGFile("resources/apitest/test.pag"); -// auto pagSurface = OffscreenSurface::Make(pagFile->width(), pagFile->height()); -// auto pagPlayer = std::make_shared(); -// pagPlayer->setSurface(pagSurface); pagPlayer->setComposition(pagFile); - -// Isolated setup: creates PAGSurface::MakeOffscreen() instead of shared pool -PAG_SETUP_ISOLATED(pagSurface, pagPlayer, pagFile); - -// Custom path setup -PAG_SETUP_WITH_PATH(pagSurface, pagPlayer, pagFile, "resources/apitest/custom.pag"); -``` - -**Typical Test Pattern:** -```cpp -/** - * 用例描述: CornerPin用例 - */ -PAG_TEST(PAGFilterTest, CornerPin) { - auto pagFile = LoadPAGFile("resources/filter/cornerpin.pag"); - ASSERT_NE(pagFile, nullptr); - auto pagSurface = OffscreenSurface::Make(pagFile->width(), pagFile->height()); - ASSERT_NE(pagSurface, nullptr); - auto pagPlayer = std::make_shared(); - pagPlayer->setSurface(pagSurface); - pagPlayer->setComposition(pagFile); - - pagFile->setCurrentTime(1000000); - pagPlayer->flush(); - EXPECT_TRUE(Baseline::Compare(pagSurface, "PAGFilterTest/CornerPin")); -} -``` - -## Screenshot Testing - -**Mechanism:** `Baseline::Compare(surface, key)` compares rendered output against stored baseline images. - -**Key format:** `"{Folder}/{Name}"` — e.g., `"PAGFilterTest/CornerPin"`, `"PAGSurfaceTest/Mask"` - -**Output path:** `test/out/{Folder}/{Name}.webp` - -**Baseline path:** `test/out/{Folder}/{Name}_base.webp` - -**Version files:** -- `test/baseline/version.json` — committed repository baselines -- `test/baseline/.cache/version.json` — local cache of accepted versions - -**Comparison logic:** -- Both repo and cache versions exist AND differ → skip comparison, return `true` (change accepted) -- Otherwise → compare rendered output against `_base.webp`; fail if missing or pixels differ - -**Accepting baseline changes:** -- **Never** manually run `accept_baseline.sh` or modify `version.json` directly -- The only permitted workflow is the user running the `/accept-baseline` slash command - -**Baseline.Compare overloads** (`test/src/utils/Baseline.h`): -```cpp -Baseline::Compare(std::shared_ptr surface, const std::string& key); -Baseline::Compare(const tgfx::Bitmap& bitmap, const std::string& key); -Baseline::Compare(const tgfx::Pixmap& pixmap, const std::string& key); -Baseline::Compare(const std::shared_ptr& surface, const std::string& key); -Baseline::Compare(const std::shared_ptr& byteData, const std::string& key); -``` - -**Test output images** are written as WebP at 100% quality via `tgfx::ImageCodec::Encode()`. - -## Test Coverage - -**Test Suites** (`test/src/`): - -| File | Suite | Coverage Area | -|------|-------|---------------| -| `PAGFileTest.cpp` | `PAGFileTest` | PAGFile load, metadata, text/image layer access | -| `PAGPlayerTest.cpp` | `PAGPlayerTest` | PAGPlayer composition, flush, surface switching | -| `PAGSurfaceTest.cpp` | `PAGSurfaceTest` | Surface from texture, pixel readback | -| `PAGFilterTest.cpp` | `PAGFilterTest` | All 30+ filter effects (CornerPin, Bulge, blur, etc.) | -| `PAGLayerTest.cpp` | `PAGLayerTest` | Layer properties and manipulation | -| `PAGImageTest.cpp` | `PAGImageTest` | PAGImage creation and replacement | -| `PAGImageLayerTest.cpp` | `PAGImageLayerTest` | Image layer behavior | -| `PAGTextLayerTest.cpp` | `PAGTextLayerTest` | Text layer editing and rendering | -| `PAGShapeLayerTest.cpp` | `PAGShapeLayerTest` | Shape layer rendering | -| `PAGSolidLayerTest.cpp` | `PAGSolidLayerTest` | Solid layer rendering | -| `PAGCompositionTest.cpp` | `PAGCompositionTest` | Composition nesting and playback | -| `PAGBlendTest.cpp` | `PAGBlendTest` | Blend mode rendering | -| `PAGFontTest.cpp` | `PAGFontTest` | Font loading and fallback | -| `PAGDiskCacheTest.cpp` | `PAGDiskCacheTest` | Disk cache behavior | -| `PAGSequenceTest.cpp` | `PAGSequenceTest` | Bitmap/video sequence decoding | -| `PAGTimeStretchTest.cpp` | `PAGTimeStretchTest` | Time remapping | -| `PAGTimeUtilsTest.cpp` | `PAGTimeUtilsTest` | Time utility functions | -| `PAGGradientColorTest.cpp` | `PAGGradientColorTest` | Gradient color rendering | -| `PAGSimplePathTest.cpp` | `PAGSimplePathTest` | Path simplification | -| `PAGCompareFrameTest.cpp` | `PAGCompareFrameTest` | Frame-by-frame comparison | -| `PAGFuzzTest.cpp` | `PAGFuzzTest` | Fuzz / malformed input handling | -| `AsyncDecodeTest.cpp` | `AsyncDecode` | Async video/bitmap decoding | -| `MultiThreadCase.cpp` | `SimpleMultiThreadCase` | Concurrent multi-PAGPlayer rendering | -| `PAGXTest.cpp` | Various PAGX suites | PAGX import/export, layout, CLI operations | -| `PAGXSVGTest.cpp` | `PAGXSVGTest` | SVG export correctness | -| `PAGXCliTest.cpp` | Various CLI suites | CLI command execution (embed, export, verify, etc.) | - -## Running Tests - -**Build requirement:** Must pass `-DPAG_BUILD_TESTS=ON` to enable all modules. - -```bash -# Standard build + run -./codeformat.sh 2>/dev/null; true -cmake -G Ninja -DPAG_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -B cmake-build-debug -cmake --build cmake-build-debug --target PAGFullTest -./cmake-build-debug/PAGFullTest - -# Filter to one suite -./cmake-build-debug/PAGFullTest --gtest_filter="PAGFilterTest.*" - -# Filter to one case -./cmake-build-debug/PAGFullTest --gtest_filter="PAGFileTest.TestPAGFileBase" - -# Build with local TGFX source (when debugging rendering engine) -cmake -G Ninja -DPAG_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DTGFX_DIR=../tgfx -B cmake-build-debuglocal -cmake --build cmake-build-debuglocal --target PAGFullTest -``` - -**Non-zero exit code from PAGFullTest is normal when tests fail** — do not re-run the same command. - -**Test resources** live in `resources/` directory. Access via: -```cpp -LoadPAGFile("resources/apitest/test.pag"); // resolved to absolute path internally -MakePAGImage("resources/apitest/rotation.jpg"); -``` - -**Numeric test values** (font sizes, coordinates, matrices) must use integers — no floating-point literals. - ---- - -*Testing analysis: 2025-05-27* From 6a3251bee04dba98cc4a0fb7b8b325ca754ed1aa Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 6 May 2026 11:40:53 +0800 Subject: [PATCH 60/87] Implement SystemFonts::FindFont for Windows using DirectWrite FindFamilyName. --- src/pagx/SystemFonts.cpp | 136 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 3 deletions(-) diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index 19f3bc564a..566ee90491 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -497,9 +497,139 @@ std::vector SystemFonts::AllFontFamilies() { return entries; } -FontLocation SystemFonts::FindFont(const std::string&, const std::string&) { - // Windows FreeType backend already implements MakeFromName via DirectWrite. - return {}; +FontLocation SystemFonts::FindFont(const std::string& family, const std::string& style) { + if (family.empty()) { + return {}; + } + + IDWriteFactory* factory = nullptr; + HRESULT hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), + reinterpret_cast(&factory)); + if (FAILED(hr) || factory == nullptr) { + return {}; + } + + IDWriteFontCollection* fontCollection = nullptr; + hr = factory->GetSystemFontCollection(&fontCollection); + if (FAILED(hr) || fontCollection == nullptr) { + SafeRelease(&factory); + return {}; + } + + int familyLen = MultiByteToWideChar(CP_UTF8, 0, family.c_str(), -1, nullptr, 0); + if (familyLen <= 0) { + SafeRelease(&fontCollection); + SafeRelease(&factory); + return {}; + } + std::wstring wideFamily(static_cast(familyLen), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, family.c_str(), -1, wideFamily.data(), familyLen); + + UINT32 familyIndex = 0; + BOOL exists = FALSE; + hr = fontCollection->FindFamilyName(wideFamily.c_str(), &familyIndex, &exists); + if (FAILED(hr) || !exists) { + SafeRelease(&fontCollection); + SafeRelease(&factory); + return {}; + } + + IDWriteFontFamily* fontFamily = nullptr; + hr = fontCollection->GetFontFamily(familyIndex, &fontFamily); + if (FAILED(hr) || fontFamily == nullptr) { + SafeRelease(&fontCollection); + SafeRelease(&factory); + return {}; + } + + UINT32 fontCount = fontFamily->GetFontCount(); + IDWriteFont* matchedFont = nullptr; + + if (style.empty()) { + for (UINT32 j = 0; j < fontCount; j++) { + hr = fontFamily->GetFont(j, &matchedFont); + if (FAILED(hr) || matchedFont == nullptr) { + continue; + } + if (matchedFont->GetSimulations() == DWRITE_FONT_SIMULATIONS_NONE) { + break; + } + SafeRelease(&matchedFont); + } + if (matchedFont == nullptr) { + fontFamily->GetFont(0, &matchedFont); + } + } else { + for (UINT32 j = 0; j < fontCount; j++) { + IDWriteFont* font = nullptr; + hr = fontFamily->GetFont(j, &font); + if (FAILED(hr) || font == nullptr) { + continue; + } + if (font->GetSimulations() != DWRITE_FONT_SIMULATIONS_NONE) { + SafeRelease(&font); + continue; + } + auto faceName = GetFaceName(font); + if (_stricmp(faceName.c_str(), style.c_str()) == 0) { + matchedFont = font; + break; + } + SafeRelease(&font); + } + } + + if (matchedFont == nullptr) { + SafeRelease(&fontFamily); + SafeRelease(&fontCollection); + SafeRelease(&factory); + return {}; + } + + IDWriteFontFace* fontFace = nullptr; + hr = matchedFont->CreateFontFace(&fontFace); + SafeRelease(&matchedFont); + if (FAILED(hr) || fontFace == nullptr) { + SafeRelease(&fontFamily); + SafeRelease(&fontCollection); + SafeRelease(&factory); + return {}; + } + + UINT32 fileCount = 1; + IDWriteFontFile* fontFile = nullptr; + hr = fontFace->GetFiles(&fileCount, &fontFile); + if (FAILED(hr) || fontFile == nullptr) { + SafeRelease(&fontFace); + SafeRelease(&fontFamily); + SafeRelease(&fontCollection); + SafeRelease(&factory); + return {}; + } + + const void* refKey = nullptr; + UINT32 refKeySize = 0; + hr = fontFile->GetReferenceKey(&refKey, &refKeySize); + + FontLocation location = {}; + // GetReferenceKey includes the null terminator in the byte count. + int keyLen = static_cast(refKeySize / sizeof(wchar_t)); + if (SUCCEEDED(hr) && refKey != nullptr && keyLen > 1) { + location.path = WideToUTF8(static_cast(refKey), keyLen - 1); + } + SafeRelease(&fontFile); + + location.ttcIndex = static_cast(fontFace->GetIndex()); + location.fontFamily = GetFamilyName(fontFamily); + if (!style.empty()) { + location.fontStyle = style; + } + + SafeRelease(&fontFace); + SafeRelease(&fontFamily); + SafeRelease(&fontCollection); + SafeRelease(&factory); + return location; } } // namespace pagx From a21eac17fcbc8070506d56dd658507e88f32f9c8 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 7 May 2026 17:04:14 +0800 Subject: [PATCH 61/87] Stop ignoring AI planning artifacts. --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9bcee3817f..1c2e2b27e5 100644 --- a/.gitignore +++ b/.gitignore @@ -48,9 +48,6 @@ local.properties # CodeBuddy .codebuddy/designs/ -# AI agent planning artifacts -.planning/ - # Local config *.local.json *.local.md From ee06e50b2ef439df531670b32ceb721566870bed Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 8 May 2026 11:12:03 +0800 Subject: [PATCH 62/87] Fix code review issues from PR #3405 second round review. --- cli/npm/README.md | 6 +-- include/pagx/PAGXDocument.h | 40 ++++++++++++++++++-- src/cli/CliUtils.h | 22 ++++++++++- src/cli/CommandEmbed.cpp | 22 +++++++++-- src/pagx/PAGXDocument.cpp | 60 +++++++++++++++++++++++++++--- src/pagx/SystemFonts.cpp | 68 ++++++++++++++++------------------ src/renderer/FontEmbedder.cpp | 33 +++++------------ src/renderer/ImageEmbedder.cpp | 12 +++--- 8 files changed, 178 insertions(+), 85 deletions(-) diff --git a/cli/npm/README.md b/cli/npm/README.md index 07132f6697..6d0940d09e 100644 --- a/cli/npm/README.md +++ b/cli/npm/README.md @@ -76,8 +76,8 @@ pagx font --list # Embed fonts and images into a PAGX file pagx embed input.pagx -# Embed with a custom font file and fallback -pagx embed --file BrandFont.ttf --fallback "Arial" input.pagx +# Embed with a custom font file and fallback (--font-file is an alias for --file) +pagx embed --font-file BrandFont.ttf --fallback "Arial" input.pagx # Embed images only (skip font embedding) pagx embed --skip-fonts input.pagx @@ -176,7 +176,7 @@ Embed font glyphs and images into a PAGX file for self-contained output. | Option | Description | |--------|-------------| | `-o, --output ` | Output file path (default: overwrite input) | -| `--file ` | Register a font file (repeatable) | +| `--file, --font-file ` | Register a font file (repeatable) | | `--fallback ` | Add a fallback font file or system font name (repeatable) | | `--skip-fonts` | Skip font embedding | | `--skip-images` | Skip image embedding | diff --git a/include/pagx/PAGXDocument.h b/include/pagx/PAGXDocument.h index 635cb91892..6324de8a3d 100644 --- a/include/pagx/PAGXDocument.h +++ b/include/pagx/PAGXDocument.h @@ -20,6 +20,8 @@ #include #include +#include +#include #include #include "pagx/FontConfig.h" #include "pagx/nodes/Layer.h" @@ -127,11 +129,20 @@ class PAGXDocument : public Node { */ bool loadFileData(const std::string& filePath, std::shared_ptr data); + /** + * Batch version of loadFileData. Loads file data for all Image nodes whose filePath matches + * a key in the map in a single pass over the nodes. More efficient than calling loadFileData + * individually for each file when embedding multiple images. + * @param fileDataMap a map from file path to the file content to embed + */ + void loadFileDataMap( + const std::unordered_map>& fileDataMap); + /** * Executes auto layout on the document, positioning layers according to their layout - * constraints. Must be called before rendering or font embedding. This method should only - * be called once per document — repeated calls may produce incorrect results because - * measurement data is cached and some layout operations permanently modify source geometry. + * constraints. Must be called before rendering or font embedding. Repeated calls are safe + * only after calling ClearEmbeddedGlyphRuns + resetLayoutState(), which clears embedded + * glyph data and resets the layout flag so layout re-runs with fresh shaping data. * @param fontConfig Optional font config for text measurement and rendering. When provided, * updates the internal config before layout. Pass nullptr to use the * previously set config (or no config). @@ -160,6 +171,28 @@ class PAGXDocument : public Node { */ void clearEmbed(); + /** + * Removes the specified nodes from the document and cleans up their nodeMap entries. + * Node pointers in the set and any pointers derived from them (e.g. child glyphs) are + * invalidated after this call. Callers must first collect all affected nodes to remove + * before calling. + */ + void removeNodes(const std::unordered_set& nodesToRemove); + + /** + * Assigns or changes the ID of an existing node. If the new ID already exists in the + * document, the old entry is replaced. If the ID is empty, the node is removed from the + * lookup index. The node must already be managed by this document. + */ + void setNodeId(Node* node, const std::string& id); + + /** + * Resets the layout-applied flag to allow applyLayout() to be called again. Must be paired + * with ClearEmbeddedGlyphRuns before re-embedding to ensure layout re-runs with fresh + * shaping data. + */ + void resetLayoutState(); + NodeType nodeType() const override { return NodeType::Document; } @@ -178,7 +211,6 @@ class PAGXDocument : public Node { friend class PAGXImporter; friend class PAGXExporter; friend class TextLayoutContext; - friend class FontEmbedder; }; } // namespace pagx diff --git a/src/cli/CliUtils.h b/src/cli/CliUtils.h index d0d79fde09..9679817235 100644 --- a/src/cli/CliUtils.h +++ b/src/cli/CliUtils.h @@ -47,6 +47,22 @@ static inline bool FontFamilyMatch(const std::string& requested, const std::stri return true; } +static inline bool FontStyleMatch(const std::string& requested, const std::string& actual) { + if (requested.empty()) { + return true; + } + if (requested.size() != actual.size()) { + return false; + } + for (size_t i = 0; i < requested.size(); i++) { + if (std::tolower(static_cast(requested[i])) != + std::tolower(static_cast(actual[i]))) { + return false; + } + } + return true; +} + /** * Resolves a system font by family and style with fallback. First attempts MakeFromName for an * exact match. If MakeFromName is unavailable (e.g. FreeType backend on macOS), falls back to @@ -55,12 +71,14 @@ static inline bool FontFamilyMatch(const std::string& requested, const std::stri static inline std::shared_ptr ResolveSystemTypeface(const std::string& family, const std::string& style) { auto typeface = tgfx::Typeface::MakeFromName(family, style); - if (typeface != nullptr && FontFamilyMatch(family, typeface->fontFamily())) { + if (typeface != nullptr && FontFamilyMatch(family, typeface->fontFamily()) && + FontStyleMatch(style, typeface->fontStyle())) { return typeface; } if (!style.empty()) { typeface = tgfx::Typeface::MakeFromName(family, ""); - if (typeface != nullptr && FontFamilyMatch(family, typeface->fontFamily())) { + if (typeface != nullptr && FontFamilyMatch(family, typeface->fontFamily()) && + FontStyleMatch(style, typeface->fontStyle())) { return typeface; } } diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp index 567976740a..b23f8817f4 100644 --- a/src/cli/CommandEmbed.cpp +++ b/src/cli/CommandEmbed.cpp @@ -18,6 +18,8 @@ #include "cli/CommandEmbed.h" #include +#include +#include #include #include #include @@ -139,11 +141,23 @@ int RunEmbed(int argc, char* argv[]) { auto xml = PAGXExporter::ToXML(*document); if (options.outputFile == options.inputFile) { auto tempPath = options.outputFile + ".tmp"; - if (!WriteStringToFile(xml, tempPath, "pagx embed")) { - std::remove(tempPath.c_str()); - return 1; + { + std::ofstream out(tempPath); + if (!out.is_open()) { + std::cerr << "pagx embed: failed to write '" << tempPath << "'\n"; + return 1; + } + out << xml; + out.close(); + if (out.fail()) { + std::cerr << "pagx embed: error writing to '" << tempPath << "'\n"; + std::remove(tempPath.c_str()); + return 1; + } } - if (std::rename(tempPath.c_str(), options.outputFile.c_str()) != 0) { + std::error_code ec; + std::filesystem::rename(tempPath, options.outputFile, ec); + if (ec) { std::cerr << "pagx embed: failed to replace '" << options.outputFile << "'\n"; std::remove(tempPath.c_str()); return 1; diff --git a/src/pagx/PAGXDocument.cpp b/src/pagx/PAGXDocument.cpp index d52742546f..a1294e952a 100644 --- a/src/pagx/PAGXDocument.cpp +++ b/src/pagx/PAGXDocument.cpp @@ -75,6 +75,40 @@ void PAGXDocument::registerNode(Node* node, const std::string& id) { nodeMap[id] = node; } +void PAGXDocument::removeNodes(const std::unordered_set& toRemove) { + for (auto it = nodeMap.begin(); it != nodeMap.end();) { + if (toRemove.count(it->second) > 0) { + it = nodeMap.erase(it); + } else { + ++it; + } + } + size_t writeIdx = 0; + for (size_t readIdx = 0; readIdx < nodes.size(); readIdx++) { + if (toRemove.count(nodes[readIdx].get()) == 0) { + nodes[writeIdx++] = std::move(nodes[readIdx]); + } + } + nodes.resize(writeIdx); +} + +void PAGXDocument::setNodeId(Node* node, const std::string& id) { + if (node == nullptr) { + return; + } + if (!node->id.empty()) { + auto it = nodeMap.find(node->id); + if (it != nodeMap.end() && it->second == node) { + nodeMap.erase(it); + } + } + registerNode(node, id); +} + +void PAGXDocument::resetLayoutState() { + layoutApplied = false; +} + static bool LayersHaveImports(const std::vector& layers) { for (auto* layer : layers) { if (!layer->importDirective.source.empty() || !layer->importDirective.content.empty()) { @@ -103,12 +137,9 @@ bool PAGXDocument::hasUnresolvedImports() const { } static bool IsUrlPath(const std::string& path) { - if (path.find("data:") == 0) { - return true; - } // Match known URL schemes rather than generic :// to avoid false positives on Windows paths - // like C://Users/file.png. - return path.find("http://") == 0 || path.find("https://") == 0; + // like C://Users/file.png. data: is handled by PAGXImporter before this point. + return path.find("http://") == 0 || path.find("https://") == 0 || path.find("file://") == 0; } std::vector PAGXDocument::getExternalFilePaths() const { @@ -162,4 +193,23 @@ void PAGXDocument::clearEmbed() { FontEmbedder::ClearEmbeddedGlyphRuns(this); } +void PAGXDocument::loadFileDataMap( + const std::unordered_map>& fileDataMap) { + for (auto& node : nodes) { + if (node->nodeType() != NodeType::Image) { + continue; + } + auto* image = static_cast(node.get()); + if (image->data != nullptr || image->filePath.empty()) { + continue; + } + auto it = fileDataMap.find(image->filePath); + if (it == fileDataMap.end()) { + continue; + } + image->data = it->second; + image->filePath = {}; + } +} + } // namespace pagx diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index 566ee90491..b847195bde 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -179,6 +179,10 @@ std::vector SystemFonts::AllFontFamilies() { const void* mandatoryKeys[] = {kCTFontFamilyNameAttribute}; CFSetRef mandatoryAttributes = CFSetCreate(kCFAllocatorDefault, mandatoryKeys, 1, &kCFTypeSetCallBacks); + if (mandatoryAttributes == nullptr) { + CFRelease(familyNames); + return {}; + } for (CFIndex i = 0; i < familyCount; i++) { auto cfFamilyName = static_cast(CFArrayGetValueAtIndex(familyNames, i)); @@ -269,12 +273,13 @@ FontLocation SystemFonts::FindFont(const std::string& family, const std::string& int mandatoryCount = style.empty() ? 1 : 2; CFSetRef mandatoryAttributes = CFSetCreate(kCFAllocatorDefault, mandatoryKeys, mandatoryCount, &kCFTypeSetCallBacks); + if (mandatoryAttributes == nullptr) { + return {}; + } CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes(attributes); CFRelease(attributes); if (descriptor == nullptr) { - if (mandatoryAttributes != nullptr) { - CFRelease(mandatoryAttributes); - } + CFRelease(mandatoryAttributes); return {}; } CFArrayRef matches = @@ -325,10 +330,8 @@ static std::string WideToUTF8(const wchar_t* wide, int length) { return result; } -static std::string GetFamilyName(IDWriteFontFamily* fontFamily) { - IDWriteLocalizedStrings* names = nullptr; - HRESULT hr = fontFamily->GetFamilyNames(&names); - if (FAILED(hr) || names == nullptr) { +static std::string GetLocalizedName(IDWriteLocalizedStrings* names) { + if (names == nullptr) { return {}; } @@ -340,15 +343,13 @@ static std::string GetFamilyName(IDWriteFontFamily* fontFamily) { } UINT32 length = 0; - hr = names->GetStringLength(index, &length); + HRESULT hr = names->GetStringLength(index, &length); if (FAILED(hr) || length == 0) { - SafeRelease(&names); return {}; } std::wstring wide(static_cast(length) + 1, L'\0'); hr = names->GetString(index, wide.data(), length + 1); - SafeRelease(&names); if (FAILED(hr)) { return {}; } @@ -356,35 +357,26 @@ static std::string GetFamilyName(IDWriteFontFamily* fontFamily) { return WideToUTF8(wide.c_str(), static_cast(length)); } -static std::string GetFaceName(IDWriteFont* font) { +static std::string GetFamilyName(IDWriteFontFamily* fontFamily) { IDWriteLocalizedStrings* names = nullptr; - HRESULT hr = font->GetFaceNames(&names); - if (FAILED(hr) || names == nullptr) { - return {}; - } - - UINT32 index = 0; - BOOL exists = FALSE; - names->FindLocaleName(L"en-us", &index, &exists); - if (!exists) { - index = 0; - } - - UINT32 length = 0; - hr = names->GetStringLength(index, &length); - if (FAILED(hr) || length == 0) { - SafeRelease(&names); + HRESULT hr = fontFamily->GetFamilyNames(&names); + if (FAILED(hr)) { return {}; } - - std::wstring wide(static_cast(length) + 1, L'\0'); - hr = names->GetString(index, wide.data(), length + 1); + auto result = GetLocalizedName(names); SafeRelease(&names); + return result; +} + +static std::string GetFaceName(IDWriteFont* font) { + IDWriteLocalizedStrings* names = nullptr; + HRESULT hr = font->GetFaceNames(&names); if (FAILED(hr)) { return {}; } - - return WideToUTF8(wide.c_str(), static_cast(length)); + auto result = GetLocalizedName(names); + SafeRelease(&names); + return result; } std::vector SystemFonts::FallbackTypefaces() { @@ -522,8 +514,10 @@ FontLocation SystemFonts::FindFont(const std::string& family, const std::string& SafeRelease(&factory); return {}; } - std::wstring wideFamily(static_cast(familyLen), L'\0'); + std::wstring wideFamily; + wideFamily.resize(static_cast(familyLen)); MultiByteToWideChar(CP_UTF8, 0, family.c_str(), -1, wideFamily.data(), familyLen); + wideFamily.resize(static_cast(familyLen) - 1); UINT32 familyIndex = 0; BOOL exists = FALSE; @@ -740,13 +734,13 @@ std::vector SystemFonts::AllFontFamilies() { continue; } - auto it = familyIndex.find(familyStr); - if (it == familyIndex.end()) { + auto [it, inserted] = familyIndex.try_emplace(familyStr, entries.size()); + if (inserted) { FontFamilyEntry entry = {}; entry.family = familyStr; entries.push_back(std::move(entry)); - seenStylesPerEntry.push_back({}); - it = familyIndex.insert({familyStr, entries.size() - 1}).first; + seenStylesPerEntry.emplace_back(); + it->second = entries.size() - 1; } FcChar8* styleRaw = nullptr; diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index 2d4c7f1ef0..9b6d6eb2c1 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -39,6 +39,7 @@ namespace pagx { static constexpr int VectorFontUnitsPerEm = 1000; +static constexpr float MinFontSize = 0.001f; static void PathToPathData(const tgfx::Path& path, PathData* pathData) { for (const auto& segment : path) { @@ -118,7 +119,7 @@ static void CollectVectorGlyph(PAGXDocument* document, const tgfx::Font& font, if (!font.getPath(glyphID, &glyphPath) || glyphPath.isEmpty()) { return; } - if (fontSize < 0.001f) { + if (fontSize < MinFontSize) { return; } float scale = static_cast(VectorFontUnitsPerEm) / fontSize; @@ -168,7 +169,7 @@ static void CollectBitmapGlyph( if (builder.backingSize == 0) { float scaleX = std::abs(imageMatrix.getScaleX()); - if (scaleX < 0.001f) { + if (scaleX < MinFontSize) { return; } builder.backingSize = static_cast(std::round(font.getSize() / scaleX)); @@ -358,7 +359,7 @@ static void CollectSpacingGlyph( auto* typeface = font.getTypeface().get(); GlyphKey key = {typeface, glyphID}; float runFontSize = font.getSize(); - if (runFontSize < 0.001f) { + if (runFontSize < MinFontSize) { return; } auto bitmapIt = bitmapBuilders.find(typeface); @@ -412,23 +413,8 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { toRemove.insert(node.get()); } } - // Clean up nodeMap before compacting nodes so the pointers in nodeMap remain valid. - for (auto it = document->nodeMap.begin(); it != document->nodeMap.end();) { - if (toRemove.count(it->second) > 0) { - it = document->nodeMap.erase(it); - } else { - ++it; - } - } - - auto& nodes = document->nodes; - size_t writeIdx = 0; - for (size_t readIdx = 0; readIdx < nodes.size(); readIdx++) { - if (toRemove.count(nodes[readIdx].get()) == 0) { - nodes[writeIdx++] = std::move(nodes[readIdx]); - } - } - nodes.resize(writeIdx); + document->removeNodes(toRemove); + document->resetLayoutState(); } bool FontEmbedder::embed(PAGXDocument* document) { @@ -482,10 +468,11 @@ bool FontEmbedder::embed(PAGXDocument* document) { } } - // Assign sequential IDs to all fonts + // Assign sequential IDs to all embedded fonts using a reserved prefix to avoid + // collisions with user-created nodes. int fontIndex = 1; if (vectorBuilder.font != nullptr) { - vectorBuilder.font->id = "font" + std::to_string(fontIndex++); + document->setNodeId(vectorBuilder.font, "__embed_font_" + std::to_string(fontIndex++)); } for (auto* typeface : bitmapTypefaces) { if (typeface == nullptr) { @@ -493,7 +480,7 @@ bool FontEmbedder::embed(PAGXDocument* document) { } auto builderIt = bitmapBuilders.find(typeface); if (builderIt != bitmapBuilders.end() && builderIt->second.font != nullptr) { - builderIt->second.font->id = "font" + std::to_string(fontIndex++); + document->setNodeId(builderIt->second.font, "__embed_font_" + std::to_string(fontIndex++)); } } diff --git a/src/renderer/ImageEmbedder.cpp b/src/renderer/ImageEmbedder.cpp index df01acccca..cb83ae0b20 100644 --- a/src/renderer/ImageEmbedder.cpp +++ b/src/renderer/ImageEmbedder.cpp @@ -18,7 +18,7 @@ #include "renderer/ImageEmbedder.h" #include -#include +#include #include "base/utils/Log.h" #include "pagx/types/Data.h" @@ -51,9 +51,9 @@ static std::shared_ptr ReadFileToData(const std::string& path) { bool ImageEmbedder::embed(PAGXDocument* document) { if (document == nullptr) return false; auto paths = document->getExternalFilePaths(); - std::unordered_set loaded; + std::unordered_map> fileDataMap; for (const auto& path : paths) { - if (!loaded.insert(path).second) { + if (fileDataMap.count(path) > 0) { continue; } auto data = ReadFileToData(path); @@ -61,11 +61,9 @@ bool ImageEmbedder::embed(PAGXDocument* document) { lastErrorPath_ = path; return false; } - if (!document->loadFileData(path, data)) { - lastErrorPath_ = path; - return false; - } + fileDataMap[path] = data; } + document->loadFileDataMap(fileDataMap); return true; } From ebc82dd4d17e6d8159f1fadf5c68f487961428e7 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 25 May 2026 15:25:58 +0800 Subject: [PATCH 63/87] Add file attribute to PAGX Font node with XSD schema and import/export support. --- include/pagx/nodes/Font.h | 8 ++++++++ spec/pagx.xsd | 1 + src/pagx/PAGXExporter.cpp | 3 +++ src/pagx/PAGXImporter.cpp | 8 ++++++++ 4 files changed, 20 insertions(+) diff --git a/include/pagx/nodes/Font.h b/include/pagx/nodes/Font.h index d2f4434ea5..b9e62dfc00 100644 --- a/include/pagx/nodes/Font.h +++ b/include/pagx/nodes/Font.h @@ -18,6 +18,7 @@ #pragma once +#include #include #include "pagx/nodes/Node.h" #include "pagx/types/Point.h" @@ -76,6 +77,13 @@ class Font : public Node { */ int unitsPerEm = 1000; + /** + * Path to an external font file. When set, glyph data is extracted from this file during embed + * instead of being read from Glyph children. The path is resolved relative to the PAGX file's + * directory. An empty string means no external reference. + */ + std::string file = {}; + /** * The list of glyphs in this font. GlyphID is the index + 1 (GlyphID 0 is reserved for missing * glyph). diff --git a/spec/pagx.xsd b/spec/pagx.xsd index d2e5da817d..b8dcdb6c04 100644 --- a/spec/pagx.xsd +++ b/spec/pagx.xsd @@ -508,6 +508,7 @@ + diff --git a/src/pagx/PAGXExporter.cpp b/src/pagx/PAGXExporter.cpp index 4d76465a9d..607102378e 100644 --- a/src/pagx/PAGXExporter.cpp +++ b/src/pagx/PAGXExporter.cpp @@ -1004,6 +1004,9 @@ static void WriteResource(XMLBuilder& xml, const Node* node, const Options& opti xml.openElement("Font"); xml.addAttribute("id", font->id); xml.addAttribute("unitsPerEm", font->unitsPerEm, Default().unitsPerEm); + if (!font->file.empty()) { + xml.addAttribute("file", font->file); + } WriteCustomData(xml, node); if (font->glyphs.empty()) { xml.closeElementSelfClosing(); diff --git a/src/pagx/PAGXImporter.cpp b/src/pagx/PAGXImporter.cpp index 32aaf9244f..9e9d764ad3 100644 --- a/src/pagx/PAGXImporter.cpp +++ b/src/pagx/PAGXImporter.cpp @@ -1451,6 +1451,7 @@ static Font* ParseFont(const DOMNode* node, PAGXDocument* doc) { return nullptr; } font->unitsPerEm = GetIntAttribute(node, "unitsPerEm", Default().unitsPerEm, doc); + font->file = GetAttribute(node, "file"); auto child = node->firstChild; while (child) { if (child->type == DOMNodeType::Element) { @@ -2145,6 +2146,13 @@ std::shared_ptr PAGXImporter::FromFile(const std::string& filePath image->filePath = basePath + image->filePath; } } + if (node->nodeType() == NodeType::Font) { + auto* font = static_cast(node.get()); + if (!font->file.empty() && font->file[0] != '/' && + font->file.find("://") == std::string::npos) { + font->file = basePath + font->file; + } + } } } } From 1b95b5ded5c073e8ad7b2a94ca8f8b107582fd8e Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 25 May 2026 15:28:54 +0800 Subject: [PATCH 64/87] Auto-register fonts from Font file attribute in pagx embed and update spec docs. --- spec/pagx_spec.md | 6 +++++- spec/pagx_spec.zh_CN.md | 6 +++++- src/cli/CommandEmbed.cpp | 21 +++++++++++++++------ src/renderer/FontEmbedder.cpp | 26 +++++++++++++++++++------- src/renderer/FontEmbedder.h | 4 +++- 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/spec/pagx_spec.md b/spec/pagx_spec.md index 264f7614b1..0ee736e1ce 100644 --- a/spec/pagx_spec.md +++ b/spec/pagx_spec.md @@ -488,7 +488,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). Embedding glyph data makes PAGX files fully self-contained, ensuring consistent rendering across platforms. +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. Font nodes can also reference external font files via the optional `file` attribute, serving as font source declarations for `pagx embed` to discover and register before text layout. ```xml @@ -502,10 +502,14 @@ Font defines embedded font resources containing subsetted glyph data (vector out + + + ``` | Attribute | Type | Default | Description | |-----------|------|---------|-------------| +| `file` | string | - | External font file path. When set, the Font node references an external TTF/OTF file. Relative paths resolve against the PAGX file's directory. `pagx embed` discovers Font nodes with `file`, loads the fonts, and registers them for text shaping. After embed, `file` is preserved for source traceability while embedded glyph data lives in separate Font nodes. | | `unitsPerEm` | int | 1000 | Font design space units. Rendering scale = `fontSize / unitsPerEm` | **Consistency Constraint**: All Glyphs within the same Font must be of the same type—either all `path` or all `image`. Mixing is not allowed. diff --git a/spec/pagx_spec.zh_CN.md b/spec/pagx_spec.zh_CN.md index dbbdebac8c..faa5ac5b0a 100644 --- a/spec/pagx_spec.zh_CN.md +++ b/spec/pagx_spec.zh_CN.md @@ -488,7 +488,7 @@ PathData 定义可复用的路径数据,供 Path 元素和 TextPath 修改器 #### 3.3.5 字体(Font) -Font 定义嵌入字体资源,包含子集化的字形数据(矢量轮廓或位图)。PAGX 文件通过嵌入字形数据实现完全自包含,确保跨平台渲染一致性。 +Font 定义嵌入字体资源,包含子集化的字形数据(矢量轮廓或位图)。PAGX 文件通过嵌入字形数据实现完全自包含,确保跨平台渲染一致性。Font 节点还可以通过可选的 `file` 属性引用外部字体文件,作为字体来源声明,供 `pagx embed` 在执行文本排版前发现并注册。 ```xml @@ -502,10 +502,14 @@ Font 定义嵌入字体资源,包含子集化的字形数据(矢量轮廓或 + + + ``` | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| +| `file` | string | - | 外部字体文件路径。设置后 Font 节点引用外部 TTF/OTF 文件。相对路径基于 PAGX 文件所在目录解析。`pagx embed` 会自动发现带 `file` 的 Font 节点,加载并注册字体用于文本排版。嵌入后 `file` 属性保留在输出中以保持源文件可追溯性,嵌入的字形数据位于独立的 Font 节点中。 | | `unitsPerEm` | int | 1000 | 字体设计空间单位。渲染时按 `fontSize / unitsPerEm` 缩放 | **一致性约束**:同一 Font 内的所有 Glyph 必须使用相同类型(全部 `path` 或全部 `image`),不允许混用。 diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp index b23f8817f4..b98286fe06 100644 --- a/src/cli/CommandEmbed.cpp +++ b/src/cli/CommandEmbed.cpp @@ -26,6 +26,7 @@ #include "cli/CliUtils.h" #include "pagx/FontConfig.h" #include "pagx/PAGXExporter.h" +#include "pagx/nodes/Font.h" #include "renderer/FontEmbedder.h" #include "renderer/ImageEmbedder.h" @@ -34,7 +35,6 @@ namespace pagx::cli { struct EmbedOptions { std::string inputFile = {}; std::string outputFile = {}; - std::vector fontFiles = {}; std::vector fallbacks = {}; bool skipFonts = false; bool skipImages = false; @@ -48,8 +48,6 @@ static void PrintEmbedUsage() { << "\n" << "Options:\n" << " -o, --output Output file path (default: overwrite input)\n" - << " --font-file, --file Register a font file for glyph embedding\n" - << " (can be specified multiple times)\n" << " --fallback Add a fallback font file or system font name (can\n" << " be specified multiple times)\n" << " --skip-fonts Skip font embedding\n" @@ -63,8 +61,6 @@ static int ParseEmbedOptions(int argc, char* argv[], EmbedOptions* options) { std::string arg = argv[i]; if ((arg == "-o" || arg == "--output") && i + 1 < argc) { options->outputFile = argv[++i]; - } else if ((arg == "--file" || arg == "--font-file") && i + 1 < argc) { - options->fontFiles.push_back(argv[++i]); } else if (arg == "--fallback" && i + 1 < argc) { options->fallbacks.push_back(argv[++i]); } else if (arg == "--skip-fonts") { @@ -118,9 +114,22 @@ int RunEmbed(int argc, char* argv[]) { if (!options.skipFonts) { FontConfig fontConfig = {}; - if (!LoadFontConfig(&fontConfig, options.fontFiles, options.fallbacks, "pagx embed")) { + if (!LoadFontConfig(&fontConfig, {}, options.fallbacks, "pagx embed")) { return 1; } + for (auto& node : document->nodes) { + if (node->nodeType() == NodeType::Font) { + auto* font = static_cast(node.get()); + if (!font->file.empty()) { + auto typeface = tgfx::Typeface::MakeFromPath(font->file); + if (typeface == nullptr) { + std::cerr << "pagx embed: failed to load font '" << font->file << "'\n"; + return 1; + } + fontConfig.registerTypeface(typeface); + } + } + } FontEmbedder::ClearEmbeddedGlyphRuns(document.get()); document->applyLayout(&fontConfig); FontEmbedder embedder = {}; diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index 9b6d6eb2c1..b519be0f7b 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -398,15 +398,27 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { for (auto& node : document->nodes) { auto type = node->nodeType(); if (type == NodeType::Font) { - toRemove.insert(node.get()); auto* font = static_cast(node.get()); - for (auto* glyph : font->glyphs) { - toRemove.insert(glyph); - if (glyph->path != nullptr) { - toRemove.insert(glyph->path); + if (!font->file.empty()) { + for (auto* glyph : font->glyphs) { + toRemove.insert(glyph); + if (glyph->path != nullptr) { + toRemove.insert(glyph->path); + } + if (glyph->image != nullptr) { + toRemove.insert(glyph->image); + } } - if (glyph->image != nullptr) { - toRemove.insert(glyph->image); + } else { + toRemove.insert(node.get()); + for (auto* glyph : font->glyphs) { + toRemove.insert(glyph); + if (glyph->path != nullptr) { + toRemove.insert(glyph->path); + } + if (glyph->image != nullptr) { + toRemove.insert(glyph->image); + } } } } else if (type == NodeType::GlyphRun) { diff --git a/src/renderer/FontEmbedder.h b/src/renderer/FontEmbedder.h index 0cdfcc3571..7176a62302 100644 --- a/src/renderer/FontEmbedder.h +++ b/src/renderer/FontEmbedder.h @@ -39,7 +39,9 @@ class FontEmbedder { * Resets previously-embedded font data in the document so it can be re-embedded from scratch. * Clears the embedded GlyphRuns vector on every Text node and removes previously-installed * Font nodes (along with their Glyph, PathData, and Image children) plus any orphan GlyphRun - * nodes from document->nodes. Call this before applyLayout() when re-embedding a file that + * nodes from document->nodes. Font nodes with a non-empty `file` attribute are preserved + * (only their Glyph children are cleared); Font nodes without `file` are removed entirely. + * Call this before applyLayout() when re-embedding a file that * already has embedded fonts, so that layout performs runtime shaping instead of using stale * embedded data. * From 8e4ead0fd9a8036d028b9dd5ad94756bf64a00e5 Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 25 May 2026 15:34:02 +0800 Subject: [PATCH 65/87] Update skills docs and add CLI tests for Font file attribute auto-registration. --- .../skills/pagx/references/attributes.md | 1 + .codebuddy/skills/pagx/references/cli.md | 28 +++++-- .codebuddy/skills/pagx/references/guide.md | 2 +- resources/cli/embed_font_file.pagx | 12 +++ test/src/PAGXCliTest.cpp | 82 +++++++++++++++++-- 5 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 resources/cli/embed_font_file.pagx diff --git a/.codebuddy/skills/pagx/references/attributes.md b/.codebuddy/skills/pagx/references/attributes.md index f2396fbeae..1e28650ca4 100644 --- a/.codebuddy/skills/pagx/references/attributes.md +++ b/.codebuddy/skills/pagx/references/attributes.md @@ -406,6 +406,7 @@ Embedded font resource containing subsetted glyph data (vector outlines or bitma | Attribute | Type | Default | Description | |-----------|------|---------|-------------| +| `file` | string | - | External font file path (TTF/OTF). Font nodes with `file` serve as font source declarations. `pagx embed` auto-discovers them, loads the referenced font files, and registers fonts for text shaping. After embed, `file` is preserved for source traceability. | | `unitsPerEm` | int | 1000 | Font design space units; rendering scale = fontSize / unitsPerEm | ### Glyph diff --git a/.codebuddy/skills/pagx/references/cli.md b/.codebuddy/skills/pagx/references/cli.md index b0859eb9c8..a780fa0a5f 100644 --- a/.codebuddy/skills/pagx/references/cli.md +++ b/.codebuddy/skills/pagx/references/cli.md @@ -246,7 +246,7 @@ element, an error is reported. ## pagx font -Font operations with two subcommands: `info` (query metrics) and `embed` (embed into PAGX). +Query font identity and metrics from a font file or system font. (The old `info` and `embed` subcommands have been removed — use `pagx font --file` / `pagx font --name` for font info, and `pagx embed` for font embedding.) ### pagx font info @@ -272,24 +272,34 @@ Returns typeface info (fontFamily, fontStyle, glyphsCount, unitsPerEm, hasColor, and all FontMetrics fields at the specified size (top, ascent, descent, bottom, leading, xMin, xMax, xHeight, capHeight, underlineThickness, underlinePosition). -### pagx font embed +`pagx font embed` was removed in Phase 1. Use `pagx embed` instead. -Embed fonts into a PAGX file by performing text layout and glyph extraction. +--- + +## pagx embed + +Embed font glyphs and images into a PAGX file for self-contained output. Font embedding extracts glyph data from laid-out text; image embedding inlines external image files as base64. Font nodes with a `file` attribute are automatically discovered and registered for text shaping — no `--font-file` flag needed. ```bash -pagx font embed input.pagx -pagx font embed -o out.pagx input.pagx -pagx font embed --file a.ttf --file b.ttf input.pagx -pagx font embed --file a.ttf --fallback "PingFang SC" --fallback b.otf input.pagx +pagx embed input.pagx # embed fonts + images (overwrite) +pagx embed -o out.pagx input.pagx # embed fonts + images to new file +pagx embed --skip-fonts input.pagx # embed images only +pagx embed --skip-images input.pagx # embed fonts only ``` | Option | Description | |--------|-------------| | `-o, --output ` | Output file path (default: overwrite input) | -| `--file ` | Register a font file (can be specified multiple times) | | `--fallback ` | Fallback font file or system font name (can be specified multiple times) | +| `--skip-fonts` | Skip font embedding | +| `--skip-images` | Skip image embedding | +| `-h, --help` | Show this help message | + +Fonts are resolved in the following order: +1. Font nodes with `file` attribute are loaded and registered by their internal family name +2. `--fallback` fonts are tried when a character is not found in the primary font -`--file` and `--fallback` work the same as in `pagx render`. +Image embedding inlines external file references (Image nodes with `filePath`) as base64 data. See the Font `file` attribute in `attributes.md` for details on external font references. --- diff --git a/.codebuddy/skills/pagx/references/guide.md b/.codebuddy/skills/pagx/references/guide.md index 9225aab40e..1fd03186da 100644 --- a/.codebuddy/skills/pagx/references/guide.md +++ b/.codebuddy/skills/pagx/references/guide.md @@ -425,7 +425,7 @@ over `position`. - **Text**: `text`, `fontFamily`, `fontStyle`, `fontSize`, `letterSpacing`. Wrap in TextBox for paragraph features. `fauxBold`/`fauxItalic` for algorithmic styles. ` ` for line breaks. Use `` for XML special characters (e.g., ``). -- **GlyphRun**: Pre-laid-out glyph data with embedded font. Generated by `pagx font embed`, +- **GlyphRun**: Pre-laid-out glyph data with embedded font. Generated by `pagx embed`, not written by hand. ## Painters diff --git a/resources/cli/embed_font_file.pagx b/resources/cli/embed_font_file.pagx new file mode 100644 index 0000000000..429ce630b6 --- /dev/null +++ b/resources/cli/embed_font_file.pagx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index ea697f6d5f..00dfa27482 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -36,6 +36,7 @@ #include "pagx/PAGXDocument.h" #include "pagx/PAGXExporter.h" #include "pagx/PAGXImporter.h" +#include "pagx/nodes/Font.h" #include "pagx/nodes/Image.h" #include "tgfx/core/Bitmap.h" #include "tgfx/core/ImageCodec.h" @@ -3019,22 +3020,85 @@ CLI_TEST(PAGXCliTest, Embed_BothSkipFlags_ExitsWithError) { EXPECT_EQ(contentBefore, contentAfter); } -CLI_TEST(PAGXCliTest, Embed_FontFlags_AcceptedLikeOldSubcommand) { - auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); - auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); - auto outPagx = TempDir() + "/embed_fontflags_out.pagx"; - auto fontPath = ProjectPath::Absolute("resources/font/NotoSansSC-Regular.otf"); - auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--file", fontPath, "-o", outPagx, tempPagx}); +CLI_TEST(PAGXCliTest, Embed_FontFile_AutoRegistersAndEmbeds) { + auto tempPagx = CopyToTemp("embed_font_file.pagx", "embed_font_file.pagx"); + auto fontFile = CopyResourceToTemp("resources/font/NotoSansSC-Regular.otf", + "NotoSansSC-Regular.otf"); + auto outPagx = TempDir() + "/embed_fontfile_out.pagx"; + StreamCapture outCapture(std::cout); + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", outPagx}); EXPECT_EQ(ret, 0); + EXPECT_NE(outCapture.str().find("pagx embed: wrote"), std::string::npos); auto document = pagx::PAGXImporter::FromFile(outPagx); ASSERT_NE(document, nullptr); - bool hasFontNode = false; + bool hasFileFont = false; + bool hasEmbedFont = false; for (auto& node : document->nodes) { if (node->nodeType() == pagx::NodeType::Font) { - hasFontNode = true; + auto* font = static_cast(node.get()); + if (!font->file.empty()) { + hasFileFont = true; + EXPECT_TRUE(font->id == "noto"); + } + if (font->id.find("__embed_font_") == 0) { + hasEmbedFont = true; + } } } - EXPECT_TRUE(hasFontNode); + EXPECT_TRUE(hasFileFont); + EXPECT_TRUE(hasEmbedFont); +} + +CLI_TEST(PAGXCliTest, Embed_FileFlag_RejectedAsUnknown) { + auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); + auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); + StreamCapture errCapture(std::cerr); + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--file", "font.ttf", tempPagx}); + EXPECT_EQ(ret, 1); + EXPECT_NE(errCapture.str().find("pagx embed: unknown option"), std::string::npos); +} + +CLI_TEST(PAGXCliTest, Embed_MissingFontFile_FailsLoud) { + auto tempPagx = CopyToTemp("embed_font_file.pagx", "embed_font_file.pagx"); + auto content = ReadFile(tempPagx); + auto pos = content.find("NotoSansSC-Regular.otf"); + ASSERT_NE(pos, std::string::npos); + content.replace(pos, std::strlen("NotoSansSC-Regular.otf"), "nonexistent_font.otf"); + std::ofstream out(tempPagx); + out << content; + out.close(); + auto outPagx = TempDir() + "/embed_missing_font_out.pagx"; + StreamCapture errCapture(std::cerr); + auto ret = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", outPagx}); + EXPECT_EQ(ret, 1); + EXPECT_NE(errCapture.str().find("pagx embed: failed to load font '"), + std::string::npos); + EXPECT_FALSE(std::filesystem::exists(outPagx)); +} + +CLI_TEST(PAGXCliTest, Embed_FontFile_ReembedPreservesNode) { + auto tempPagx = CopyToTemp("embed_font_file.pagx", "embed_font_file.pagx"); + auto fontFile = CopyResourceToTemp("resources/font/NotoSansSC-Regular.otf", + "NotoSansSC-Regular.otf"); + auto pass1 = TempDir() + "/embed_reembed_pass1.pagx"; + auto pass2 = TempDir() + "/embed_reembed_pass2.pagx"; + auto ret1 = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", pass1}); + EXPECT_EQ(ret1, 0); + auto ret2 = CallRun(pagx::cli::RunEmbed, {"embed", pass1, "-o", pass2}); + EXPECT_EQ(ret2, 0); + auto doc2 = pagx::PAGXImporter::FromFile(pass2); + ASSERT_NE(doc2, nullptr); + bool hasFileFont = false; + for (auto& node : doc2->nodes) { + if (node->nodeType() == pagx::NodeType::Font) { + auto* font = static_cast(node.get()); + if (!font->file.empty()) { + hasFileFont = true; + EXPECT_TRUE(font->id == "noto"); + } + } + } + EXPECT_TRUE(hasFileFont); } CLI_TEST(PAGXCliTest, Embed_AlreadyEmbeddedImage_IsNoOp) { From 68c863e826daa9a013953622f2269c936b936bd1 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 27 May 2026 15:47:57 +0800 Subject: [PATCH 66/87] Fix dangling glyph pointers in ClearEmbeddedGlyphRuns and remove unused test variables. --- src/renderer/FontEmbedder.cpp | 1 + test/src/PAGXCliTest.cpp | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index b519be0f7b..5d44eecf22 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -409,6 +409,7 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { toRemove.insert(glyph->image); } } + font->glyphs.clear(); } else { toRemove.insert(node.get()); for (auto* glyph : font->glyphs) { diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 00dfa27482..9e414ed47a 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -3022,8 +3022,7 @@ CLI_TEST(PAGXCliTest, Embed_BothSkipFlags_ExitsWithError) { CLI_TEST(PAGXCliTest, Embed_FontFile_AutoRegistersAndEmbeds) { auto tempPagx = CopyToTemp("embed_font_file.pagx", "embed_font_file.pagx"); - auto fontFile = CopyResourceToTemp("resources/font/NotoSansSC-Regular.otf", - "NotoSansSC-Regular.otf"); + CopyResourceToTemp("resources/font/NotoSansSC-Regular.otf", "NotoSansSC-Regular.otf"); auto outPagx = TempDir() + "/embed_fontfile_out.pagx"; StreamCapture outCapture(std::cout); auto ret = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", outPagx}); @@ -3078,8 +3077,7 @@ CLI_TEST(PAGXCliTest, Embed_MissingFontFile_FailsLoud) { CLI_TEST(PAGXCliTest, Embed_FontFile_ReembedPreservesNode) { auto tempPagx = CopyToTemp("embed_font_file.pagx", "embed_font_file.pagx"); - auto fontFile = CopyResourceToTemp("resources/font/NotoSansSC-Regular.otf", - "NotoSansSC-Regular.otf"); + CopyResourceToTemp("resources/font/NotoSansSC-Regular.otf", "NotoSansSC-Regular.otf"); auto pass1 = TempDir() + "/embed_reembed_pass1.pagx"; auto pass2 = TempDir() + "/embed_reembed_pass2.pagx"; auto ret1 = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", pass1}); From bafc4ef38efe4442dbc31377e1f31b7453f0fc84 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 27 May 2026 17:28:56 +0800 Subject: [PATCH 67/87] Fix stale comment in ImageEmbedder.h to reflect all-or-nothing semantics. --- src/renderer/ImageEmbedder.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/renderer/ImageEmbedder.h b/src/renderer/ImageEmbedder.h index eeb12bf732..c0b80159b5 100644 --- a/src/renderer/ImageEmbedder.h +++ b/src/renderer/ImageEmbedder.h @@ -29,9 +29,8 @@ namespace pagx { * * URL-form paths (containing "://") are silently skipped; production PAGX does not use them. * - * On the first read failure, embed() returns false and lastErrorPath() identifies the - * offending file. The document may be partially mutated on failure (earlier successful - * reads are already applied); callers must not write the output on failure. + * embed() is all-or-nothing with respect to document state — the document is only + * mutated when all files are successfully read. On failure, no mutations are applied. */ class ImageEmbedder { public: From f3fd086693f8b656532c09d66013fee42bbea6fa Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 27 May 2026 17:29:06 +0800 Subject: [PATCH 68/87] Make IsUrlPath case-insensitive per RFC 3986 Section 3.1. --- src/pagx/PAGXDocument.cpp | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/pagx/PAGXDocument.cpp b/src/pagx/PAGXDocument.cpp index a1294e952a..4653134db1 100644 --- a/src/pagx/PAGXDocument.cpp +++ b/src/pagx/PAGXDocument.cpp @@ -137,9 +137,37 @@ bool PAGXDocument::hasUnresolvedImports() const { } static bool IsUrlPath(const std::string& path) { - // Match known URL schemes rather than generic :// to avoid false positives on Windows paths - // like C://Users/file.png. data: is handled by PAGXImporter before this point. - return path.find("http://") == 0 || path.find("https://") == 0 || path.find("file://") == 0; + // Match known URL schemes (case-insensitive per RFC 3986 Section 3.1) rather than generic :// + // to avoid false positives on Windows paths like C://Users/file.png. + // data: is handled by PAGXImporter before this point. + auto schemeEnd = path.find("://"); + if (schemeEnd == std::string::npos) { + return false; + } + auto scheme = path.substr(0, schemeEnd); + if (scheme.size() == 4) { + if ((scheme[0] == 'h' || scheme[0] == 'H') && + (scheme[1] == 't' || scheme[1] == 'T') && + (scheme[2] == 't' || scheme[2] == 'T') && + (scheme[3] == 'p' || scheme[3] == 'P')) { + return true; + } + if ((scheme[0] == 'f' || scheme[0] == 'F') && + (scheme[1] == 'i' || scheme[1] == 'I') && + (scheme[2] == 'l' || scheme[2] == 'L') && + (scheme[3] == 'e' || scheme[3] == 'E')) { + return true; + } + } else if (scheme.size() == 5) { + if ((scheme[0] == 'h' || scheme[0] == 'H') && + (scheme[1] == 't' || scheme[1] == 'T') && + (scheme[2] == 't' || scheme[2] == 'T') && + (scheme[3] == 'p' || scheme[3] == 'P') && + (scheme[4] == 's' || scheme[4] == 'S')) { + return true; + } + } + return false; } std::vector PAGXDocument::getExternalFilePaths() const { From a954969eb37a27e8a0941cda51074e0ffc7fbfd5 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 27 May 2026 17:29:13 +0800 Subject: [PATCH 69/87] Fix CFMutableDictionaryRef leak on CFSetCreate failure in FindFont. --- src/pagx/SystemFonts.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index b847195bde..25a5f83e72 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -274,6 +274,7 @@ FontLocation SystemFonts::FindFont(const std::string& family, const std::string& CFSetRef mandatoryAttributes = CFSetCreate(kCFAllocatorDefault, mandatoryKeys, mandatoryCount, &kCFTypeSetCallBacks); if (mandatoryAttributes == nullptr) { + CFRelease(attributes); return {}; } CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes(attributes); From edf44122d2b3d5ce136f6d4b9667a02f4d7d16aa Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 27 May 2026 17:29:31 +0800 Subject: [PATCH 70/87] Remove documented --file/--font-file flags from pagx embed per CommandEmbed.cpp. --- cli/npm/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/npm/README.md b/cli/npm/README.md index 6d0940d09e..fb4cf3ac41 100644 --- a/cli/npm/README.md +++ b/cli/npm/README.md @@ -76,8 +76,8 @@ pagx font --list # Embed fonts and images into a PAGX file pagx embed input.pagx -# Embed with a custom font file and fallback (--font-file is an alias for --file) -pagx embed --font-file BrandFont.ttf --fallback "Arial" input.pagx +# Embed with fallback fonts +pagx embed --fallback "Arial" input.pagx # Embed images only (skip font embedding) pagx embed --skip-fonts input.pagx @@ -176,7 +176,6 @@ Embed font glyphs and images into a PAGX file for self-contained output. | Option | Description | |--------|-------------| | `-o, --output ` | Output file path (default: overwrite input) | -| `--file, --font-file ` | Register a font file (repeatable) | | `--fallback ` | Add a fallback font file or system font name (repeatable) | | `--skip-fonts` | Skip font embedding | | `--skip-images` | Skip image embedding | From 21292745b5d2336feb796ce7c9aebeeae6b4953e Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 28 May 2026 16:33:51 +0800 Subject: [PATCH 71/87] Remove unused tempPng copy from Embed_FileFlag_RejectedAsUnknown that never reaches load. --- test/src/PAGXCliTest.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 9e414ed47a..1767d6d62c 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -3050,7 +3050,6 @@ CLI_TEST(PAGXCliTest, Embed_FontFile_AutoRegistersAndEmbeds) { CLI_TEST(PAGXCliTest, Embed_FileFlag_RejectedAsUnknown) { auto tempPagx = CopyToTemp("embed_sample.pagx", "embed_sample.pagx"); - auto tempPng = CopyResourceToTemp("resources/apitest/image_as_mask.png", "image_as_mask.png"); StreamCapture errCapture(std::cerr); auto ret = CallRun(pagx::cli::RunEmbed, {"embed", "--file", "font.ttf", tempPagx}); EXPECT_EQ(ret, 1); @@ -3070,8 +3069,7 @@ CLI_TEST(PAGXCliTest, Embed_MissingFontFile_FailsLoud) { StreamCapture errCapture(std::cerr); auto ret = CallRun(pagx::cli::RunEmbed, {"embed", tempPagx, "-o", outPagx}); EXPECT_EQ(ret, 1); - EXPECT_NE(errCapture.str().find("pagx embed: failed to load font '"), - std::string::npos); + EXPECT_NE(errCapture.str().find("pagx embed: failed to load font '"), std::string::npos); EXPECT_FALSE(std::filesystem::exists(outPagx)); } From 7c71e51ee62cfdce89863c689c040ac73161d43a Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 28 May 2026 16:33:56 +0800 Subject: [PATCH 72/87] Lift duplicated glyph-removal loop in ClearEmbeddedGlyphRuns above the file-attribute branch. --- src/renderer/FontEmbedder.cpp | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index 5d44eecf22..54f97ce812 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -399,28 +399,19 @@ void FontEmbedder::ClearEmbeddedGlyphRuns(PAGXDocument* document) { auto type = node->nodeType(); if (type == NodeType::Font) { auto* font = static_cast(node.get()); - if (!font->file.empty()) { - for (auto* glyph : font->glyphs) { - toRemove.insert(glyph); - if (glyph->path != nullptr) { - toRemove.insert(glyph->path); - } - if (glyph->image != nullptr) { - toRemove.insert(glyph->image); - } + for (auto* glyph : font->glyphs) { + toRemove.insert(glyph); + if (glyph->path != nullptr) { + toRemove.insert(glyph->path); } + if (glyph->image != nullptr) { + toRemove.insert(glyph->image); + } + } + if (!font->file.empty()) { font->glyphs.clear(); } else { toRemove.insert(node.get()); - for (auto* glyph : font->glyphs) { - toRemove.insert(glyph); - if (glyph->path != nullptr) { - toRemove.insert(glyph->path); - } - if (glyph->image != nullptr) { - toRemove.insert(glyph->image); - } - } } } else if (type == NodeType::GlyphRun) { toRemove.insert(node.get()); From ef24936f2a841dc0688659c18b5e5125ded7d705 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 28 May 2026 16:34:19 +0800 Subject: [PATCH 73/87] Update Font::file doc-comment to match the embed flow that preserves the source node. --- include/pagx/nodes/Font.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/include/pagx/nodes/Font.h b/include/pagx/nodes/Font.h index b9e62dfc00..35251672f8 100644 --- a/include/pagx/nodes/Font.h +++ b/include/pagx/nodes/Font.h @@ -78,9 +78,11 @@ class Font : public Node { int unitsPerEm = 1000; /** - * Path to an external font file. When set, glyph data is extracted from this file during embed - * instead of being read from Glyph children. The path is resolved relative to the PAGX file's - * directory. An empty string means no external reference. + * Path to an external font file. When set, this Font node serves as a font source declaration: + * `pagx embed` loads and registers the referenced font for text shaping. Extracted glyph data + * is stored in separate Font nodes (the source node's `glyphs` is preserved as empty across + * embed). The path is resolved relative to the PAGX file's directory. An empty string means + * no external reference. */ std::string file = {}; From d1f6586476068c4e5258cb35549ed3cfacf19eb3 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 28 May 2026 16:34:35 +0800 Subject: [PATCH 74/87] Rewrite pagx font cli reference to match the flat command and drop retired info subsection. --- .codebuddy/skills/pagx/references/cli.md | 30 +++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/.codebuddy/skills/pagx/references/cli.md b/.codebuddy/skills/pagx/references/cli.md index a780fa0a5f..523c503ff3 100644 --- a/.codebuddy/skills/pagx/references/cli.md +++ b/.codebuddy/skills/pagx/references/cli.md @@ -246,33 +246,35 @@ element, an error is reported. ## pagx font -Query font identity and metrics from a font file or system font. (The old `info` and `embed` subcommands have been removed — use `pagx font --file` / `pagx font --name` for font info, and `pagx embed` for font embedding.) - -### pagx font info - -Query font identity and metrics from a font file or system font. +Query font identity and metrics from a font file or system font, or enumerate installed +system font families. ```bash -pagx font info --file ./CustomFont.ttf -pagx font info --file ./CustomFont.ttf --size 24 -pagx font info --name "PingFang SC,Bold" -pagx font info --name "Arial" --size 24 --json +pagx font --file ./CustomFont.ttf +pagx font --file ./CustomFont.ttf --size 24 +pagx font --name "PingFang SC,Bold" +pagx font --name "Arial" --size 24 --json +pagx font --list +pagx font --list --json ``` | Option | Description | |--------|-------------| -| `--file ` | Font file path | -| `--name ` | System font by name (e.g., `"Arial"` or `"Arial,Bold"`) | +| `--file ` | Query a font file | +| `--name ` | Query a system font (e.g., `"Arial"` or `"Arial,Bold"`) | | `--size ` | Font size in points (default: 12, the PAGX spec default) | -| `--json` | JSON output | +| `--json` | Output in JSON format | +| `--list` | List every installed system font family | -Either `--file` or `--name` is required (mutually exclusive). +Exactly one of `--file`, `--name`, or `--list` is required. `--list` cannot be combined +with `--file` or `--name`; `--file` and `--name` are mutually exclusive. Returns typeface info (fontFamily, fontStyle, glyphsCount, unitsPerEm, hasColor, hasOutlines) and all FontMetrics fields at the specified size (top, ascent, descent, bottom, leading, xMin, xMax, xHeight, capHeight, underlineThickness, underlinePosition). -`pagx font embed` was removed in Phase 1. Use `pagx embed` instead. +The retired `pagx font info` and `pagx font embed` subcommands now error out with a redirect +message; use `pagx font ...` for font queries and `pagx embed` for font embedding. --- From 815d1fa8f03184ec726587a6f3c738ffb89d234c Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 28 May 2026 16:35:20 +0800 Subject: [PATCH 75/87] Extract ResolveRelativePath helper to deduplicate Image and Font path resolution. --- src/pagx/PAGXImporter.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pagx/PAGXImporter.cpp b/src/pagx/PAGXImporter.cpp index 9e9d764ad3..ec83b287c5 100644 --- a/src/pagx/PAGXImporter.cpp +++ b/src/pagx/PAGXImporter.cpp @@ -2113,6 +2113,13 @@ static Color GetColorAttribute(const DOMNode* node, const char* name, PAGXDocume // Public API implementation //============================================================================== +static void ResolveRelativePath(const std::string& basePath, std::string& path) { + if (path.empty() || path[0] == '/' || path.find("://") != std::string::npos) { + return; + } + path = basePath + path; +} + std::shared_ptr PAGXImporter::FromFile(const std::string& filePath) { std::ifstream file(filePath, std::ios::binary | std::ios::ate); if (!file) { @@ -2141,17 +2148,11 @@ std::shared_ptr PAGXImporter::FromFile(const std::string& filePath 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; - } + ResolveRelativePath(basePath, image->filePath); } if (node->nodeType() == NodeType::Font) { auto* font = static_cast(node.get()); - if (!font->file.empty() && font->file[0] != '/' && - font->file.find("://") == std::string::npos) { - font->file = basePath + font->file; - } + ResolveRelativePath(basePath, font->file); } } } From 6e4f513ec74a92d8a0c2daf64a4ccac0b8a0851e Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 29 May 2026 16:50:47 +0800 Subject: [PATCH 76/87] Add missing StreamCapture RAII helper used by Font file CLI tests. --- test/src/PAGXCliTest.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 1767d6d62c..c71f18fe39 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -113,6 +113,30 @@ static std::string ReadFile(const std::string& path) { return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; } +// RAII helper that redirects an ostream (e.g. std::cout, std::cerr) into an internal +// stringstream for the lifetime of the instance and restores the original buffer on +// destruction. Use str() to read what was captured. +class StreamCapture { + public: + explicit StreamCapture(std::ostream& stream) : stream_(stream), oldBuf_(stream.rdbuf()) { + stream_.rdbuf(captured_.rdbuf()); + } + ~StreamCapture() { + stream_.rdbuf(oldBuf_); + } + StreamCapture(const StreamCapture&) = delete; + StreamCapture& operator=(const StreamCapture&) = delete; + + std::string str() const { + return captured_.str(); + } + + private: + std::ostream& stream_; + std::streambuf* oldBuf_; + std::stringstream captured_; +}; + static std::string ExportToSVG(const std::string& pagxResourceName, const std::string& svgTempName, std::vector extraExportArgs = {}) { auto pagxPath = TestResourcePath(pagxResourceName); From 38eb8f482f33c166f8cef2c6e40a419b08c18ea7 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 29 May 2026 16:57:05 +0800 Subject: [PATCH 77/87] Run clang-format on PAGXDocument.cpp to satisfy project style. --- src/pagx/PAGXDocument.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/pagx/PAGXDocument.cpp b/src/pagx/PAGXDocument.cpp index 4653134db1..a2eff8e7b1 100644 --- a/src/pagx/PAGXDocument.cpp +++ b/src/pagx/PAGXDocument.cpp @@ -146,23 +146,17 @@ static bool IsUrlPath(const std::string& path) { } auto scheme = path.substr(0, schemeEnd); if (scheme.size() == 4) { - if ((scheme[0] == 'h' || scheme[0] == 'H') && - (scheme[1] == 't' || scheme[1] == 'T') && - (scheme[2] == 't' || scheme[2] == 'T') && - (scheme[3] == 'p' || scheme[3] == 'P')) { + if ((scheme[0] == 'h' || scheme[0] == 'H') && (scheme[1] == 't' || scheme[1] == 'T') && + (scheme[2] == 't' || scheme[2] == 'T') && (scheme[3] == 'p' || scheme[3] == 'P')) { return true; } - if ((scheme[0] == 'f' || scheme[0] == 'F') && - (scheme[1] == 'i' || scheme[1] == 'I') && - (scheme[2] == 'l' || scheme[2] == 'L') && - (scheme[3] == 'e' || scheme[3] == 'E')) { + if ((scheme[0] == 'f' || scheme[0] == 'F') && (scheme[1] == 'i' || scheme[1] == 'I') && + (scheme[2] == 'l' || scheme[2] == 'L') && (scheme[3] == 'e' || scheme[3] == 'E')) { return true; } } else if (scheme.size() == 5) { - if ((scheme[0] == 'h' || scheme[0] == 'H') && - (scheme[1] == 't' || scheme[1] == 'T') && - (scheme[2] == 't' || scheme[2] == 'T') && - (scheme[3] == 'p' || scheme[3] == 'P') && + if ((scheme[0] == 'h' || scheme[0] == 'H') && (scheme[1] == 't' || scheme[1] == 'T') && + (scheme[2] == 't' || scheme[2] == 'T') && (scheme[3] == 'p' || scheme[3] == 'P') && (scheme[4] == 's' || scheme[4] == 'S')) { return true; } From a7c64a0407a5a6778b0d46d560cb0dc897d677ef Mon Sep 17 00:00:00 2001 From: codywwang Date: Mon, 1 Jun 2026 19:16:14 +0800 Subject: [PATCH 78/87] Fix Windows font path resolution and path comments. --- include/pagx/PAGXDocument.h | 2 +- src/pagx/SystemFonts.cpp | 60 +++++++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/include/pagx/PAGXDocument.h b/include/pagx/PAGXDocument.h index 6324de8a3d..64fe1a7963 100644 --- a/include/pagx/PAGXDocument.h +++ b/include/pagx/PAGXDocument.h @@ -115,7 +115,7 @@ class PAGXDocument : public Node { /** * Returns a list of external file paths referenced by Image nodes that have no embedded data. - * Data URIs (paths starting with "data:") are excluded. + * URL-form paths (http://, https://, and file://) are excluded. */ std::vector getExternalFilePaths() const; diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index 25a5f83e72..d5aefc0728 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -380,6 +380,49 @@ static std::string GetFaceName(IDWriteFont* font) { return result; } +static std::string GetFilePath(IDWriteFontFile* fontFile) { + if (fontFile == nullptr) { + return {}; + } + + IDWriteFontFileLoader* loader = nullptr; + HRESULT hr = fontFile->GetLoader(&loader); + if (FAILED(hr) || loader == nullptr) { + return {}; + } + + IDWriteLocalFontFileLoader* localLoader = nullptr; + hr = loader->QueryInterface(__uuidof(IDWriteLocalFontFileLoader), + reinterpret_cast(&localLoader)); + SafeRelease(&loader); + if (FAILED(hr) || localLoader == nullptr) { + return {}; + } + + const void* refKey = nullptr; + UINT32 refKeySize = 0; + hr = fontFile->GetReferenceKey(&refKey, &refKeySize); + if (FAILED(hr) || refKey == nullptr) { + SafeRelease(&localLoader); + return {}; + } + + UINT32 length = 0; + hr = localLoader->GetFilePathLengthFromKey(refKey, refKeySize, &length); + if (FAILED(hr) || length == 0) { + SafeRelease(&localLoader); + return {}; + } + + std::wstring wide(static_cast(length) + 1, L'\0'); + hr = localLoader->GetFilePathFromKey(refKey, refKeySize, wide.data(), length + 1); + SafeRelease(&localLoader); + if (FAILED(hr)) { + return {}; + } + return WideToUTF8(wide.c_str(), static_cast(length)); +} + std::vector SystemFonts::FallbackTypefaces() { IDWriteFactory* factory = nullptr; HRESULT hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), @@ -602,17 +645,16 @@ FontLocation SystemFonts::FindFont(const std::string& family, const std::string& return {}; } - const void* refKey = nullptr; - UINT32 refKeySize = 0; - hr = fontFile->GetReferenceKey(&refKey, &refKeySize); - FontLocation location = {}; - // GetReferenceKey includes the null terminator in the byte count. - int keyLen = static_cast(refKeySize / sizeof(wchar_t)); - if (SUCCEEDED(hr) && refKey != nullptr && keyLen > 1) { - location.path = WideToUTF8(static_cast(refKey), keyLen - 1); - } + location.path = GetFilePath(fontFile); SafeRelease(&fontFile); + if (location.path.empty()) { + SafeRelease(&fontFace); + SafeRelease(&fontFamily); + SafeRelease(&fontCollection); + SafeRelease(&factory); + return {}; + } location.ttcIndex = static_cast(fontFace->GetIndex()); location.fontFamily = GetFamilyName(fontFamily); From 17e370f6e40e997913f199349bec3786fb5a1a26 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 3 Jun 2026 15:39:59 +0800 Subject: [PATCH 79/87] Remove duplicated MakeStandaloneParams in Text and reuse the one from ExporterUtils. --- src/pagx/nodes/Text.cpp | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/pagx/nodes/Text.cpp b/src/pagx/nodes/Text.cpp index 24e0220a1d..431da70760 100644 --- a/src/pagx/nodes/Text.cpp +++ b/src/pagx/nodes/Text.cpp @@ -20,6 +20,7 @@ #include "pagx/TextLayout.h" #include "pagx/TextLayoutParams.h" #include "pagx/nodes/LayoutNode.h" +#include "pagx/utils/ExporterUtils.h" namespace pagx { @@ -30,23 +31,6 @@ Text::~Text() { Text::Text() : glyphData(new GlyphData()) { } -static TextLayoutParams MakeStandaloneParams(const Text* text) { - TextLayoutParams params = {}; - params.baseline = text->baseline; - switch (text->textAnchor) { - case TextAnchor::Start: - params.textAlign = TextAlign::Start; - break; - case TextAnchor::Center: - params.textAlign = TextAlign::Center; - break; - case TextAnchor::End: - params.textAlign = TextAlign::End; - break; - } - return params; -} - void Text::onMeasure(LayoutContext* context) { textScale = 1.0f; auto params = MakeStandaloneParams(this); From 6fa0fd711f16edd456694de57bf18bb412e60c8f Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 3 Jun 2026 15:48:44 +0800 Subject: [PATCH 80/87] Add missing Font file import round-trip test for Phase 3 plan 03. --- test/src/PAGXCliTest.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index c71f18fe39..3e49e34a9a 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -3080,6 +3080,26 @@ CLI_TEST(PAGXCliTest, Embed_FileFlag_RejectedAsUnknown) { EXPECT_NE(errCapture.str().find("pagx embed: unknown option"), std::string::npos); } +CLI_TEST(PAGXCliTest, Embed_FontFileImport_RoundTrips) { + // Import a PAGX with Font(file) — verify file path survives import without existence check + // (D-05: no file existence check during import). + auto path = TestResourcePath("embed_font_file.pagx"); + auto document = pagx::PAGXImporter::FromFile(path); + ASSERT_NE(document, nullptr); + bool foundFileFont = false; + for (auto& node : document->nodes) { + if (node->nodeType() == pagx::NodeType::Font) { + auto* font = static_cast(node.get()); + if (!font->file.empty()) { + foundFileFont = true; + EXPECT_NE(font->file.find("NotoSansSC-Regular"), std::string::npos) + << "Font::file should contain the resolved font filename"; + } + } + } + EXPECT_TRUE(foundFileFont); +} + CLI_TEST(PAGXCliTest, Embed_MissingFontFile_FailsLoud) { auto tempPagx = CopyToTemp("embed_font_file.pagx", "embed_font_file.pagx"); auto content = ReadFile(tempPagx); From b90fe4b0098c6e42b2288f71db2d8f63676dc855 Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 3 Jun 2026 21:33:03 +0800 Subject: [PATCH 81/87] Make removeNodes, setNodeId and resetLayoutState private with FontEmbedder friend. --- include/pagx/PAGXDocument.h | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/include/pagx/PAGXDocument.h b/include/pagx/PAGXDocument.h index 64fe1a7963..32f43fbdb0 100644 --- a/include/pagx/PAGXDocument.h +++ b/include/pagx/PAGXDocument.h @@ -141,8 +141,8 @@ class PAGXDocument : public Node { /** * Executes auto layout on the document, positioning layers according to their layout * constraints. Must be called before rendering or font embedding. Repeated calls are safe - * only after calling ClearEmbeddedGlyphRuns + resetLayoutState(), which clears embedded - * glyph data and resets the layout flag so layout re-runs with fresh shaping data. + * only after calling clearEmbed(), which clears embedded glyph data and resets the layout + * flag so layout re-runs with fresh shaping data. * @param fontConfig Optional font config for text measurement and rendering. When provided, * updates the internal config before layout. Pass nullptr to use the * previously set config (or no config). @@ -171,28 +171,6 @@ class PAGXDocument : public Node { */ void clearEmbed(); - /** - * Removes the specified nodes from the document and cleans up their nodeMap entries. - * Node pointers in the set and any pointers derived from them (e.g. child glyphs) are - * invalidated after this call. Callers must first collect all affected nodes to remove - * before calling. - */ - void removeNodes(const std::unordered_set& nodesToRemove); - - /** - * Assigns or changes the ID of an existing node. If the new ID already exists in the - * document, the old entry is replaced. If the ID is empty, the node is removed from the - * lookup index. The node must already be managed by this document. - */ - void setNodeId(Node* node, const std::string& id); - - /** - * Resets the layout-applied flag to allow applyLayout() to be called again. Must be paired - * with ClearEmbeddedGlyphRuns before re-embedding to ensure layout re-runs with fresh - * shaping data. - */ - void resetLayoutState(); - NodeType nodeType() const override { return NodeType::Document; } @@ -208,6 +186,11 @@ class PAGXDocument : public Node { bool layoutApplied = false; std::unordered_map nodeMap = {}; + void removeNodes(const std::unordered_set& nodesToRemove); + void setNodeId(Node* node, const std::string& id); + void resetLayoutState(); + + friend class FontEmbedder; friend class PAGXImporter; friend class PAGXExporter; friend class TextLayoutContext; From 890fb042016cebc5439d1461b9667c36a6d99f4b Mon Sep 17 00:00:00 2001 From: codywwang Date: Wed, 3 Jun 2026 21:54:49 +0800 Subject: [PATCH 82/87] Error when --skip-fonts and --fallback are both set in embed command. --- src/cli/CommandEmbed.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp index b98286fe06..3de9fab49c 100644 --- a/src/cli/CommandEmbed.cpp +++ b/src/cli/CommandEmbed.cpp @@ -102,6 +102,10 @@ int RunEmbed(int argc, char* argv[]) { std::cerr << "pagx embed: --skip-fonts and --skip-images cannot both be set\n"; return 1; } + if (options.skipFonts && !options.fallbacks.empty()) { + std::cerr << "pagx embed: --skip-fonts and --fallback cannot both be set\n"; + return 1; + } auto document = LoadDocument(options.inputFile, "pagx embed"); if (document == nullptr) { From 1cd6eac1086b0cc696700dc3276d5ee6d7985073 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 4 Jun 2026 11:25:53 +0800 Subject: [PATCH 83/87] Fix IsUrlPath to also match data: URI scheme. --- src/pagx/PAGXDocument.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pagx/PAGXDocument.cpp b/src/pagx/PAGXDocument.cpp index a2eff8e7b1..3a0ffbb61e 100644 --- a/src/pagx/PAGXDocument.cpp +++ b/src/pagx/PAGXDocument.cpp @@ -139,7 +139,12 @@ bool PAGXDocument::hasUnresolvedImports() const { static bool IsUrlPath(const std::string& path) { // Match known URL schemes (case-insensitive per RFC 3986 Section 3.1) rather than generic :// // to avoid false positives on Windows paths like C://Users/file.png. - // data: is handled by PAGXImporter before this point. + // data: URIs have no "://" so they are checked separately. + if (path.size() >= 5 && (path[0] == 'd' || path[0] == 'D') && + (path[1] == 'a' || path[1] == 'A') && (path[2] == 't' || path[2] == 'T') && + (path[3] == 'a' || path[3] == 'A') && path[4] == ':') { + return true; + } auto schemeEnd = path.find("://"); if (schemeEnd == std::string::npos) { return false; From 54f6708020ef962aa7814475c0c37802cb6382e8 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 4 Jun 2026 11:36:51 +0800 Subject: [PATCH 84/87] Preserve Font file original path across import/export round-trip. --- include/pagx/nodes/Font.h | 11 +++++++++++ src/pagx/PAGXExporter.cpp | 3 ++- src/pagx/PAGXImporter.cpp | 3 +++ test/src/PAGXCliTest.cpp | 11 +++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/include/pagx/nodes/Font.h b/include/pagx/nodes/Font.h index 35251672f8..12f07a166b 100644 --- a/include/pagx/nodes/Font.h +++ b/include/pagx/nodes/Font.h @@ -83,9 +83,20 @@ class Font : public Node { * is stored in separate Font nodes (the source node's `glyphs` is preserved as empty across * embed). The path is resolved relative to the PAGX file's directory. An empty string means * no external reference. + * + * After PAGXImporter::FromFile() resolves relative paths to absolute paths for runtime use, + * the original verbatim string from the XML attribute is preserved here so that PAGXExporter + * can round-trip the authored path (relative or URL) unchanged. */ std::string file = {}; + /** + * The verbatim `file` attribute value as it appeared in the source XML, before any path + * resolution. PAGXExporter writes this value when non-empty, ensuring that relative paths + * authored by the user are preserved after a round-trip through embed and re-export. + */ + std::string fileOriginal = {}; + /** * The list of glyphs in this font. GlyphID is the index + 1 (GlyphID 0 is reserved for missing * glyph). diff --git a/src/pagx/PAGXExporter.cpp b/src/pagx/PAGXExporter.cpp index 607102378e..3b31a19541 100644 --- a/src/pagx/PAGXExporter.cpp +++ b/src/pagx/PAGXExporter.cpp @@ -1005,7 +1005,8 @@ static void WriteResource(XMLBuilder& xml, const Node* node, const Options& opti xml.addAttribute("id", font->id); xml.addAttribute("unitsPerEm", font->unitsPerEm, Default().unitsPerEm); if (!font->file.empty()) { - xml.addAttribute("file", font->file); + const std::string& fileAttr = !font->fileOriginal.empty() ? font->fileOriginal : font->file; + xml.addAttribute("file", fileAttr); } WriteCustomData(xml, node); if (font->glyphs.empty()) { diff --git a/src/pagx/PAGXImporter.cpp b/src/pagx/PAGXImporter.cpp index ec83b287c5..f2487f3086 100644 --- a/src/pagx/PAGXImporter.cpp +++ b/src/pagx/PAGXImporter.cpp @@ -2152,6 +2152,9 @@ std::shared_ptr PAGXImporter::FromFile(const std::string& filePath } if (node->nodeType() == NodeType::Font) { auto* font = static_cast(node.get()); + if (!font->file.empty()) { + font->fileOriginal = font->file; + } ResolveRelativePath(basePath, font->file); } } diff --git a/test/src/PAGXCliTest.cpp b/test/src/PAGXCliTest.cpp index 3e49e34a9a..3f719b6303 100644 --- a/test/src/PAGXCliTest.cpp +++ b/test/src/PAGXCliTest.cpp @@ -699,8 +699,13 @@ CLI_TEST(PAGXCliTest, FontList_MutualExclusive) { } CLI_TEST(PAGXCliTest, Font_UnknownSubcommand) { + std::streambuf* oldCerr = std::cerr.rdbuf(); + std::ostringstream capturedErr; + std::cerr.rdbuf(capturedErr.rdbuf()); auto ret = CallRun(pagx::cli::RunFont, {"font", "xyz"}); + std::cerr.rdbuf(oldCerr); EXPECT_NE(ret, 0); + EXPECT_NE(capturedErr.str().find("pagx font: unknown subcommand 'xyz'"), std::string::npos); } CLI_TEST(PAGXCliTest, FontEmbed_Retired_PrintsRedirectError) { @@ -3062,6 +3067,9 @@ CLI_TEST(PAGXCliTest, Embed_FontFile_AutoRegistersAndEmbeds) { if (!font->file.empty()) { hasFileFont = true; EXPECT_TRUE(font->id == "noto"); + // fileOriginal should be preserved; the resolved absolute path must not be written out. + EXPECT_EQ(font->fileOriginal, "NotoSansSC-Regular.otf") + << "fileOriginal should retain the verbatim relative path from the source XML"; } if (font->id.find("__embed_font_") == 0) { hasEmbedFont = true; @@ -3135,6 +3143,9 @@ CLI_TEST(PAGXCliTest, Embed_FontFile_ReembedPreservesNode) { if (!font->file.empty()) { hasFileFont = true; EXPECT_TRUE(font->id == "noto"); + // fileOriginal must survive the second embed round-trip unchanged. + EXPECT_EQ(font->fileOriginal, "NotoSansSC-Regular.otf") + << "fileOriginal must not become absolute after a second embed round-trip"; } } } From 9d07c41c56b06990f764cb9ad877181dcc4528c4 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 4 Jun 2026 14:39:06 +0800 Subject: [PATCH 85/87] Unify atomic write in WriteStringToFile, fix font ID collision, remove FC_SCALABLE filter. --- spec/samples/app_icons.pagx | 122 +++++++++++++++++++++------------- src/cli/CliUtils.cpp | 29 +++++--- src/cli/CommandEmbed.cpp | 29 -------- src/cli/CommandFont.cpp | 2 +- src/pagx/SystemFonts.cpp | 1 - src/renderer/FontEmbedder.cpp | 15 +++-- 6 files changed, 109 insertions(+), 89 deletions(-) diff --git a/spec/samples/app_icons.pagx b/spec/samples/app_icons.pagx index 649604d6b4..fdc4d15917 100644 --- a/spec/samples/app_icons.pagx +++ b/spec/samples/app_icons.pagx @@ -1,13 +1,9 @@ - - - - @@ -35,12 +31,7 @@ - - - - - @@ -51,11 +42,12 @@ - + + + - @@ -69,11 +61,12 @@ - + + + - @@ -91,11 +84,12 @@ - + + + - @@ -105,13 +99,13 @@ - + + + - - @@ -128,16 +122,17 @@ - + + + - - + @@ -151,11 +146,12 @@ - + + + - @@ -173,11 +169,12 @@ - + + + - @@ -192,38 +189,41 @@ - + + + - - - - - + + + - + - + + + - + + + - @@ -231,22 +231,23 @@ - + - + - + + + - @@ -261,11 +262,12 @@ - + + + - @@ -282,24 +284,52 @@ - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/src/cli/CliUtils.cpp b/src/cli/CliUtils.cpp index 83840e746e..caa1bb04f6 100644 --- a/src/cli/CliUtils.cpp +++ b/src/cli/CliUtils.cpp @@ -17,6 +17,8 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "cli/CliUtils.h" +#include +#include #include #include #include "pagx/PAGXImporter.h" @@ -64,15 +66,26 @@ bool LoadFontConfig(FontConfig* fontConfig, const std::vector& font bool WriteStringToFile(const std::string& content, const std::string& filePath, const std::string& command) { - std::ofstream out(filePath); - if (!out.is_open()) { - std::cerr << command << ": failed to write '" << filePath << "'\n"; - return false; + auto tempPath = filePath + ".tmp"; + { + std::ofstream out(tempPath); + if (!out.is_open()) { + std::cerr << command << ": failed to write '" << tempPath << "'\n"; + return false; + } + out << content; + out.close(); + if (out.fail()) { + std::cerr << command << ": error writing to '" << tempPath << "'\n"; + std::remove(tempPath.c_str()); + return false; + } } - out << content; - out.close(); - if (out.fail()) { - std::cerr << command << ": error writing to '" << filePath << "'\n"; + std::error_code ec; + std::filesystem::rename(tempPath, filePath, ec); + if (ec) { + std::cerr << command << ": failed to replace '" << filePath << "'\n"; + std::remove(tempPath.c_str()); return false; } std::cout << command << ": wrote " << filePath << "\n"; diff --git a/src/cli/CommandEmbed.cpp b/src/cli/CommandEmbed.cpp index 3de9fab49c..87bfe5411f 100644 --- a/src/cli/CommandEmbed.cpp +++ b/src/cli/CommandEmbed.cpp @@ -17,9 +17,6 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "cli/CommandEmbed.h" -#include -#include -#include #include #include #include @@ -152,32 +149,6 @@ int RunEmbed(int argc, char* argv[]) { } auto xml = PAGXExporter::ToXML(*document); - if (options.outputFile == options.inputFile) { - auto tempPath = options.outputFile + ".tmp"; - { - std::ofstream out(tempPath); - if (!out.is_open()) { - std::cerr << "pagx embed: failed to write '" << tempPath << "'\n"; - return 1; - } - out << xml; - out.close(); - if (out.fail()) { - std::cerr << "pagx embed: error writing to '" << tempPath << "'\n"; - std::remove(tempPath.c_str()); - return 1; - } - } - std::error_code ec; - std::filesystem::rename(tempPath, options.outputFile, ec); - if (ec) { - std::cerr << "pagx embed: failed to replace '" << options.outputFile << "'\n"; - std::remove(tempPath.c_str()); - return 1; - } - std::cout << "pagx embed: wrote " << options.outputFile << "\n"; - return 0; - } if (!WriteStringToFile(xml, options.outputFile, "pagx embed")) { return 1; } diff --git a/src/cli/CommandFont.cpp b/src/cli/CommandFont.cpp index 162b23b956..7ab02ac8b3 100644 --- a/src/cli/CommandFont.cpp +++ b/src/cli/CommandFont.cpp @@ -139,7 +139,7 @@ int RunFont(int argc, char* argv[]) { std::cerr << "pagx font: unknown option '" << arg << "'\n"; return 1; } else { - std::cerr << "pagx font: unexpected argument '" << arg << "'\n"; + std::cerr << "pagx font: unknown subcommand '" << arg << "'\n"; return 1; } i++; diff --git a/src/pagx/SystemFonts.cpp b/src/pagx/SystemFonts.cpp index d5aefc0728..afa95ef8a8 100644 --- a/src/pagx/SystemFonts.cpp +++ b/src/pagx/SystemFonts.cpp @@ -817,7 +817,6 @@ FontLocation SystemFonts::FindFont(const std::string& family, const std::string& if (!style.empty()) { FcPatternAddString(pattern, FC_STYLE, reinterpret_cast(style.c_str())); } - FcPatternAddBool(pattern, FC_SCALABLE, FcTrue); FcConfigSubstitute(nullptr, pattern, FcMatchPattern); FcDefaultSubstitute(pattern); diff --git a/src/renderer/FontEmbedder.cpp b/src/renderer/FontEmbedder.cpp index 54f97ce812..1343676653 100644 --- a/src/renderer/FontEmbedder.cpp +++ b/src/renderer/FontEmbedder.cpp @@ -472,11 +472,18 @@ bool FontEmbedder::embed(PAGXDocument* document) { } } - // Assign sequential IDs to all embedded fonts using a reserved prefix to avoid - // collisions with user-created nodes. + // Assign sequential IDs to all embedded fonts. Skip any index already taken by a user node to + // avoid silently displacing it from the nodeMap. int fontIndex = 1; + auto nextEmbedFontId = [&]() -> std::string { + std::string id; + do { + id = "__embed_font_" + std::to_string(fontIndex++); + } while (document->findNode(id) != nullptr); + return id; + }; if (vectorBuilder.font != nullptr) { - document->setNodeId(vectorBuilder.font, "__embed_font_" + std::to_string(fontIndex++)); + document->setNodeId(vectorBuilder.font, nextEmbedFontId()); } for (auto* typeface : bitmapTypefaces) { if (typeface == nullptr) { @@ -484,7 +491,7 @@ bool FontEmbedder::embed(PAGXDocument* document) { } auto builderIt = bitmapBuilders.find(typeface); if (builderIt != bitmapBuilders.end() && builderIt->second.font != nullptr) { - document->setNodeId(builderIt->second.font, "__embed_font_" + std::to_string(fontIndex++)); + document->setNodeId(builderIt->second.font, nextEmbedFontId()); } } From e738016b4da8dca3179d6d8fdf18eab288315267 Mon Sep 17 00:00:00 2001 From: codywwang Date: Thu, 4 Jun 2026 17:44:12 +0800 Subject: [PATCH 86/87] Update screenshot baseline for layout_flex_weights test. --- test/baseline/version.json | 1 + 1 file changed, 1 insertion(+) diff --git a/test/baseline/version.json b/test/baseline/version.json index e284297125..18c1a60556 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8724,6 +8724,7 @@ "layout_container_hug": "42abc278f", "layout_container_vertical": "b4fef60e9", "layout_flex": "b4fef60e9", + "layout_flex_weights": "d31df930a", "layout_padding_unified": "720d735b", "layout_textbox_constraint_wrap": "69357df37", "layout_textbox_explicit_height": "3bab0356d", From 1d4e909c684cfdabda8133b5dc98724854c7f627 Mon Sep 17 00:00:00 2001 From: codywwang Date: Fri, 5 Jun 2026 16:29:25 +0800 Subject: [PATCH 87/87] Update screenshot baseline for spec app_icons game_hud glyph_run and text_box tests. --- test/baseline/version.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/baseline/version.json b/test/baseline/version.json index 18c1a60556..02f419cfb6 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8753,7 +8753,7 @@ "skills_star_badge": "912722813", "skills_tab_bar": "eb9361216", "skills_text_decoration": "e593ba6e8", - "spec_app_icons": "451bc13e", + "spec_app_icons": "e738016b4", "spec_clip_to_bounds": "b4fef60e9", "spec_color_source_coordinates": "625e411a", "spec_complete_example": "451bc13e", @@ -8769,8 +8769,8 @@ "spec_document_structure": "f1d07bfc7", "spec_ellipse": "f1d07bfc7", "spec_fill": "f1d07bfc7", - "spec_game_hud": "eb9361216", - "spec_glyph_run": "912722813", + "spec_game_hud": "e738016b4", + "spec_glyph_run": "e738016b4", "spec_group": "f1d07bfc7", "spec_group_isolation": "625e411a", "spec_group_propagation": "625e411a", @@ -8797,7 +8797,7 @@ "spec_space_explorer": "eb9361216", "spec_stroke": "9c8fb076", "spec_text": "4d8a9803", - "spec_text_box": "73083086", + "spec_text_box": "e738016b4", "spec_text_modifier": "004b53bf", "spec_text_path": "4d8a9803", "spec_trim_path": "eb9361216",