diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 83c92b7..22e01b7 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -81,6 +81,47 @@ 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.
+ #
+ # The rasteriser is installed because the deck that carries a vector needs one to be
+ # written at all — the library refuses a picture it cannot draw rather than writing a
+ # diagram that has lost its words. Whether the runner's Imagick can draw one is the
+ # runner's business: where it cannot, that deck's test says so and skips itself, and
+ # the rest of the gallery is checked all the same.
+ 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 validator is a .NET program, so the SDK is what runs it. The step says so
+ # where it is missing rather than leaving the gate to skip itself: a skipped test
+ # is a green tick for a check that never ran.
+ - name: The .NET SDK is there
+ run: dotnet --version
+
+ - name: Install
+ run: composer install --prefer-dist --no-interaction --no-progress
+
+ - name: The decks are valid OOXML
+ run: vendor/bin/pest --testsuite=conformance
+
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..827dbe9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,73 @@ 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).
+## [0.3.1] - 2026-10-31
+
+### Added
+
+- **The decks the library writes are 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.
+
+ The one deck that carries a vector needs a rasteriser to be written at all — the library
+ refuses a picture it cannot draw rather than writing a diagram that has lost its words
+ — so a machine without one says which deck it did not check and checks the rest.
+ **The continuous integration runner is such a machine**, so the gate checks eighteen of
+ the nineteen decks there, and the nineteenth is checked where a vector can be drawn.
+
+### Changed
+
+- **The patch carries four more fixes 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)).
+
+ Three of them are charts that carried less than they were given: the area, the line,
+ the radar and the scatter dropped the fill of a single data point
+ ([#1048](https://github.com/PHPOffice/PHPPresentation/issues/1048),
+ [#1052](https://github.com/PHPOffice/PHPPresentation/pull/1052)), a doughnut, a pie
+ and a pie in three dimensions dropped the fill of the series itself
+ ([#1049](https://github.com/PHPOffice/PHPPresentation/issues/1049),
+ [#1051](https://github.com/PHPOffice/PHPPresentation/pull/1051)), and a category axis
+ wrote its major unit as the number of labels to skip — and as a fraction, where the
+ schema wants a count, so the file was outside the schema
+ ([#1047](https://github.com/PHPOffice/PHPPresentation/issues/1047),
+ [#1050](https://github.com/PHPOffice/PHPPresentation/pull/1050)).
+
+ 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.
+
+- **The charts are checked too, and the gate knows what a chart has to carry.** The
+ patch is the library that writes the charts, and a patch that is re-rolled without a
+ fix is a patch that has lost it — nothing else in this suite would have noticed. So
+ the gate writes a chart of every kind through the patched library, and checks two
+ things about it: that the file is within the schema, which the validator reads, and
+ that it carries what it was given, which the validator cannot see.
+
+ A chart that is missing a data point is a valid chart. The validator reads what a
+ file holds and says nothing about what it does not, so the elements themselves are
+ asserted on: every kind of series carries the fill it was given, and every kind of
+ series carries the fill of the point it was given.
+
+ Three defects were found this way and are fixed upstream:
+ [#1047](https://github.com/PHPOffice/PHPPresentation/issues/1047) — a category axis
+ wrote its major unit as the number of labels to skip, and wrote it as a fraction
+ where the schema wants a count, so the file was outside the schema;
+ [#1048](https://github.com/PHPOffice/PHPPresentation/issues/1048) — the area, line,
+ radar and scatter charts dropped the fill of a single data point;
+ [#1049](https://github.com/PHPOffice/PHPPresentation/issues/1049) — a doughnut, a pie
+ and a pie in three dimensions dropped the fill of the series itself.
+
## [0.3.0] - 2026-10-30
### Added
@@ -2500,5 +2567,6 @@ slide begins and which theme a deck means, and renders the deck to a `.pptx`.
[0.2.1]: https://github.com/markdown-office/markdown-presentation/releases/tag/v0.2.1
[0.2.2]: https://github.com/markdown-office/markdown-presentation/releases/tag/v0.2.2
[0.3.0]: https://github.com/markdown-office/markdown-presentation/releases/tag/v0.3.0
+[0.3.1]: https://github.com/markdown-office/markdown-presentation/releases/tag/v0.3.1
[composer-patches]: https://github.com/cweagans/composer-patches
[the-patch]: https://github.com/markdown-office/PHPPresentation/blob/patches/1.2.0-to-master/patches/phpoffice-phppresentation-1.2.0-to-master.patch
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a1711a3..a31ff1c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -146,6 +146,19 @@ 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
+the decks 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 those tests and says what is
+missing; the job that runs them in continuous integration installs it, so the gate cannot
+skip itself there. The one deck that carries a vector needs a rasteriser to be written at
+all, and a machine without one says which deck it did not check.
+
## What a change has to pass
```
diff --git a/composer.json b/composer.json
index 4064e28..d028a04 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, that write a chart series in the order the schema names it, that write the data points of a series and the fill of the series itself, and that give a category axis a tick label skip of its own",
+ "url": "https://raw.githubusercontent.com/markdown-office/PHPPresentation/605bc7c9724ac4a0781cc1692ff7ae279a33bb8a/patches/phpoffice-phppresentation-1.2.0-to-master.patch",
+ "sha256": "46862cef4470ae8e755a03f1f8afd94c7e772cc1ee4ab0a39441c2cb392d5146"
}
]
}
@@ -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..9181279
--- /dev/null
+++ b/examples/decks.php
@@ -0,0 +1,34 @@
+ '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.
+ */
+const VARIANTS = [
+ '03-themes' => ['theme' => ['default', 'corporate-dark', 'minimal']],
+ '11-type-scale' => ['theme' => ['default', 'dense']],
+];
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index b4ac8f7..411e456 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -2,11 +2,11 @@
# tools/phpstan.sh for the version and the hash.
#
# Level 9, with no `ignoreErrors`, no baseline and no suppression comment anywhere
-# in this repository. `tests/Unit` and `tests/Readme` are not analysed: every one of
-# their tests is a closure handed to `it()` and every assertion goes through
-# `expect()`, both of which resolve at run time, so an analyser given neither can
-# only report that the functions are unknown. The support code those tests lean on
-# *is* analysed, and it is where the array shapes live.
+# in this repository. `tests/Unit`, `tests/Conformance` and `tests/Readme` are not
+# analysed: every one of their tests is a closure handed to `it()` and every assertion
+# goes through `expect()`, both of which resolve at run time, so an analyser given
+# neither can only report that the functions are unknown. The support code those tests
+# lean on *is* analysed, and it is where the array shapes live.
parameters:
level: 9
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
+
+
+
+
+
+
+
+