diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 83c92b7..1360115 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -81,6 +81,57 @@ jobs:
- name: Test
run: vendor/bin/pest
+ # The decks the library writes, checked against the schema Microsoft's own validator
+ # reads.
+ #
+ # Its own job rather than a step in the test matrix, because the validator is a .NET
+ # program: the SDK it is built and run with is a second toolchain, and the job that
+ # installs it is the job that runs the gate.
+ #
+ # `--fail-on-skipped` because the test skips itself where the .NET SDK is missing, and
+ # a skipped test is a green tick for a check that never ran. The rasteriser is
+ # installed for the same reason: the deck carrying an SVG cannot be written without
+ # one, and a gate that checked one deck fewer than the gallery holds would be a gate
+ # that says less than it says.
+ conformance:
+ name: valid OOXML
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+
+ - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
+ with:
+ php-version: '8.5'
+ coverage: none
+ tools: composer:v2
+ extensions: imagick
+
+ - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ with:
+ dotnet-version: '8.0.x'
+
+ # The delegate ImageMagick hands an SVG to. Without one that can draw a letter the
+ # picture in the deck is refused rather than drawn, and the deck is not written at
+ # all — which would leave the gate checking one deck fewer than the gallery holds.
+ - name: A rasteriser that can draw an SVG
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes --no-install-recommends librsvg2-bin
+
+ - name: Install
+ run: composer install --prefer-dist --no-interaction --no-progress
+
+ - name: What the runner has
+ run: |
+ php -r 'var_dump(extension_loaded("imagick"));'
+ php -r 'print_r(Imagick::queryFormats("*SVG*"));'
+ convert -list format 2>/dev/null | grep -i svg || echo "no convert"
+ convert -list delegate 2>/dev/null | grep -i svg || echo "no delegate"
+ fc-list 2>/dev/null | head -3 || echo "no fonts"
+
+ - name: The decks are valid OOXML
+ run: vendor/bin/pest --testsuite=conformance --fail-on-skipped
+
coverage:
name: coverage
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index 646b245..48b1714 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,10 @@
# The OOXML schemas `tools/install-ooxml-schemas.php` fetches and checksums.
/tools/ooxml-schemas/
+# The validator's own build output, which the .NET SDK writes beside the project.
+/tools/ooxml-validator/bin/
+/tools/ooxml-validator/obj/
+
# The fonts `tools/generate-font-metrics.php` fetches and checksums.
/tools/fonts/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a816266..98e8614 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,34 @@ All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+### Added
+
+- **Every deck the library writes is checked against the schema Microsoft's own validator
+ reads.** Nothing in the suite could see that before: the decks were checked against the
+ parts they carry, and a deck whose XML is outside the schema passes every one of those
+ checks and opens in PowerPoint as `[Repaired]`.
+
+ The validator is Microsoft's own — the one the Open XML SDK Productivity Tool uses — and
+ it is a program in `tools/ooxml-validator/` rather than a dependency: nothing that ships
+ with the library needs it. A machine without the .NET SDK runs the rest of the suite and
+ says what is missing, and the job that runs the gate in continuous integration installs
+ it, so the gate cannot skip itself there.
+
+### Changed
+
+- **The patch carries one more fix from upstream.** A chart's series is written in the
+ order the schema names it: the shape properties right after the title rather than after
+ the data points and the data labels, and the separator of the data labels of a doughnut
+ before the leader lines rather than after them. PowerPoint repairs a chart written the
+ other way round before it opens it
+ ([#695](https://github.com/PHPOffice/PHPPresentation/issues/695),
+ [#1044](https://github.com/PHPOffice/PHPPresentation/pull/1044)).
+
+ This library writes no chart, so nothing it draws changes: the patch is upstream's
+ branch whole, and the fix belongs to that branch rather than to this one.
+
## [0.3.0] - 2026-10-30
### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a1711a3..2fa63bf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -146,6 +146,17 @@ it would import the file through a lossy converter and report nothing about whet
is valid. PowerPoint is the only application on this machine that will tell the truth about
whether these decks open.
+### The schema is checked by Microsoft's own validator
+
+A deck whose XML is outside the schema opens in PowerPoint as `[Repaired]`, and the
+checks written by hand here did not catch that. `tests/Conformance/ooxml.php` runs
+Microsoft's Open XML validator — the one the Open XML SDK Productivity Tool uses — over
+every deck the library writes, and the program that does it is in `tools/ooxml-validator/`.
+
+It is a .NET program, so a machine without the SDK skips that test and says what is
+missing; the job that runs it in continuous integration installs it, so the gate cannot
+skip itself there.
+
## What a change has to pass
```
diff --git a/composer.json b/composer.json
index 4064e28..4815a58 100644
--- a/composer.json
+++ b/composer.json
@@ -39,9 +39,9 @@
"patches": {
"phpoffice/phppresentation": [
{
- "description": "PHPPresentation 1.2.0 brought up to upstream's master, with the fixes that give a picture the path, the bytes and the name it was read from, and that find the folder of a part of a package with the slashes of the package",
- "url": "https://raw.githubusercontent.com/markdown-office/PHPPresentation/9aa3f4624f1cab54fc5d275397c2fc59cf6f276d/patches/phpoffice-phppresentation-1.2.0-to-master.patch",
- "sha256": "0b8f7c150f7ba5d12671fb9e211c9f1a4db549d7609239f49c0cea707a190551"
+ "description": "PHPPresentation 1.2.0 brought up to upstream's master, with the fixes that give a picture the path, the bytes and the name it was read from, that find the folder of a part of a package with the slashes of the package, and that write a chart series in the order the schema names it",
+ "url": "https://raw.githubusercontent.com/markdown-office/PHPPresentation/92c2c64ca20a6b3febf6512e892b21a3c0ae4b56/patches/phpoffice-phppresentation-1.2.0-to-master.patch",
+ "sha256": "67ce1a44e76e4dc23639fefee365ec811ef7a763331343bde468ae781bf5427a"
}
]
}
@@ -69,6 +69,7 @@
"scripts": {
"test": "pest",
"test:unit": "pest --testsuite=unit",
+ "test:conformance": "pest --testsuite=conformance",
"test:readme": "pest --testsuite=readme",
"test:coverage": "php -d memory_limit=-1 vendor/bin/pest --coverage --min=100",
"analyse": "tools/phpstan.sh",
diff --git a/examples/build.php b/examples/build.php
index a2653f6..8b92085 100644
--- a/examples/build.php
+++ b/examples/build.php
@@ -12,22 +12,11 @@
*/
require_once __DIR__ . '/../vendor/autoload.php';
+require_once __DIR__ . '/decks.php';
use MarkdownPresentation\Document\DeckParser;
use MarkdownPresentation\Render\PresentationRenderer;
-/**
- * The sources this script does not convert, and why.
- *
- * Keyed by the file's stem. A deck is here because converting it is the failure it
- * exists to demonstrate, which is the same reason `markdown-word` does not build its
- * `14-rejected.md`: the build would stop, or — worse — would not stop and the gallery
- * would contain a deck that quietly came out wrong.
- */
-const NOT_BUILT = [
- '10-rejected' => 'converting it is the failure it exists to demonstrate; see examples/README.md',
-];
-
/** Where the decks, their PDFs and their previews are written. */
const OUT = __DIR__ . '/out';
@@ -37,19 +26,6 @@
/** The directory a relative image path in a deck resolves against. */
const SOURCES = __DIR__ . '/markdown';
-/**
- * The sources written more than once, and the one line each copy differs in.
- *
- * A theme is a frontmatter key, so the only way to see the three palettes side by side is
- * to write the same deck three times. Varying it here rather than in three files keeps
- * the three decks identical apart from the one line, which is the point of the
- * comparison.
- */
-const VARIANTS = [
- '03-themes' => ['theme' => ['default', 'corporate-dark', 'minimal']],
- '11-type-scale' => ['theme' => ['default', 'dense']],
-];
-
/**
* What each built deck is for, for the report at the end and for nothing else.
*/
diff --git a/examples/decks.php b/examples/decks.php
new file mode 100644
index 0000000..863e6e7
--- /dev/null
+++ b/examples/decks.php
@@ -0,0 +1,41 @@
+ 'converting it is the failure it exists to demonstrate; see examples/README.md',
+];
+
+/**
+ * The sources written more than once, and the one line each copy differs in.
+ *
+ * A theme is a frontmatter key, so the only way to see the three palettes side by side is
+ * to write the same deck three times. Varying it here rather than in three files keeps
+ * the three decks identical apart from the one line, which is the point of the
+ * comparison.
+ */
+const VARIANTS = [
+ '03-themes' => ['theme' => ['default', 'corporate-dark', 'minimal']],
+ '11-type-scale' => ['theme' => ['default', 'dense']],
+];
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 383a647..c3c920a 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -15,6 +15,14 @@
tests/Unit
+
+
+ tests/Conformance
+
+ net8.0
+
+ Major
+ enable
+ enable
+ OoxmlValidator
+ ooxml-validator
+
+
+
+
+
+
+
+