diff --git a/.cspell.yaml b/.cspell.yaml
index a27d356c..648ba2ba 100644
--- a/.cspell.yaml
+++ b/.cspell.yaml
@@ -65,6 +65,7 @@ words:
- combinators
- CSUM
- Decodability
+ - DOTLESS
- dequantize
- dequantizes
- Deflate
@@ -97,6 +98,19 @@ words:
- gAMA
- Grayscale
- grayscale
+ - gridline
+ - gridlines
+ - horz
+ - Horz
+ - Dmnd
+ - patt
+ - pythonpptx
+ - slidenum
+ - xfrm
+ - drawingml
+ - Calibri
+ - unoverridden
+ - subtable
- IDAT
- IDCT
- IEND
@@ -114,8 +128,10 @@ words:
- mcus
- microarchitecture
- misdecoded
+ - misrender
- nameof
- nonzero
+ - paintable
- NOTAFONT
- NOTAGIF
- Outliner
@@ -309,6 +325,41 @@ words:
- thinspace
- Thinspace
- subsetted
+ - rels
+ - Rels
+ - Linq
+ - OOXML
+ - openxmlformats
+ - officedocument
+ - presentationml
+ - pptx
+ - Pptx
+ - autofit
+ - Autofit
+ - unrotated
+ - unflipped
+ - unshifted
+ - autoshape
+ - Autoshape
+ - autoshapes
+ - Autonumber
+ - srgb
+ - MCMXCIV
+ - hlink
+ - Hlink
+ - clrmap
+ - clrmapovr
+ - colormap
+ - openxml
+ - textless
+ - aiden
+ - misrouting
+ - parameterless
+ - Lbls
+ - texel
+ - precomputes
+ - uncontained
+ - prst
# Exclude common build artifacts, dependencies, and vendored third-party code
ignorePaths:
diff --git a/.reviewmark.yaml b/.reviewmark.yaml
index 011131bf..83266295 100644
--- a/.reviewmark.yaml
+++ b/.reviewmark.yaml
@@ -769,6 +769,238 @@ reviews:
- "test/DemaConsulting.CanvasNet.Pdf.Tests/PdfSystemIntegrationTests.cs" # integration tests
- "test/DemaConsulting.CanvasNet.Pdf.Tests/PdfFixtureTests.cs" # fixture conformance tests
+ # CanvasNetPptx-Architecture Review (one per system)
+ - id: CanvasNetPptx-Architecture
+ title: Review that CanvasNetPptx Architecture Satisfies Requirements
+ context:
+ - "README.md"
+ - "docs/user_guide/**/*.md"
+ paths:
+ - "docs/reqstream/canvas-net-pptx.yaml" # system requirements
+ - "docs/design/introduction.md" # design introduction
+ - "docs/design/canvas-net-pptx.md" # system design
+ - "docs/verification/introduction.md" # verification introduction
+ - "docs/verification/canvas-net-pptx.md" # system verification
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxSystemIntegrationTests.cs" # integration tests
+
+ # CanvasNetPptx-Design Review (one per system)
+ - id: CanvasNetPptx-Design
+ title: Review that CanvasNetPptx Design is Consistent and Complete
+ context:
+ - "docs/reqstream/canvas-net-pptx.yaml"
+ paths:
+ - "docs/reqstream/canvas-net-pptx/platform-requirements.yaml" # platform requirements
+ - "docs/design/introduction.md" # design introduction
+ - "docs/design/canvas-net-pptx.md" # system design
+ - "docs/design/canvas-net-pptx/**/*.md" # all system design documents
+
+ # CanvasNetPptx-Verification Review (one per system)
+ - id: CanvasNetPptx-Verification
+ title: Review that CanvasNetPptx Verification is Consistent and Complete
+ context:
+ - "docs/reqstream/canvas-net-pptx.yaml"
+ paths:
+ - "docs/verification/introduction.md" # verification introduction
+ - "docs/verification/canvas-net-pptx.md" # system verification
+ - "docs/verification/canvas-net-pptx/**/*.md" # all verification documents
+
+ # CanvasNetPptx-AllRequirements Review (one per system)
+ - id: CanvasNetPptx-AllRequirements
+ title: Review that All CanvasNetPptx Requirements are Complete
+ context:
+ - "docs/design/canvas-net-pptx.md"
+ - "docs/reqstream/canvas-net-pptx.yaml"
+ paths:
+ - "docs/reqstream/canvas-net-pptx/**/*.yaml" # all requirements files
+
+ # CanvasNetPptx-PptxDocument Review (one per direct unit)
+ - id: CanvasNetPptx-PptxDocument
+ title: Review that CanvasNetPptx PptxDocument Implementation is Correct
+ context:
+ - "docs/design/canvas-net-pptx.md"
+ - "docs/reqstream/canvas-net-pptx.yaml"
+ paths:
+ - "docs/reqstream/canvas-net-pptx/pptx-document.yaml" # unit requirements
+ - "docs/design/canvas-net-pptx/pptx-document.md" # unit design
+ - "docs/verification/canvas-net-pptx/pptx-document.md" # unit verification
+ - "docs/sysml2/model/canvas-net-pptx/pptx-document.sysml" # unit SysML2 model
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.cs" # phase 1a unit source (public API)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Package.cs" # phase 1a unit source (package layer)
+ - "src/DemaConsulting.CanvasNet.Pptx/NamespaceDoc.cs" # Pptx namespace documentation
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Presentation.cs" # phase 1b unit source (presentation)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxSlideSize.cs" # phase 1b unit source (slide size model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Theme.cs" # phase 1b unit source (theme parsing)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxTheme.cs" # phase 1b unit source (theme model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Masters.cs" # phase 1b unit source (master parsing)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Layouts.cs" # phase 1b unit source (layout parsing)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Slides.cs" # phase 1b unit source (slide parsing)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxPlaceholder.cs" # phase 1b unit source (placeholder model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Inheritance.cs" # phase 1b unit source (inheritance)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxPlaceholderProperties.cs" # phase 1b unit source (resolved properties)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Geometry.cs" # phase 1c unit source (geometry resolvers)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Paint.cs" # phase 1c unit source (fill/color/stroke resolvers)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxPresetGeometry.cs" # phase 1c unit source (preset shape builder)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxShapeFrame.cs" # phase 1c unit source (resolved shape frame model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxPaint.cs" # phase 1c unit source (resolved paint model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxLineStyle.cs" # phase 1c unit source (resolved line style model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxUnsupportedFeatureException.cs" # phase 1c unit source (exceptions)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxTextBody.cs" # phase 1d unit source (text body model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Text.cs" # phase 1d unit source (text body parsing)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxEffectiveTextProperties.cs" # phase 1d unit source (text props)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.TextInheritance.cs" # phase 1d unit source (inheritance)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxTextLayout.cs" # phase 1d unit source (resolved text layout model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.TextLayout.cs" # phase 1d unit source (text layout)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.TextRender.cs" # phase 1d unit source (glyph rendering)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxPicture.cs" # phase 1e unit source (crop-rectangle model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Images.cs" # phase 1e unit source (picture decode/crop/paint)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxTable.cs" # phase 1e unit source (table data model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Tables.cs" # phase 1e unit source (table parsing/painting)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxShapeTree.cs" # phase 1e unit source (shape-tree node model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.ShapeTree.cs" # phase 1e unit source (shape-tree parsing)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxRenderOptions.cs" # phase 1f unit source (render options)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Render.cs" # phase 1f unit source (public Render API)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Background.cs" # phase 2 follow-up (background fill)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Bullets.cs" # phase 2 follow-up (bullet/numbering formatting)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxArrowheadStyle.cs" # phase 2 follow-up (connector arrowhead model)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxArrowheadGeometry.cs" # phase 2 follow-up (arrowhead geometry)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Connectors.cs" # phase 2 follow-up (connector resolution)
+ - "src/DemaConsulting.CanvasNet.Pptx/PptxDocument.Charts.cs" # phase 4 unit source (chart frame integration)
+ - "src/DemaConsulting.CanvasNet.Pptx/DemaConsulting.CanvasNet.Pptx.csproj" # project file (phase 4: Charts ref)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxDocumentTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxGeometryTests.cs" # phase 1c unit tests (geometry)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxPaintTests.cs" # phase 1c unit tests (paint)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxTextTests.cs" # phase 1d unit tests (text parsing)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxTextLayoutTests.cs" # phase 1d unit tests (text layout)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxTextRenderTests.cs" # phase 1d unit tests (glyph rendering)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxImagesTests.cs" # phase 1e unit tests (images)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxTablesTests.cs" # phase 1e unit tests (tables)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxGroupsTests.cs" # phase 1e unit tests (shape tree/groups)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxSystemIntegrationTests.cs" # integration tests
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxRenderTests.cs" # phase 1f unit tests (render API)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxBackgroundTests.cs" # phase 2 follow-up (background resolver)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxBulletTests.cs" # phase 2 follow-up (bullet/numbering tests)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxColorMapTests.cs" # phase 2 follow-up (color map tests)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxConnectorTests.cs" # phase 2 follow-up (connector unit tests)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxChartsTests.cs" # phase 4 unit tests (chart frame integration)
+ - "test/DemaConsulting.CanvasNet.Pptx.Tests/PptxFixturesCorpusTests.cs" # fixture conformance tests
+
+ # CanvasNetCharts-Architecture Review (one per system)
+ - id: CanvasNetCharts-Architecture
+ title: Review that CanvasNetCharts Architecture Satisfies Requirements
+ context:
+ - "README.md"
+ - "docs/user_guide/**/*.md"
+ paths:
+ - "docs/reqstream/canvas-net-charts.yaml" # system requirements
+ - "docs/design/introduction.md" # design introduction
+ - "docs/design/canvas-net-charts.md" # system design
+ - "docs/verification/introduction.md" # verification introduction
+ - "docs/verification/canvas-net-charts.md" # system verification
+
+ # CanvasNetCharts-Design Review (one per system)
+ - id: CanvasNetCharts-Design
+ title: Review that CanvasNetCharts Design is Consistent and Complete
+ context:
+ - "docs/reqstream/canvas-net-charts.yaml"
+ paths:
+ - "docs/design/introduction.md" # design introduction
+ - "docs/design/canvas-net-charts.md" # system design
+ - "docs/design/canvas-net-charts/**/*.md" # all system design documents
+
+ # CanvasNetCharts-Verification Review (one per system)
+ - id: CanvasNetCharts-Verification
+ title: Review that CanvasNetCharts Verification is Consistent and Complete
+ context:
+ - "docs/reqstream/canvas-net-charts.yaml"
+ paths:
+ - "docs/verification/introduction.md" # verification introduction
+ - "docs/verification/canvas-net-charts.md" # system verification
+ - "docs/verification/canvas-net-charts/**/*.md" # all verification documents
+
+ # CanvasNetCharts-AllRequirements Review (one per system)
+ - id: CanvasNetCharts-AllRequirements
+ title: Review that All CanvasNetCharts Requirements are Complete
+ context:
+ - "docs/design/canvas-net-charts.md"
+ - "docs/reqstream/canvas-net-charts.yaml"
+ paths:
+ - "docs/reqstream/canvas-net-charts/**/*.yaml" # all requirements files
+
+ # CanvasNetCharts-ChartDocument Review (one per unit)
+ - id: CanvasNetCharts-ChartDocument
+ title: Review that CanvasNetCharts ChartDocument Implementation is Correct
+ context:
+ - "docs/design/canvas-net-charts.md"
+ - "docs/reqstream/canvas-net-charts.yaml"
+ - "docs/design/canvas-net-charts/chart-model.md"
+ - "docs/reqstream/canvas-net-charts/chart-model.yaml"
+ paths:
+ - "docs/reqstream/canvas-net-charts/chart-model/chart-document.yaml" # unit requirements
+ - "docs/design/canvas-net-charts/chart-model/chart-document.md" # unit design
+ - "docs/verification/canvas-net-charts/chart-model/chart-document.md" # unit verification
+ - "docs/sysml2/model/canvas-net-charts/chart-model/chart-document.sysml" # unit SysML2 model
+ - "src/DemaConsulting.CanvasNet.Charts/DemaConsulting.CanvasNet.Charts.csproj" # unit project file
+ - "src/DemaConsulting.CanvasNet.Charts/NamespaceDoc.cs" # Charts namespace documentation
+ - "src/DemaConsulting.CanvasNet.Charts/ChartType.cs" # unit source (chart kind enum)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartAxis.cs" # unit source (axis model)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartLegend.cs" # unit source (legend model)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartTitle.cs" # unit source (title model)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartSeries.cs" # unit source (series model)
+ - "src/DemaConsulting.CanvasNet.Charts/Chart.cs" # unit source (root aggregate)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartBuilder.cs" # unit source (fluent builder)
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/DemaConsulting.CanvasNet.Charts.Tests.csproj" # unit test project
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartTypeTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartAxisTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartLegendTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartTitleTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartSeriesTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartBuilderTests.cs" # unit tests
+
+ # CanvasNetCharts-ChartRenderer Review (one per unit)
+ - id: CanvasNetCharts-ChartRenderer
+ title: Review that CanvasNetCharts ChartRenderer Implementation is Correct
+ context:
+ - "docs/design/canvas-net-charts.md"
+ - "docs/reqstream/canvas-net-charts.yaml"
+ - "docs/design/canvas-net-charts/chart-model.md"
+ - "docs/reqstream/canvas-net-charts/chart-model.yaml"
+ paths:
+ - "docs/reqstream/canvas-net-charts/chart-model/chart-renderer.yaml" # unit requirements
+ - "docs/design/canvas-net-charts/chart-model/chart-renderer.md" # unit design
+ - "docs/verification/canvas-net-charts/chart-model/chart-renderer.md" # unit verification
+ - "docs/sysml2/model/canvas-net-charts/chart-model/chart-renderer.sysml" # unit SysML2 model
+ - "src/DemaConsulting.CanvasNet.Charts/ChartColorPalette.cs" # unit source (default palette)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartRenderOptions.cs" # unit source (render options)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartRenderer.cs" # unit source (entry point)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartRenderer.Layout.cs" # unit source (layout computation)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartRenderer.CategoryChart.cs" # unit source (Bar/Col/Line/Area painters)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartRenderer.PieChart.cs" # unit source (Pie/Doughnut painter)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartRenderer.Legend.cs" # unit source (legend painter)
+ - "src/DemaConsulting.CanvasNet.Charts/ChartRenderer.Title.cs" # unit source (title painter)
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartRendererTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartRendererAxisLegendTitleTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/ChartRendererEdgeCaseTests.cs" # unit tests
+
+ # CanvasNetCharts-OpenXmlChartParser Review (one per unit)
+ - id: CanvasNetCharts-OpenXmlChartParser
+ title: Review that CanvasNetCharts OpenXmlChartParser Implementation is Correct
+ context:
+ - "docs/design/canvas-net-charts.md"
+ - "docs/reqstream/canvas-net-charts.yaml"
+ - "docs/design/canvas-net-charts/open-xml-chart.md"
+ - "docs/reqstream/canvas-net-charts/open-xml-chart.yaml"
+ paths:
+ - "docs/reqstream/canvas-net-charts/open-xml-chart/open-xml-chart-parser.yaml" # unit requirements
+ - "docs/design/canvas-net-charts/open-xml-chart/open-xml-chart-parser.md" # unit design
+ - "docs/verification/canvas-net-charts/open-xml-chart/open-xml-chart-parser.md" # unit verification
+ - "docs/sysml2/model/canvas-net-charts/open-xml-chart/open-xml-chart-parser.sysml" # unit SysML2 model
+ - "src/DemaConsulting.CanvasNet.Charts/OpenXml/NamespaceDoc.cs" # OpenXml namespace documentation
+ - "src/DemaConsulting.CanvasNet.Charts/OpenXml/OpenXmlChartParser.cs" # unit source (parser entry point)
+ - "src/DemaConsulting.CanvasNet.Charts/OpenXml/ChartUnsupportedFeatureException.cs" # unit source (exception)
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/OpenXmlChartParserTests.cs" # unit tests
+ - "test/DemaConsulting.CanvasNet.Charts.Tests/OpenXmlChartFixtures/README.md" # fixture provenance
+
# OTS-BuildMark Review (one per OTS item)
- id: OTS-BuildMark
title: Review that BuildMark Provides Required Functionality
diff --git a/CanvasNet.slnx b/CanvasNet.slnx
index 229269f3..f2e3e588 100644
--- a/CanvasNet.slnx
+++ b/CanvasNet.slnx
@@ -3,10 +3,14 @@
+
+
+
+
diff --git a/README.md b/README.md
index f86e6742..6d7e7c44 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,17 @@ image operations using `Span`, and supports independent-copy cropping for loa
TrueType text to a surface, automatically substituting a matching system font (or a bundled
Liberation Sans/Serif/Mono font) for text using a non-embedded font (ships as the separate
`DemaConsulting.CanvasNet.Pdf` package)
+- 📊 **Chart Rendering** - Build a validated Bar/Column/Line/Area/Pie/Doughnut chart (with axes,
+ legend, title, and data labels) via a fluent builder and paint it onto a surface with documented
+ default styling and a categorical color palette (ships as the separate
+ `DemaConsulting.CanvasNet.Charts` package)
+- 📈 **OOXML Chart Parsing** - Parse a raw ECMA-376 DrawingML-Charts `c:chartSpace`/`c:chart` XML
+ element (for example, a `chart1.xml` OPC part) into a validated chart, reading only cached
+ values, with no OPC/ZIP packaging or host document-format knowledge (ships as part of the same
+ `DemaConsulting.CanvasNet.Charts` package). When rendering a `.pptx` presentation via the
+ separate `DemaConsulting.CanvasNet.Pptx` package's `PptxDocument.Render`, an embedded
+ `` chart is automatically parsed and painted using this same package, with no
+ extra caller code required.
- 🔍 **Header-Only Probing** - `GetInfo` reads headers without decoding pixels (GIF excepted)
- 🖌️ **Path Filling** - Antialiased nonzero/even-odd fill of vector paths
- 🖊️ **Stroke-to-Fill** - Convert stroked paths into fillable outlines
@@ -92,6 +103,31 @@ Or via Package Manager Console:
Install-Package DemaConsulting.CanvasNet.Pdf
```
+Chart rendering requires the separate `DemaConsulting.CanvasNet.Charts` package:
+
+```bash
+dotnet add package DemaConsulting.CanvasNet.Charts
+```
+
+Or via Package Manager Console:
+
+```powershell
+Install-Package DemaConsulting.CanvasNet.Charts
+```
+
+PowerPoint (`.pptx`) document parsing and rendering requires the separate
+`DemaConsulting.CanvasNet.Pptx` package:
+
+```bash
+dotnet add package DemaConsulting.CanvasNet.Pptx
+```
+
+Or via Package Manager Console:
+
+```powershell
+Install-Package DemaConsulting.CanvasNet.Pptx
+```
+
## Usage
```csharp
@@ -163,6 +199,44 @@ if (!info.CanDecode)
using var safeSurface = PngCodec.Load("untrusted.png");
```
+Building and rendering a chart:
+
+```csharp
+using DemaConsulting.CanvasNet.Canvas;
+using DemaConsulting.CanvasNet.Charts;
+
+// Build a validated Chart fluently
+var chart = new ChartBuilder()
+ .OfType(ChartType.Column)
+ .WithCategoryAxis(["Q1", "Q2"])
+ .WithValueAxis(minimum: 0f, maximum: 10f)
+ .AddSeries("Revenue", [4.0, 9.0], color: new Rgba32(31, 119, 180, 255))
+ .WithTitle("Quarterly Revenue")
+ .WithLegend(ChartLegendPosition.Bottom)
+ .Build();
+
+// Render it onto a new surface using every rendering default
+using var chartSurface = ChartRenderer.Render(chart, 400, 300);
+```
+
+Parsing a raw OOXML chart part and rendering it:
+
+```csharp
+using DemaConsulting.CanvasNet.Canvas;
+using DemaConsulting.CanvasNet.Charts;
+using DemaConsulting.CanvasNet.Charts.OpenXml;
+using System.Xml.Linq;
+
+// Parse a c:chartSpace/c:chart XML document (for example, a chart1.xml OPC part a host
+// document-format library - such as CanvasNetPptx - has already located and opened)
+var chartDocument = XDocument.Load("chart1.xml");
+var parsedChart = OpenXmlChartParser.Parse(chartDocument);
+
+// The parsed Chart is directly consumable by ChartRenderer, exactly like a Chart built via
+// ChartBuilder
+using var parsedChartSurface = ChartRenderer.Render(parsedChart, 400, 300);
+```
+
Filling a vector path onto a surface:
```csharp
diff --git a/docs/design/canvas-net-charts.md b/docs/design/canvas-net-charts.md
new file mode 100644
index 00000000..9019aafe
--- /dev/null
+++ b/docs/design/canvas-net-charts.md
@@ -0,0 +1,256 @@
+# System Design
+
+This document provides the system-level design for CanvasNetCharts.
+
+
+
+## Architecture
+
+CanvasNetCharts is a .NET library providing chart support, distributed as its own NuGet package
+(`DemaConsulting.CanvasNet.Charts`, namespace `DemaConsulting.CanvasNet.Charts`), independent of,
+but depending on, the core `CanvasNet` system (its own separate package,
+`DemaConsulting.CanvasNet`) — see the Dependencies section below.
+
+`CanvasNetCharts` consists of two subsystems:
+
+- **ChartModel** (folder `src/DemaConsulting.CanvasNet.Charts/`, flat — no further nesting): a
+ public, immutable, validating chart data model (`Chart`/`ChartSeries`/`ChartAxis`/
+ `ChartLegend`/`ChartTitle`/`ChartType`), the `ChartBuilder` fluent construction API, and a
+ pixel-rendering engine (`ChartRenderer`/`ChartRenderOptions`/`ChartColorPalette`) that paints a
+ `Chart` onto a core `CanvasNet.Canvas.Surface`, contained in two implemented units,
+ `ChartDocument` and `ChartRenderer`. See _ChartModel Subsystem Design_
+ (`canvas-net-charts/chart-model.md`).
+- **OpenXmlChart** (folder `src/DemaConsulting.CanvasNet.Charts/OpenXml/`): a format-agnostic
+ `OpenXmlChartParser` that produces a `Chart` directly from a raw ECMA-376 DrawingML-Charts
+ `c:chartSpace`/`c:chart` `System.Xml.Linq` element, reading only cached values, plus the
+ `ChartUnsupportedFeatureException` it throws for a recognized-but-unimplemented chart kind or
+ data shape, contained in one unit, `OpenXmlChartParser`. See _OpenXmlChart Subsystem Design_
+ (`canvas-net-charts/open-xml-chart.md`).
+
+No Subsystem tier would normally be needed for a system containing only one or two units
+(compare `CanvasNetSvg`'s `SvgCodec`, which has no interposed subsystem — see _CanvasNetSvg
+System Design_, `canvas-net-svg.md`). A `ChartModel` subsystem was nonetheless introduced from
+Phase 1 onward, ahead of `CanvasNetCharts` containing more than one unit, to avoid a disruptive
+re-model of the system's tier structure partway through its incremental delivery. Phase 2's
+`ChartRenderer` was added as a second unit within the existing `ChartModel` subsystem — rather
+than under a new subsystem, as originally anticipated in Phase 1 — because it shares the same
+single flat source folder and the same `ChartModel` Purpose statement ("what chart to draw" and
+"how to draw it" are two facets of the same data-model-plus-rendering whole). Phase 3's
+`OpenXmlChartParser`, by contrast, genuinely is a distinct concern (untrusted-input XML parsing,
+in its own `OpenXml/` sub-folder, with no dependency in either direction on `ChartRenderer`'s own
+implementation), so it was given its own `OpenXmlChart` subsystem as originally anticipated.
+
+`CanvasNetPptx` integrates both subsystems into its own slide rendering: it locates and opens a
+`chart1.xml` OPC part and hands its content to `OpenXmlChartParser`, then renders the resulting
+`Chart` via `ChartRenderer`, so that a `` containing a chart reference renders its
+chart content instead of throwing — see _CanvasNetPptx System Design_ (`canvas-net-pptx.md`) for
+that integration's own detail.
+
+## External Interfaces
+
+The system exposes the following public API, all in the `DemaConsulting.CanvasNet.Charts`
+namespace:
+
+- **`Chart`**: An immutable, validated description of a chart — its `ChartType`, its `Series`,
+ and optional `CategoryAxis`/`ValueAxis`/`Legend`/`Title`/`ColorPalette`. Constructed directly,
+ or via `ChartBuilder`.
+- **`ChartSeries`**: An immutable, validated named series of numeric values, with optional
+ per-point colors, per-point labels, and a series-level color.
+- **`ChartAxis`**: An immutable, validated axis description — an optional ordered category label
+ collection, an optional value range (minimum/maximum/tick interval), and an optional title.
+ Used for both a chart's category axis and its value axis.
+- **`ChartLegend`**: An immutable, validated legend description — a placement position
+ (`ChartLegendPosition`) and a visibility flag.
+- **`ChartTitle`**: An immutable, validated title description — text and an optional font-size
+ hint.
+- **`ChartType`**: An enumeration of the chart kinds Phase 1 models: `Bar`, `Column`, `Line`,
+ `Pie`, `Doughnut`, `Area`.
+- **`ChartLegendPosition`**: An enumeration of legend placements: `Top`, `Bottom`, `Left`,
+ `Right`, `None`.
+- **`ChartBuilder`**: A fluent, mutable-until-`Build` API for constructing a `Chart` ergonomically,
+ delegating all data-shape validation to the model types' own constructors.
+- **`ChartRenderer`**: A public static entry point that paints a `Chart` onto a new
+ `CanvasNet.Canvas.Surface`, via `Render(Chart, int, int, ChartRenderOptions?)` (direct pixel
+ dimensions) or `Render(Chart, float, float, float, ChartRenderOptions?)` (physical
+ width/height/DPI).
+- **`ChartRenderOptions`**: An immutable options bag for `ChartRenderer.Render` — background
+ color, an optional font override, per-element default font sizes, and an optional color
+ palette override — with a `Default` singleton, mirroring `PptxRenderOptions`'s own shape.
+- **`ChartColorPalette`**: A fixed, documented 10-entry categorical color palette, used by
+ `ChartRenderer` when neither `Chart.ColorPalette` nor `ChartRenderOptions.ColorPalette` supplies
+ one, resolved by index with wraparound.
+- **`OpenXmlChartParser`** (namespace `DemaConsulting.CanvasNet.Charts.OpenXml`): A public static
+ entry point that parses a raw `c:chartSpace`/`c:chart` `System.Xml.Linq.XElement` or
+ `XDocument` into a validated `Chart`, via `Parse(XDocument)`/`Parse(XElement)`.
+- **`ChartUnsupportedFeatureException`** (namespace `DemaConsulting.CanvasNet.Charts.OpenXml`): An
+ `IOException`-derived exception, carrying a short, stable `Feature` token, thrown by
+ `OpenXmlChartParser` for a recognized-but-unimplemented chart kind or data shape.
+
+
+| Interface | Direction | Format | Constraints |
+| ------------------------ | ---------------- | -------------------------- | --------------------------------------------------------------- |
+| `Chart` constructor | Inbound | Method call / `Chart` instance | At least one non-null series; see _ChartModel Subsystem Design_ |
+| `ChartBuilder.Build()` | Inbound/Outbound | Method call / `Chart` return | A chart type and at least one series must be configured first |
+| `ChartRenderer.Render(...)` | Inbound/Outbound | Method call / `Surface` return | `chart` must not be null; pixel/physical dimensions must be positive and within `Surface`'s supported range |
+| `OpenXmlChartParser.Parse(...)` | Inbound/Outbound | Method call / `Chart` return | `chartDocument`/`chartSpaceOrChartElement` must not be null; must resolve to a `c:chart` with a `c:plotArea` |
+
+
+See _ChartModel Subsystem Design_ (`canvas-net-charts/chart-model.md`), _ChartDocument Unit
+Design_ (`canvas-net-charts/chart-model/chart-document.md`), _ChartRenderer Unit Design_
+(`canvas-net-charts/chart-model/chart-renderer.md`), _OpenXmlChart Subsystem Design_
+(`canvas-net-charts/open-xml-chart.md`), and _OpenXmlChartParser Unit Design_
+(`canvas-net-charts/open-xml-chart/open-xml-chart-parser.md`) for every type's complete
+constructor parameter and exception detail.
+
+## Dependencies
+
+`CanvasNetCharts` depends on the separate `CanvasNet` system (its own package,
+`DemaConsulting.CanvasNet`, referenced via a project reference from
+`src/DemaConsulting.CanvasNet.Charts/DemaConsulting.CanvasNet.Charts.csproj`), specifically:
+
+- The `Canvas` subsystem's `Rgba32` and `Surface` units — representing a series/point/palette
+ color and the pixel buffer `ChartRenderer` paints onto and returns
+- The `Drawing` subsystem — path filling/stroking and tile-paint primitives `ChartRenderer` uses
+ to paint bars/columns/lines/areas/wedges/axes/legend/title
+- The `Geometry` subsystem — transforms and rectangles used throughout layout and painting
+- The `Fonts` subsystem — TrueType text layout/metrics, used to measure and draw every text label
+- The `Rendering` subsystem — the bundled Liberation Sans fallback font used when
+ `ChartRenderOptions.Font` is not supplied
+
+**`CanvasNetCharts` must never reference `DemaConsulting.CanvasNet.Pptx`,
+`DemaConsulting.CanvasNet.Pdf`, `DemaConsulting.CanvasNet.Svg`, or a future
+`DemaConsulting.CanvasNet.Vsdx`.** This is a hard architectural constraint, not a Phase 1-specific
+limitation: `CanvasNetCharts` is designed to be a reusable chart library any consumer of the core
+`CanvasNet` system can use directly, independent of any specific document format. `CanvasNetPptx`
+depends on `CanvasNetCharts` as of its own Phase 4 (the opposite direction), the same way it
+already depends on the core `CanvasNet` system — never the other way around; see
+`docs/design/canvas-net-pptx.md`'s own Dependencies section for that integration's details.
+
+This is an ordinary, same-repository, system-to-system dependency: both `CanvasNet` and
+`CanvasNetCharts` are produced by this repository, so it is neither an OTS Software Item (not a
+third-party/external-program dependency) nor a Shared Package (that category is scoped to a
+package produced by a _different_ repository within the same program). `CanvasNetCharts`
+introduces no new OTS Software Item beyond those already used to build and verify the `CanvasNet`
+system (BuildMark, FileAssert, Pandoc, ReqStream, ReviewMark, SarifMark, SonarMark, VersionMark,
+WeasyPrint, xUnit) — see _OTS Integration Design_ (`docs/design/ots.md`).
+
+## Risk Control Measures
+
+Construction-time validation remains the primary risk control measure: every `ChartModel`
+subsystem type validates its complete set of constructor arguments eagerly and fails closed (via
+`ArgumentException`/`ArgumentNullException`/`ArgumentOutOfRangeException`) rather than silently
+coercing, clamping, or accepting invalid data — see _ChartModel Subsystem Design_
+(`canvas-net-charts/chart-model.md`) for the complete set of validation rules; `OpenXmlChartParser`
+ultimately calls these same constructors, so it inherits this same risk control rather than
+duplicating it.
+
+Phase 3 introduces this system's first untrusted-input parsing surface: `OpenXmlChartParser`
+reads a `c:chartSpace`/`c:chart` XML element that, in a real deployment, would originate from an
+externally-authored document (for example, a PowerPoint file a `CanvasNetPptx` caller opened).
+Its risk control measures are: reading only cached values (`c:numCache`/`c:strCache`), never
+recomputing from a `c:f` formula, so no spreadsheet-formula-evaluation attack surface exists at
+all; classifying the plot area's chart-type element against a fixed, closed lookup table before
+any series parsing is attempted, rejecting an unrecognized or combo shape immediately via
+`ChartUnsupportedFeatureException`; and delegating every data-shape invariant (non-empty series,
+finite values, matching category/value counts) to the `ChartModel` subsystem's own constructors,
+so a malformed cached value surfaces the identical, already-reviewed exception a directly
+constructed `Chart` would throw, rather than a second, parser-specific validation path that could
+drift out of sync. `OpenXmlChartParser` performs no XML entity expansion of its own (no `DTD`
+processing is attempted; `System.Xml.Linq`'s own default `XDocument.Load`/`Parse` behavior does
+not resolve external entities), so it carries no XXE risk either, consistent with
+`CanvasNetSvg`'s own precedent for untrusted-XML parsing — see _CanvasNetSvg System Design_,
+`canvas-net-svg.md`, Risk Control Measures.
+
+## Data Flow
+
+**`ChartBuilder` → validated `Chart` construction path (the only data flow Phase 1 implements):**
+
+1. **Input**: A sequence of fluent `ChartBuilder` method calls (`OfType`, `AddSeries`,
+ `WithCategoryAxis`, `WithValueAxis`, `WithLegend`, `WithTitle`, `WithColorPalette`), each either
+ storing already-valid state or delegating construction of a model type to that type's own
+ validating constructor
+2. **Validation**: Each `ChartBuilder` method that constructs a model type propagates that type's
+ own constructor exceptions unchanged; `Build()` additionally throws
+ `InvalidOperationException` when no chart type was set via `OfType`, or when no series was
+ added via `AddSeries`
+3. **Processing**: `Build()` calls `Chart`'s own constructor with the builder's accumulated state,
+ which performs the complete set of cross-member validation (for example, series/category-axis
+ length matching for category-based chart types) and defensively copies every collection
+4. **Output**: A new, fully validated, immutable `Chart` instance
+
+A caller may equivalently construct a `Chart` (and its constituent `ChartSeries`/`ChartAxis`/
+`ChartLegend`/`ChartTitle` instances) directly, without `ChartBuilder`, with identical validation
+behavior, since `ChartBuilder` enforces no rule the model constructors do not already enforce
+themselves.
+
+**`Chart` → rendered `Surface` path (added in Phase 2):**
+
+1. **Input**: A validated `Chart` instance, caller-chosen pixel (or physical width/height/DPI)
+ dimensions, and an optional `ChartRenderOptions`
+2. **Validation**: `ChartRenderer.Render` throws `ArgumentNullException` when `chart` is null, and
+ `ArgumentOutOfRangeException` when the requested (or DPI-computed) pixel dimensions are not
+ positive/finite or exceed `Surface.MaxDimension`
+3. **Processing**: `ChartRenderer` computes a title/legend/plot-area layout (skipping a band that
+ would not fit the render target), resolves every series'/point's color (`Chart.ColorPalette`,
+ then `ChartRenderOptions.ColorPalette`, then `ChartColorPalette.Default`, wrapping by index),
+ and dispatches per `Chart.Type` to paint bars/columns/lines/areas or pie/doughnut wedges, plus
+ axes (for category-based types), a legend, a title, and any opt-in per-point data labels
+4. **Output**: A new `Surface` containing the rendered chart, which the caller owns and disposes
+
+**OOXML `c:chartSpace`/`c:chart` XML → validated `Chart` path (added in Phase 3):**
+
+1. **Input**: A `System.Xml.Linq.XDocument` or `XElement` carrying a `c:chartSpace` (or bare
+ `c:chart`) element, typically loaded directly from a `chart#.xml` part's content by a future
+ caller such as `CanvasNetPptx`
+2. **Validation**: `OpenXmlChartParser.Parse` throws `ArgumentNullException` for a null document/
+ element, and `ArgumentException` for a document with no root, an element with no `c:chart`
+ child (and that is not itself one), or a `c:chart` with no `c:plotArea`; the plot area's
+ chart-type element is then classified against a fixed supported/unsupported lookup table,
+ throwing `ChartUnsupportedFeatureException` for zero, more than one, or a single
+ recognized-but-unimplemented match
+3. **Processing**: Each `c:ser` is parsed into a `ChartSeries` (cached name from `c:tx`, cached
+ values from `c:val/c:numCache`, cached categories from `c:cat` routed to the shared category
+ axis or to per-series point labels depending on the classified `ChartType`); the chart title,
+ legend, and category/value axis titles/range are extracted from their own cached text/scaling
+ elements; every extracted value is finally handed to `Chart`'s own constructor, which performs
+ the identical cross-member validation a directly constructed `Chart` would
+4. **Output**: A new, fully validated, immutable `Chart` instance, directly consumable by
+ `ChartRenderer.Render`
+
+## Design Constraints
+
+- **Simplicity**: Minimal functionality kept easy to understand and extend
+- **Compliance**: All functionality must be traceable to requirements
+- **Quality**: Zero warnings, full test coverage, complete documentation
+- **Portability**: Compatible across supported .NET platforms
+- **Dependency direction**: `CanvasNetCharts` must never reference `CanvasNetPptx`,
+ `CanvasNetPdf`, `CanvasNetSvg`, or a future `CanvasNetVsdx` (see Dependencies above)
+
+### Platform Support
+
+The library targets the following frameworks, identical to the `CanvasNet` system it depends on,
+enabling compatibility across modern, currently supported .NET runtimes:
+
+| Target Framework | Runtime / Environment |
+| ---------------- | --------------------- |
+| `net8.0` | .NET 8 LTS |
+| `net9.0` | .NET 9 |
+| `net10.0` | .NET 10 |
+
+The library is supported on the following operating systems:
+
+- **Windows** — primary developer and CI platform
+- **Linux** — CI/CD and containerized environments
+- **macOS** — developer workstations using Apple platforms
+
+Portability is achieved by restricting the implementation exclusively to Base Class Library (BCL)
+APIs and the `CanvasNet` system's own public API, available across all target frameworks. No
+platform-specific native interop, OS-specific APIs, or framework-version-specific features are
+used.
+
+### Integration Patterns
+
+- **NuGet Packaging**: Standard .NET library packaging and distribution, referencing the
+ `CanvasNet` package as an ordinary NuGet dependency
+- **CI/CD Integration**: Automated build, test, and quality validation
+- **Requirements Traceability**: All features linked to passing tests
diff --git a/docs/design/canvas-net-charts/chart-model.md b/docs/design/canvas-net-charts/chart-model.md
new file mode 100644
index 00000000..cfab766b
--- /dev/null
+++ b/docs/design/canvas-net-charts/chart-model.md
@@ -0,0 +1,53 @@
+## ChartModel
+
+
+
+The `ChartModel` subsystem is the first (and, as of this release, only) software subsystem in
+`CanvasNetCharts`. It groups two flat, hand-rolled units: `ChartDocument`, containing the public,
+immutable, validating chart data model and the `ChartBuilder` fluent construction API; and
+`ChartRenderer`, a pixel-rendering engine that paints a `Chart` onto a core
+`CanvasNet.Canvas.Surface`.
+
+### Purpose
+
+The `ChartModel` subsystem provides both the in-memory representation of "what chart to draw"
+(`ChartDocument`) and "how to draw it" (`ChartRenderer`) — the complete set of chart functionality
+the `OpenXmlChart` subsystem's `chart1.xml` parser and the `CanvasNetPptx` integration both
+consume. `ChartDocument` owns the complete set of data-shape validation rules — non-empty series,
+finite numeric values, count-matched point colors/labels, consistent series/category-axis
+lengths, and well-ordered axis ranges — so that both `ChartRenderer` and the OOXML parser can
+trust every `Chart` instance they receive without re-validating it themselves. `ChartRenderer`
+owns every pixel-rendering decision — layout, default styling, color-palette resolution, and
+graceful degradation for extreme render-target sizes or legend overflow — so that the OOXML
+parser and `CanvasNetPptx` integration need not duplicate any of that logic.
+
+### Units
+
+- **ChartDocument** — the `Chart`/`ChartSeries`/`ChartAxis`/`ChartLegend`/`ChartTitle`/
+ `ChartType` model types and the `ChartBuilder` fluent construction API; see
+ _ChartDocument Unit Design_ (`chart-model/chart-document.md`)
+- **ChartRenderer** — the `ChartRenderer`/`ChartRenderOptions`/`ChartColorPalette` pixel-rendering
+ engine; see _ChartRenderer Unit Design_ (`chart-model/chart-renderer.md`)
+
+### Dependencies
+
+The `ChartModel` subsystem depends on the core `CanvasNet` system's `Canvas` subsystem (its
+`Rgba32` unit, for series/point/palette color, and, for `ChartRenderer` only, its `Surface` unit,
+the pixel buffer it paints onto and returns), and, for `ChartRenderer` only, the `Drawing`,
+`Geometry`, and `Fonts` subsystems (path filling/stroking, transforms/rectangles, and TrueType
+text layout/metrics, respectively) and the `Rendering` subsystem (the bundled Liberation Sans
+fallback font) — see _CanvasNetCharts System Design_ (`../canvas-net-charts.md`)'s Dependencies
+section. Beyond this, `ChartDocument` uses only the .NET base class library
+(`System.Collections.ObjectModel`'s `ReadOnlyCollection`, for defensive-copy immutable
+snapshots), available on every one of `CanvasNetCharts`'s target frameworks with no new runtime
+NuGet dependency.
+
+### Callers
+
+`ChartDocument` and `ChartRenderer` are each a public API entry point, invoked directly by
+consumers of the `DemaConsulting.CanvasNet.Charts` package. `ChartRenderer` additionally calls
+`ChartDocument`'s public types as its own input (a caller-supplied `Chart` instance and its
+constituent `ChartSeries`/`ChartAxis`/`ChartLegend`/`ChartTitle`/`ChartType` members) — this is
+the one intra-subsystem unit-to-unit call `ChartModel` contains as of this release. No unit
+within the `ChartModel` subsystem is called by any other subsystem as of this release (there is
+no other subsystem yet).
diff --git a/docs/design/canvas-net-charts/chart-model/chart-document.md b/docs/design/canvas-net-charts/chart-model/chart-document.md
new file mode 100644
index 00000000..1dcf7ac3
--- /dev/null
+++ b/docs/design/canvas-net-charts/chart-model/chart-document.md
@@ -0,0 +1,92 @@
+### ChartDocument
+
+The `ChartDocument` unit is the first software unit in the `ChartModel` subsystem. It comprises
+six public types, all in the `DemaConsulting.CanvasNet.Charts` namespace, each a `sealed class`
+with a single validating constructor and read-only auto-properties — not a C# `record` — plus one
+public enum and one fluent builder:
+
+- `ChartType` (enum): `Bar`, `Column`, `Line`, `Pie`, `Doughnut`, `Area`
+- `ChartAxis` (sealed class): an optional ordered category label collection, an optional value
+ range, and an optional title
+- `ChartLegendPosition` (enum): `Top`, `Bottom`, `Left`, `Right`, `None`
+- `ChartLegend` (sealed class): a placement position and a visibility flag
+- `ChartTitle` (sealed class): title text and an optional font-size hint
+- `ChartSeries` (sealed class): a name, at least one finite value, and optional per-point colors/
+ labels and a series-level color
+- `Chart` (sealed class): the root aggregate — a `ChartType`, a non-empty series collection, and
+ optional category axis, value axis, legend, title, and color palette
+- `ChartBuilder` (sealed class): a fluent, mutable-until-`Build` API for constructing a `Chart`
+
+#### Purpose
+
+`ChartDocument` lets callers construct a fully validated, immutable description of a chart,
+either directly via each type's own constructor or ergonomically via `ChartBuilder`'s fluent
+API, with identical validation behavior either way. It is implemented entirely against the .NET
+base class library (`System.Collections.ObjectModel.ReadOnlyCollection` for defensive copies)
+and the core `CanvasNet` system's `Rgba32` color type, with no third-party dependency.
+
+Every type's constructor validates its complete argument set eagerly, rejecting invalid data with
+a specific, documented exception rather than silently coercing or clamping it:
+
+- A `null` required reference-type argument → `ArgumentNullException`
+- An empty or whitespace-only name/title → `ArgumentException`
+- A non-finite (`NaN`/`Infinity`) numeric value, where a finite value is required →
+ `ArgumentOutOfRangeException` (for a single scalar argument, e.g. `ChartAxis.Minimum`) or
+ `ArgumentException` (for an entry within a collection argument, e.g. a non-finite
+ `ChartSeries.Values` entry)
+- A collection argument that is empty where at least one entry is required (`ChartSeries.Values`,
+ `Chart.Series`), or that is empty where supplied-but-empty is itself invalid
+ (`Chart.ColorPalette`) → `ArgumentException`
+- A collection argument containing a `null` entry where none is permitted (`Chart.Series`,
+ `ChartAxis.Labels`, `ChartSeries.PointLabels`) → `ArgumentException`
+- A collection argument whose count does not exactly match a related collection's count
+ (`ChartSeries.PointColors`/`PointLabels` vs. `Values`; a category-based chart type's series
+ `Values` vs. `ChartAxis.Labels`) → `ArgumentException`
+- An enum argument that is not a defined member of its enumeration (`ChartType`,
+ `ChartLegendPosition`) → `ArgumentOutOfRangeException`
+- A value range where the minimum is not strictly less than the maximum, when both are supplied
+ (`ChartAxis.Minimum`/`Maximum`) → `ArgumentOutOfRangeException`
+
+Every caller-supplied collection argument (`Chart.Series`, `Chart.ColorPalette`,
+`ChartSeries.Values`/`PointColors`/`PointLabels`, `ChartAxis.Labels`) is copied into an immutable
+`ReadOnlyCollection` snapshot during construction, so mutating the caller's original
+collection after construction has no effect on the constructed instance's exposed property —
+see `Constructor_MutatingOriginal*List_DoesNotAffectStoredSnapshot` in
+`test/DemaConsulting.CanvasNet.Charts.Tests/` for the test coverage proving this for every such
+argument.
+
+`Chart`'s constructor additionally validates one cross-member invariant: for a category-based
+`ChartType` (`Bar`, `Column`, `Line`, `Area`) whose supplied `CategoryAxis` has non-null `Labels`,
+every series' `Values.Count` must exactly equal that label count. This check is skipped for a
+proportional-share `ChartType` (`Pie`, `Doughnut`, whose single series' values represent wedge
+shares rather than per-category values), and skipped when no category axis — or a category axis
+with null `Labels` (a pure value axis) — is supplied.
+
+`ChartBuilder` performs no independent data-shape validation of its own: every chainable method
+either stores already-valid caller state (`OfType`, `AddSeries(ChartSeries)`,
+`WithCategoryAxis(ChartAxis)`, `WithValueAxis(ChartAxis)`, `WithLegend(ChartLegend)`,
+`WithColorPalette`) or constructs a model type and lets that type's own constructor validate the
+arguments (the convenience overloads of `AddSeries`, `WithCategoryAxis`, `WithValueAxis`,
+`WithLegend`, and `WithTitle`). Its only independent behavior is `Build()` throwing
+`InvalidOperationException` when `OfType` was never called, or when no series was added via
+`AddSeries`, before delegating to `Chart`'s own constructor for every other invariant.
+
+#### Design
+
+- `ChartType.cs` — the `ChartType` enum
+- `ChartAxis.cs` — the `ChartAxis` sealed class
+- `ChartLegend.cs` — the `ChartLegendPosition` enum and the `ChartLegend` sealed class
+- `ChartTitle.cs` — the `ChartTitle` sealed class
+- `ChartSeries.cs` — the `ChartSeries` sealed class
+- `Chart.cs` — the `Chart` sealed class (the root aggregate)
+- `ChartBuilder.cs` — the `ChartBuilder` sealed class
+
+Each model type's constructor performs its validation inline (or via a small private static
+helper, for a collection argument needing both validation and a defensive copy in the same pass,
+e.g. `ChartAxis.CopyLabels`/`ChartSeries.CopyValues`/`ChartSeries.CopyPointLabels`/
+`Chart.CopySeries`), then assigns every property from already-validated local state. No type in
+this unit exposes a public mutator; every property is a read-only auto-property (`{ get; }`) set
+only from the constructor.
+
+The `ChartModel` subsystem depends on the core `CanvasNet` system's `Canvas` subsystem's `Rgba32`
+unit — see _ChartModel Subsystem Design_ (`../chart-model.md`)'s Dependencies section.
diff --git a/docs/design/canvas-net-charts/chart-model/chart-renderer.md b/docs/design/canvas-net-charts/chart-model/chart-renderer.md
new file mode 100644
index 00000000..3d18474a
--- /dev/null
+++ b/docs/design/canvas-net-charts/chart-model/chart-renderer.md
@@ -0,0 +1,103 @@
+### ChartRenderer
+
+The `ChartRenderer` unit is the second software unit in the `ChartModel` subsystem. It comprises
+three public types, all in the `DemaConsulting.CanvasNet.Charts` namespace:
+
+- `ChartRenderer` (static class): the public rendering entry point, exposing two `Render`
+ overloads and dispatching per `ChartType` to the appropriate painter
+- `ChartRenderOptions` (sealed class): an immutable options bag for `ChartRenderer.Render` — a
+ background color, an optional font override, per-element default font sizes, and an optional
+ color-palette override — with a `Default` singleton
+- `ChartColorPalette` (static class): a fixed, documented 10-entry categorical color palette,
+ exposed as `ChartColorPalette.Default`
+
+plus a private, file-partitioned implementation spread across `ChartRenderer.Layout.cs`,
+`ChartRenderer.CategoryChart.cs`, `ChartRenderer.PieChart.cs`, `ChartRenderer.Legend.cs`, and
+`ChartRenderer.Title.cs` (all `partial` continuations of the one public `ChartRenderer` class).
+
+#### Purpose
+
+`ChartRenderer` paints a validated `ChartDocument` unit's `Chart` onto a new core `CanvasNet`
+`Canvas.Surface`, producing a deterministic, legible rendering with sensible defaults for every
+optional `Chart`/`ChartRenderOptions` setting a caller omits. It is implemented entirely against
+the core `CanvasNet` system's own public rendering primitives (`Surface`, `PathBuilder`,
+`TrueTypeFont`/text layout, `Rgba32`, `Matrix3x2`/`RectangleF`), with no third-party dependency
+and no reference to any other `DemaConsulting.CanvasNet.*` document-format package.
+
+`ChartRenderer.Render(Chart, int, int, ChartRenderOptions?)` is the primary entry point: it
+constructs a `Surface` of the requested pixel dimensions, fills it with
+`ChartRenderOptions.BackgroundColor`, computes a title/legend/plot-area layout, then dispatches
+per `Chart.Type`:
+
+- `Bar`/`Column`/`Line`/`Area` → the shared category-chart painter (`ChartRenderer.
+ CategoryChart.cs`), which also draws category/value axes and opt-in data labels
+- `Pie`/`Doughnut` → the wedge painter (`ChartRenderer.PieChart.cs`), via `PathBuilder.ArcTo`
+
+`ChartRenderer.Render(Chart, float, float, float, ChartRenderOptions?)` is a convenience overload
+that computes pixel dimensions as `round(widthInches * dpi)`/`round(heightInches * dpi)` and
+delegates to the primary overload. Unlike `PdfDocument.Render(int, float, PdfRenderOptions?)`,
+which scales a page's own intrinsic physical size, a `Chart` carries no intrinsic physical
+size — this overload exists purely so a caller who prefers to reason in physical units/DPI (for
+example, to match a target print or slide size) need not compute the pixel dimensions by hand;
+its behavior is otherwise identical to the primary overload once the pixel dimensions are known.
+
+Both overloads validate eagerly, before constructing anything:
+
+- `chart` is `null` → `ArgumentNullException`
+- The requested (or DPI-computed) pixel width/height is not positive, or exceeds
+ `Surface.MaxDimension` → `ArgumentOutOfRangeException`
+- The DPI overload's `widthInches`/`heightInches`/`dpi` is not finite or not positive →
+ `ArgumentOutOfRangeException`
+
+Every other rendering decision degrades gracefully rather than throwing:
+
+- A render target too small to reserve a title/legend/axis band simply skips that band (see
+ `ChartRenderer.Layout.cs`), rather than throwing or painting a corrupted layout
+- A legend with more entries than fit its reserved band omits the entries that do not fit,
+ rather than overlapping the plot area or throwing
+- A category/legend label too long to fit its available width is truncated, rather than
+ overflowing its region or throwing
+- A single-value `Pie`/`Doughnut` series, or a series whose total is zero, paints a well-defined
+ degenerate full-circle (or omits wedges) rather than dividing by zero
+
+#### Design
+
+- `ChartRenderer.cs` — the public `Render` overloads, argument validation, background fill, and
+ per-`ChartType` dispatch
+- `ChartRenderer.Layout.cs` — internal layout computation: reserves a title band (when
+ `Chart.Title` is set and the render target is tall enough), a legend band (when
+ `Chart.Legend`/`ChartRenderOptions` requests a visible legend and the render target has room,
+ sized per `ChartLegendPosition`), and the remaining plot-area rectangle
+- `ChartRenderer.CategoryChart.cs` — the shared `Bar`/`Column`/`Line`/`Area` painter: value-range
+ resolution (from `ChartAxis.Minimum`/`Maximum`, or the data's own min/max with zero included),
+ axis gridline/tick-label painting, per-series/per-point bar/segment painting, and opt-in
+ data-label painting
+- `ChartRenderer.PieChart.cs` — the `Pie`/`Doughnut` wedge painter, converting each value's
+ proportional share into a start/sweep angle pair and drawing each wedge via
+ `PathBuilder.ArcTo`, with a documented degenerate-case rule (a single non-zero value, or a
+ zero-total series, paints the single remaining non-zero wedge as a full circle instead of
+ computing an empty 0° sweep)
+- `ChartRenderer.Legend.cs` — the legend painter: a color swatch and name label per series/point
+ (series-level for category charts, point-level for `Pie`/`Doughnut`), laid out per
+ `ChartLegendPosition`, truncating or omitting entries that do not fit
+- `ChartRenderer.Title.cs` — the title painter: centers `Chart.Title.Text` in the reserved title
+ band at `Chart.Title.FontSize` or `ChartRenderOptions.TitleFontSize`
+
+Every file's painting logic resolves series/point color via a single shared precedence: an
+explicit `ChartSeries.Color`/`PointColors` entry, else `Chart.ColorPalette`, else
+`ChartRenderOptions.ColorPalette`, else `ChartColorPalette.Default` — each resolved by index with
+modulo wraparound, so a chart with more series/points than palette entries still renders every
+one with a distinct (repeating) color rather than throwing or leaving a gap. Text is measured and
+drawn via `ChartRenderOptions.Font`, falling back to the bundled Liberation Sans Regular font
+(the same fallback font the core `CanvasNet` `Rendering` subsystem already bundles) when omitted.
+
+`ChartColorPalette.Default` is a fixed 10-entry array of `Rgba32` values (a standard qualitative
+categorical palette), documented inline with its exact RGB values and the fallback-by-index
+(modulo) resolution rule described above. `ChartRenderOptions` is a `sealed class` with
+`init`-only properties and a `public static readonly ChartRenderOptions Default` singleton,
+mirroring `PptxRenderOptions`'s established shape so a caller already familiar with
+`CanvasNetPptx`'s rendering options recognizes the same pattern immediately.
+
+The `ChartModel` subsystem's dependencies gained by this unit (`Canvas.Surface`, `Drawing`,
+`Geometry`, `Fonts`, and `Rendering`) are documented in _ChartModel Subsystem Design_
+(`../chart-model.md`)'s Dependencies section.
diff --git a/docs/design/canvas-net-charts/open-xml-chart.md b/docs/design/canvas-net-charts/open-xml-chart.md
new file mode 100644
index 00000000..d0d737b0
--- /dev/null
+++ b/docs/design/canvas-net-charts/open-xml-chart.md
@@ -0,0 +1,50 @@
+## OpenXmlChart
+
+
+
+The `OpenXmlChart` subsystem is the second software subsystem in `CanvasNetCharts`, introduced in
+Phase 3. It groups one unit, `OpenXmlChartParser`, located in its own `src/
+DemaConsulting.CanvasNet.Charts/OpenXml/` sub-folder (distinct from `ChartModel`'s flat
+`src/DemaConsulting.CanvasNet.Charts/` folder).
+
+### Purpose
+
+The `OpenXmlChart` subsystem provides a single, format-agnostic capability: turning a raw ECMA-376
+DrawingML-Charts `c:chartSpace`/`c:chart` `System.Xml.Linq` element into the validated `Chart`
+the `ChartModel` subsystem's `ChartRenderer` already knows how to paint. It is deliberately
+format-agnostic — it has no knowledge of OPC/ZIP packaging, part relationships, or any specific
+host document format (PresentationML/`.pptx`, SpreadsheetML/`.xlsx`, or a future `.vsdx`) — the
+`CanvasNetPptx` integration is responsible for locating and opening the relevant `chart#.xml`
+part and handing this subsystem only the resulting XML content. This
+boundary is what keeps `OpenXmlChartParser` reusable by any future document-format library that
+embeds an OOXML chart part, not coupled to one specific host format. `OpenXmlChartParser` reads
+only cached values (`c:numCache`/`c:strCache`) and never recomputes from a `c:f` formula, mirroring
+`CanvasNetPptx`'s own documented `` "paint the cached value, never recompute" convention.
+
+### Units
+
+- **OpenXmlChartParser** — the `OpenXmlChartParser` static class and its
+ `ChartUnsupportedFeatureException`; see
+ _OpenXmlChartParser Unit Design_ (`open-xml-chart/open-xml-chart-parser.md`)
+
+### Dependencies
+
+The `OpenXmlChart` subsystem depends on the `ChartModel` subsystem's `ChartDocument` unit (its
+`Chart`/`ChartSeries`/`ChartAxis`/`ChartLegend`/`ChartTitle`/`ChartType` model types, which
+`OpenXmlChartParser` constructs directly), and, beyond that, only the .NET base class library's
+`System.Xml.Linq` (`XDocument`/`XElement`/`XNamespace`, for XML parsing) and `System.Globalization`
+(`CultureInfo.InvariantCulture`, for formatting a numeric category label deterministically) — see
+_CanvasNetCharts System Design_ (`../canvas-net-charts.md`)'s Dependencies section. It introduces
+no new runtime NuGet dependency, and - per that same System Design document's hard architectural
+constraint - must never reference `CanvasNetPptx`, `CanvasNetPdf`, `CanvasNetSvg`, or a future
+`CanvasNetVsdx`.
+
+### Callers
+
+`OpenXmlChartParser` is a public API entry point, invoked directly by consumers of the
+`DemaConsulting.CanvasNet.Charts` package, and is also invoked by `CanvasNetPptx`'s own
+chart-rendering integration (`PptxDocument.Charts.cs`), which locates and opens the relevant
+`chart#.xml` OPC part before handing its content to `OpenXmlChartParser`. No unit within the
+`OpenXmlChart` subsystem is called by any other subsystem of `CanvasNetCharts` itself as of this
+release; `OpenXmlChartParser` itself calls `ChartModel`'s `ChartDocument` unit, the one
+intra-system subsystem-to-subsystem call `OpenXmlChart` contains.
diff --git a/docs/design/canvas-net-charts/open-xml-chart/open-xml-chart-parser.md b/docs/design/canvas-net-charts/open-xml-chart/open-xml-chart-parser.md
new file mode 100644
index 00000000..fa9ba285
--- /dev/null
+++ b/docs/design/canvas-net-charts/open-xml-chart/open-xml-chart-parser.md
@@ -0,0 +1,98 @@
+### OpenXmlChartParser
+
+The `OpenXmlChartParser` unit is the first (and, as of this release, only) software unit in the
+`OpenXmlChart` subsystem. It comprises two public types, both in the
+`DemaConsulting.CanvasNet.Charts.OpenXml` namespace:
+
+- `OpenXmlChartParser` (static class): `Parse(XDocument)` and `Parse(XElement)` - the unit's one
+ entry point
+- `ChartUnsupportedFeatureException` (sealed class, derives `IOException`): carries a short,
+ stable `Feature` token, mirroring `CanvasNetPptx`'s own `PptxUnsupportedFeatureException` shape
+ (five parallel constructors, an `IOException` base) without referencing that type
+
+#### Purpose
+
+`OpenXmlChartParser` lets a caller turn a raw `c:chartSpace` (or bare `c:chart`) XML element into
+a validated `Chart`, reading only cached values (`c:numCache`/`c:strCache`) and never
+recomputing from the sibling `c:f` formula a spreadsheet application would use. It supports the
+five chart kinds `ChartType` models:
+
+| OOXML element | Classified `ChartType` |
+| ------------- | ----------------------- |
+| `c:barChart` with `c:barDir val="bar"` | `Bar` |
+| `c:barChart` with `c:barDir val="col"` or no `c:barDir` | `Column` |
+| `c:lineChart` | `Line` |
+| `c:pieChart` | `Pie` |
+| `c:doughnutChart` | `Doughnut` |
+| `c:areaChart` | `Area` |
+
+and throws `ChartUnsupportedFeatureException` - with a dedicated, documented `Feature` token - for
+every other schema-legal plot-area shape:
+
+| Plot-area shape | `Feature` token |
+| ---------------- | --------------- |
+| No recognized chart-type element | `charts-openxml-no-chart-type` |
+| More than one recognized chart-type element (a combo chart) | `charts-openxml-combo-chart` |
+| `c:radarChart` | `charts-openxml-radar-chart` |
+| `c:scatterChart` | `charts-openxml-scatter-chart` |
+| `c:bubbleChart` | `charts-openxml-bubble-chart` |
+| `c:stockChart` | `charts-openxml-stock-chart` |
+| `c:surfaceChart` / `c:surface3DChart` | `charts-openxml-surface-chart` |
+| `c:bar3DChart` / `c:line3DChart` / `c:pie3DChart` / `c:area3DChart` | `charts-openxml-3d-chart` |
+| `c:ofPieChart` | `charts-openxml-of-pie-chart` |
+| A series' `c:val` with no cached `c:numCache` | `charts-openxml-uncached-values` |
+
+Beyond chart-kind classification and the cached-values-only check above, `OpenXmlChartParser`
+performs no independent data-shape validation: every other invariant (non-empty series, finite
+values, matching category/value counts, a well-ordered value-axis range, and so on) is enforced
+exactly once, by `ChartDocument`'s own validating constructors, which this parser ultimately
+calls. A malformed cached value (for example, a series/category count mismatch) therefore
+surfaces the identical `ArgumentException` a directly constructed `Chart`/`ChartSeries`/
+`ChartAxis` would throw, not a parser-specific exception.
+
+#### Extraction Rules
+
+- **Series name** (`c:tx`): a cached `c:strRef`/`c:strCache` entry, or a literal `c:tx/c:v`,
+ falling back to a 1-based `"Series N"` default when `c:tx` is absent or carries no usable text
+ (never an error - unlike a missing value cache, a missing name has a safe, well-defined
+ fallback).
+- **Values** (`c:val`): the cached `c:val/c:numRef/c:numCache`, sized by `c:ptCount`, with a
+ sparse `c:pt` gap defaulting to `0.0`. Required - its absence throws
+ `ChartUnsupportedFeatureException` with `Feature` `"charts-openxml-uncached-values"`.
+- **Categories** (`c:cat`): a cached `c:strRef`/`c:strCache`, or a cached `c:numRef`/`c:numCache`
+ (converted to invariant-culture-formatted strings). For a category-based `ChartType` (`Bar`,
+ `Column`, `Line`, `Area`), only the first series' categories become the shared
+ `Chart.CategoryAxis.Labels` (every series of one chart-type element shares identical categories
+ in valid OOXML); for `Pie`/`Doughnut` (which have no per-category concept in the `ChartDocument`
+ model), each series' own categories instead become that series' own `ChartSeries.PointLabels`.
+- **Title** (`c:title`, and identically for `c:catAx`/`c:valAx`'s own `c:title`): a cached
+ `c:tx/c:strRef/c:strCache`, a rich-text `c:tx/c:rich` body (every descendant `a:t` run
+ concatenated), or a literal `c:tx/c:v`. An absent `c:title`, a `c:title` with no `c:tx` child
+ (a styled-but-textless PowerPoint auto-title placeholder - a real shape, confirmed directly
+ against a genuine `chart1.xml` part), or empty/whitespace-only resolved text, all map to `null`.
+- **Legend** (`c:legend`/`c:legendPos`): an absent `c:legend` maps to `null`; a present one maps
+ `t`/`b`/`l`/`r` to the identically named `ChartLegendPosition`, and approximates `tr`
+ (top-right - not representable by `ChartLegendPosition`'s five-value enumeration) and any
+ missing/unrecognized value to `Right` (ECMA-376's own documented default).
+- **Value axis** (`c:valAx`): `c:scaling/c:min`/`c:max` map to `Minimum`/`Maximum`, `c:majorUnit`
+ maps to `TickInterval`, and `c:valAx`'s own `c:title` maps to `Title`; an absent `c:valAx`, or
+ one carrying none of those four values, maps to a `null` `Chart.ValueAxis`.
+
+#### Dependencies
+
+`OpenXmlChartParser` depends on the `ChartModel` subsystem's `ChartDocument` unit (the model
+types it constructs), and, beyond that, only `System.Xml.Linq` (`XDocument`/`XElement`/
+`XNamespace`, for XML traversal) and `System.Globalization.CultureInfo.InvariantCulture` (for
+deterministic numeric-category formatting) - both part of the .NET base class library, available
+on every one of `CanvasNetCharts`'s target frameworks with no new runtime NuGet dependency. It
+has no knowledge of, and no dependency on, OPC/ZIP packaging or any host document-format package
+(`CanvasNetPptx`, `CanvasNetPdf`, `CanvasNetSvg`, or a future `CanvasNetVsdx`).
+
+#### Callers
+
+`OpenXmlChartParser` is a public API entry point, invoked directly by consumers of the
+`DemaConsulting.CanvasNet.Charts` package, and is invoked by `CanvasNetPptx`'s own
+chart-rendering integration (`PptxDocument.Charts.cs`), which locates and opens the relevant
+`chart#.xml` OPC part and hands this unit only the resulting XML content. It calls
+`ChartModel`'s `ChartDocument` unit (`Chart`, `ChartSeries`, `ChartAxis`, `ChartLegend`,
+`ChartTitle` constructors) as its own final step.
diff --git a/docs/design/canvas-net-pdf/pdf-document.md b/docs/design/canvas-net-pdf/pdf-document.md
index d671e1dc..016aad13 100644
--- a/docs/design/canvas-net-pdf/pdf-document.md
+++ b/docs/design/canvas-net-pdf/pdf-document.md
@@ -535,6 +535,25 @@ its own distinguishable `Feature` token.
rule. Every path-painting operator, including `n`, always clears the current path afterward
(`_pathBuilder.Clear()`); the surrounding graphics state is entirely unaffected by that clear,
so a subsequent path in the same content stream still sees the same CTM/stroke style/color.
+ - **Shared `StrokeOutliner` false-collapse investigation (regression fix note).** A separate
+ investigation into a PPTX rendering regression (a `` shape's thin stroke outline
+ rendering as a solid-filled interior instead of a thin ring) traced the root cause to the
+ shared core `Drawing.StrokeOutliner`'s inner-ring collapse detector, which used a fixed
+ absolute tolerance that false-positives when stroking a closed, curved contour tessellated
+ finely relative to its own large coordinate magnitude (see
+ `docs/design/canvas-net/drawing/path-stroker.md`'s "Inner-ring collapse" section). `PaintStroke`
+ above shares this exact `Drawing.PathStroker.Stroke`/`StrokeOutliner` code path, so the
+ investigation confirmed the same underlying defect is reproducible here too, with plain
+ large-magnitude device-space coordinates independent of PPTX/EMU. In practice, `PaintStroke`
+ strokes `path` _after_ it is already device-space-baked (unlike the PPTX caller that was
+ stroking in native, pre-transform EMU space) and typical PDF device-space coordinates stay at
+ a modest, page/pixel scale, so this defect was judged unlikely to manifest for ordinary PDF
+ documents - no `PdfDocument` source change was required. The shared `StrokeOutliner` tolerance
+ fix itself was still made (hardening every caller, including this one, against unusually large
+ pages/zoom levels), and a dedicated regression test,
+ `PdfDocumentTests.PdfDocument_PathOps_StrokeOnlyClosedBezierCircle_RendersThinRingNotSolidDisc`,
+ confirms a stroke-only, unfilled closed Bezier-curve circle continues to render as a ring
+ (not a solid disc) through this exact code path.
- **Device color operators (`PdfDocument.Color.cs`, added in Phase 3; color-space model replaced
in Phase 14)** — a private, immutable `PdfColorSpace` class (nested `Family` enum:
`DeviceGray`/`DeviceRGB`/`DeviceCMYK`/`Indexed`, with shared `DeviceGray`/`DeviceRGB`/
@@ -650,7 +669,16 @@ its own distinguishable `Feature` token.
image.Height)`, since image sample row `0` is the _top_ of the unit square per the PDF
specification's image-space convention, the opposite of user-space's y-up convention) — no
bilinear interpolation, a documented Phase 3 simplification consistent with Phase 2's own
- stroke-width simplification precedent.
+ stroke-width simplification precedent. Each sampled source pixel is alpha-blended "over" the
+ existing destination pixel via the shared internal `Canvas.Rgba32.CompositeOver(Rgba32, Rgba32)`
+ helper (standard Porter-Duff "over" alpha compositing — straight/unassociated alpha in and out,
+ `outA = fgA + bgA * (1 - fgA)`, each color channel `outC = (fgC * fgA + bgC * bgA * (1 - fgA)) /
+ outA` when `outA != 0` else `0`, round-half-away-from-zero, clamped to `[0, 255]`) rather than
+ overwritten outright — a documented fix (every decoded PDF image XObject is currently always
+ fully opaque per the `/SMask`/`/Mask` limitation noted above, so this matters only for a future
+ phase that decodes a non-opaque alpha channel, or for this same helper's shared reuse by
+ `DemaConsulting.CanvasNet.Pptx`'s own `PaintPicture`, which does sample genuinely non-opaque
+ source pixels today — see that package's own design documentation).
- **CCITT Group 4 fax decoding (`PdfDocument.CcittFax.cs`, added in Phase 15)** — a from-scratch
ITU-T T.6 decoder, implementing two-dimensional MMR coding only (`/K` must be negative;
non-negative `/K`, i.e. Group 3, and `/EndOfLine true` are rejected up front by the top-level
@@ -1047,13 +1075,24 @@ its own distinguishable `Feature` token.
bundled Liberation Sans/Serif/Mono font (which is itself already process-lifetime-cached by
`SystemFontCatalog`). This process-lifetime cache is deliberately broader-scoped than
`_fontCache`'s per-`Render` call scope, since a system or bundled font file's bytes never
- change between calls or between documents.
+ change between calls or between documents. **This ordinary (non-Symbol/non-ZapfDingbats) branch
+ now returns a 2-element candidate list** - `[primaryMatch, Fonts.SystemFontCatalog.
+ LoadBundledFallback(flavor)]`, where `primaryMatch` is whichever of the matched-system-font or
+ already-bundled-fallback result the paragraph above resolved - reusing the same composite
+ glyph-lookup mechanism the Symbol/ZapfDingbats path already established (see
+ `ResolvedSimpleFont.Resolve` immediately below) rather than introducing a second, parallel
+ mechanism: a codepoint the primary match itself does not cover now falls through to the bundled
+ Liberation fallback's own glyph for that codepoint (closing the "tofu box"/missing-glyph gap for
+ an otherwise-fully-resolved ordinary font whose own coverage is merely incomplete), instead of
+ painting `.notdef` outright. Primary-font-first ordering means a font with full coverage is
+ completely unaffected - the bundled fallback is only ever consulted on an actual per-codepoint
+ glyph-0 miss.
`BuildResolvedSimpleFont`'s `ResolvedSimpleFont.Fonts` property (plural, renamed from the
earlier single `Font` property) is an ordered, non-empty `IReadOnlyList` -
- a single-element list for every pre-existing resolution path (embedded `/FontFile2`, a matched
- system font, or the bundled Liberation fallback), and only ever multi-element for the Symbol
- Noto-substitute union above. `ResolvedSimpleFont.Resolve(code)` (the sole
+ a single-element list only for the embedded `/FontFile2` resolution path now, and multi-element
+ for every fallback-substitution path (the ordinary 2-element primary/bundled-fallback list
+ above, and the Symbol Noto-substitute union below). `ResolvedSimpleFont.Resolve(code)` (the sole
`IResolvedFont.Resolve` implementation this concerns) tries each font in `Fonts` in priority
order, returning the first one whose `GetGlyphIndex` for the code's mapped codepoint is
non-zero (its advance width is derived from that winning font), falling back to `Fonts[0]` and
diff --git a/docs/design/canvas-net-pptx.md b/docs/design/canvas-net-pptx.md
new file mode 100644
index 00000000..e9508038
--- /dev/null
+++ b/docs/design/canvas-net-pptx.md
@@ -0,0 +1,272 @@
+# System Design
+
+This document provides the system-level design for CanvasNetPptx.
+
+
+
+
+
+## Architecture
+
+CanvasNetPptx is a .NET library providing PowerPoint (`.pptx`) presentation-rendering support,
+distributed as its own NuGet package (`DemaConsulting.CanvasNet.Pptx`, namespace
+`DemaConsulting.CanvasNet.Pptx`), independent of, but depending on, the core `CanvasNet` system
+(its own separate package, `DemaConsulting.CanvasNet`) — see the Dependencies section below. The
+system consists of a single implemented unit:
+
+- **PptxDocument** (namespace `DemaConsulting.CanvasNet.Pptx`, folder
+ `src/DemaConsulting.CanvasNet.Pptx/`, flat — no further nesting): a hand-rolled OOXML (Office
+ Open XML) package parser and, in later phases, presentation-rendering codec. See _PptxDocument
+ Unit Design_ (`canvas-net-pptx/pptx-document.md`).
+
+`CanvasNetPptx` is modeled as its own top-level software system (rather than as a further unit of
+the `CanvasNet` system's `Codecs` subsystem) because it is distributed as its own
+independently-versioned NuGet package — per `software-items.md`'s rule that a software package
+contains exactly one software system, a system whose entire content ships in a distinct package
+must itself be modeled as a distinct system, not folded into the package it depends on.
+`CanvasNetPptx` contains exactly one unit, so no Subsystem tier is interposed between the system
+and `PptxDocument`: inserting one here would not separate anything (there is nothing else in the
+system to separate it from), mirroring `CanvasNetPdf`'s own single-unit precedent (see
+_CanvasNetPdf System Design_, `docs/design/canvas-net-pdf.md`).
+
+**Phased delivery — this is an in-progress, multi-phase feature.** A `.pptx` file is an OOXML
+package: a ZIP archive whose parts (`[Content_Types].xml`, `_rels/.rels`, `ppt/presentation.xml`,
+each slide/layout/master XML part, and so on) are discovered by following a chain of
+relationships from the package root. **Phase 1a** implemented only this underlying package layer:
+`PptxDocument.Open` opens a `.pptx` file as a read-only `ZipArchive`, resolves
+`[Content_Types].xml`'s default extension-to-content-type mappings and part-specific overrides,
+and resolves package-level (`_rels/.rels`) and per-part (`{dir}/_rels/{partName}.rels`)
+relationships, including OPC's relative-target resolution rules (a `"../"` parent-directory
+traversal segment, resolved relative to the referencing part's own directory, not the package
+root). `Open` succeeds on any well-formed OOXML/ZIP package, even one that is not actually a
+presentation, since nothing beyond the package layer was validated that phase.
+
+**Phase 1b** adds the presentation/theme/master/layout/slide model and the
+placeholder property-inheritance resolver, building on the Phase 1a package layer: `Open` now
+additionally locates `ppt/presentation.xml` (via the package's `/officeDocument` relationship),
+parses its declared slide size and ordered slide list (exposed as the new public `SlideSize`/
+`SlideCount` properties and `GetSlideSizeInPixels` conversion method), and rejects a package that
+is not a navigable presentation with at least one declared slide. Each slide master/layout/slide's
+placeholder shapes, and a slide master's theme (color/font scheme, resolved into
+`DemaConsulting.CanvasNet.Canvas.Rgba32` values), are resolved lazily on first access through new
+`internal` members. A new `internal` resolver (`ResolvePlaceholderProperties`) implements the
+verified ECMA-376 (ISO/IEC 29500) §19.3.1.36 placeholder matching algorithm, resolving a slide
+placeholder's effective ``/`/` property fragments through its
+layout and master. No shape geometry/paint rendering, non-placeholder (freeform) shape parsing,
+font loading, or rendering surface was implemented that phase - deferred to later phases.
+
+**Phase 1c** adds DrawingML shape geometry and paint resolution, building on the
+Phase 1b model: a shape's `` position/rotation/flip resolves to a transform; a
+`` group's `chOff`/`chExt` child coordinate space composes correctly with its children's
+own transforms; a shape's `` (24 supported preset names) or `` (parsed
+`moveTo`/`lnTo`/`cubicBezTo`/`quadBezTo`/`close` path commands) resolves to a concrete path; and a
+shape's ``/``/`` fill and `` stroke resolve to concrete
+paint, including scheme-color lookup through the resolved theme, the `lumMod`/`lumOff`/`shade`/
+`tint`/`alpha` color-transform pipeline, and a stroked outline realized via the core
+`PathStroker`. Pattern/picture fill, radial/path gradients, `` adjustment-value parsing,
+and full group-shape rendering semantics beyond transform composition remain explicitly deferred
+(see _PptxDocument Unit Design_'s "Geometry and Paint (Phase 1c)" section for the complete
+supported/deferred boundary). **No non-placeholder (freeform) shape _enumeration_ from a slide's
+full ``, font loading, or rendering surface is implemented yet** - all deferred to later
+phases, which will build on this model to actually render a presentation's slides.
+
+**Phase 1d (this release)** adds DrawingML text body parsing, attribute-level run/paragraph
+property inheritance (including a slide master's own `` title/body/other style
+buckets), word-wrap/alignment/vertical-anchor/autofit text layout, and glyph-ink rendering onto a
+core `Canvas.Surface`, building on the Phase 1b/1c models: a shape's `` resolves to an
+ordered paragraph/run structure; each run's/paragraph's effective properties resolve through the
+placeholder/layout/master/theme inheritance chain, attribute-by-attribute; the resolved text is
+word-wrapped, aligned, vertically anchored, and (where a `normAutofit` element is present)
+autofit within the owning shape's own bounding box; and the resulting glyphs are painted onto a
+`Canvas.Surface` via the core `Fonts.TrueTypeFont`/`SystemFontCatalog` and `Drawing.PathFiller`,
+mirroring `CanvasNetPdf`'s own glyph-painting pattern. Full text justification,
+`` shape-resize autofit, kerning, text clipping on overflow, and a full per-slide
+rendering entry point remain explicitly deferred (see _PptxDocument Unit Design_'s "Text Layout
+and Rendering (Phase 1d)" section for the complete supported/deferred boundary; bullets/numbering
+were deferred at that time but were later implemented by a Phase 2 Follow-Up). **No non-
+placeholder (freeform) shape _enumeration_ from a slide's full ``, nor a full per-slide
+public rendering entry point, is implemented yet** - deferred to a later phase.
+
+## External Interfaces
+
+The system exposes the following public API to external consumers, all on the sealed,
+`IDisposable` `PptxDocument` class:
+
+- **PptxDocument.Open(Stream stream)** / **PptxDocument.Open(string path)**: Opens and parses a
+ `.pptx` package exactly once, from a stream or a file path, buffering the input fully in
+ memory. `Open(Stream)` never takes ownership of (or disposes) the caller's stream. `Open(string)`
+ opens, reads, and closes its own internal `FileStream` before returning. Throws
+ `ArgumentNullException` for a null `stream`/`path`, `ArgumentException` for an
+ empty/whitespace-only `path`, and `InvalidDataException` for an unreadable/corrupt ZIP, a
+ package missing `[Content_Types].xml`/`_rels/.rels`, or (as of Phase 1b) a package that is not
+ a navigable presentation with a valid slide size and at least one declared slide.
+- **PptxDocument.SlideCount** (`int`, Phase 1b): The presentation's declared slide count.
+- **PptxDocument.SlideSize** (`PptxSlideSize`, Phase 1b): The presentation's declared slide size,
+ in EMU.
+- **PptxDocument.GetSlideSizeInPixels(float dpi)** (Phase 1b): Converts `SlideSize` to pixels at
+ the given resolution. Throws `ArgumentOutOfRangeException` for a non-positive/non-finite `dpi`.
+- **PptxDocument.Dispose()**: Idempotent; releases the underlying `ZipArchive` and its backing
+ buffer. No other public member may be called afterward without throwing
+ `ObjectDisposedException` (enforced, as of Phase 1b, by `SlideCount`/`SlideSize`/
+ `GetSlideSizeInPixels`).
+
+
+| Interface | Direction | Format | Constraints |
+| -------------------------------------- | ---------------- | ------------------------------------ | ------------------------------- |
+| `PptxDocument.Open(...)` | Inbound/Outbound | Method call / `PptxDocument` return | Valid `.pptx`/OPC presentation stream or path |
+| `PptxDocument.SlideCount` / `SlideSize` | Outbound | Property access | Document must not be disposed |
+| `PptxDocument.GetSlideSizeInPixels(...)` | Inbound/Outbound | Method call / `(int, int)` return | Positive, finite `dpi`; document must not be disposed |
+| `PptxDocument.Dispose()` | Inbound | Method call | Safe to call more than once |
+
+
+The package layer's `ResolvePart`/`ResolveRelationship` methods, and every Phase 1b
+presentation/theme/master/layout/slide/inheritance member, are `internal` (not part of the
+public surface), used by later phases of `PptxDocument` itself to navigate the package and
+resolve placeholder properties - see _PptxDocument Unit Design_
+(`canvas-net-pptx/pptx-document.md`) for their complete contract.
+
+## Dependencies
+
+`CanvasNetPptx` depends on the separate `CanvasNet` system (its own package,
+`DemaConsulting.CanvasNet`, referenced via a project reference from
+`src/DemaConsulting.CanvasNet.Pptx/DemaConsulting.CanvasNet.Pptx.csproj`), matching every other
+package-bounded system in this repository's architecture convention of depending on the core
+system for its shared types (`Canvas.Surface`, `Codecs.UnsupportedImageFeatureException`, and so
+on, as later phases add presentation rendering).
+
+**As of Phase 1b, `CanvasNetPptx` uses the `CanvasNet` core system's `Canvas` subsystem** -
+specifically `Canvas.Rgba32`, to represent a theme's resolved 12-slot color scheme (each
+``/`` slot resolved to a concrete `Rgba32` value). **As of Phase 1c,
+`CanvasNetPptx` additionally uses the `CanvasNet` core system's `Geometry` and `Drawing`
+subsystems** - `Geometry.Path`/`PathBuilder` to build and compose resolved shape geometry, and
+`Drawing.PathStroker`/`StrokeStyle` to realize a resolved stroke's outline, plus `Canvas.Gradient`/
+`LinearGradient` to represent a resolved gradient fill. **As of Phase 1d (this release),
+`CanvasNetPptx` additionally uses the `CanvasNet` core system's `Fonts` subsystem** -
+`Fonts.TrueTypeFont`/`SystemFontCatalog` to resolve a DrawingML typeface name hint to a concrete
+font and extract its glyph outlines - and the core `Drawing.PathFiller`/`Canvas.Surface` types to
+paint resolved glyph ink onto a rendering surface, mirroring `CanvasNetPdf`'s own
+font-resolution/glyph-painting pattern. **As of Phase 4, `CanvasNetPptx` additionally depends on
+the sibling `CanvasNetCharts` system** (its own separate package,
+`DemaConsulting.CanvasNet.Charts`) - specifically `DemaConsulting.CanvasNet.Charts.OpenXml`'s
+`OpenXmlChartParser`, to parse a ``'s referenced `chart#.xml` OPC part into a
+`Chart`, and `DemaConsulting.CanvasNet.Charts`'s `ChartRenderer`, to render that `Chart` onto a
+dedicated `Surface` before compositing it onto the slide via the existing `PaintPicture`
+primitive - see `docs/design/canvas-net-pptx/pptx-document.md`'s "Chart Graphic Frames (Phase 4)"
+design section for the full integration. This dependency is strictly one-directional:
+`CanvasNetCharts` must never reference `CanvasNetPptx` (see `docs/design/canvas-net-charts.md`'s
+own Dependencies section), so `CanvasNetPptx` depends on `CanvasNetCharts` exactly as it already
+depends on the core `CanvasNet` system, never the reverse. The package layer itself still relies
+solely on the .NET base class library's `System.IO.Compression.ZipArchive` (reading
+the `.pptx` ZIP container) and `System.Xml.Linq.XDocument`/`XElement` (parsing
+`[Content_Types].xml`, each `.rels` part, and, as of Phase 1b, every presentation/theme/master/
+layout/slide part). The `ProjectReference` to `CanvasNet` now has corresponding `dependency`
+edges in this system's own SysML2 model (`docs/sysml2/model/canvas-net-pptx.sysml`):
+`dependency usesCanvas from CanvasNetPptxSystem to Canvas;` (added at Phase 1b, the phase that
+actually first uses a `Canvas` subsystem type) and, added at Phase 1c,
+`dependency usesGeometry from CanvasNetPptxSystem to Geometry;`/
+`dependency usesDrawing from CanvasNetPptxSystem to Drawing;`, mirroring `CanvasNetPdf`'s and
+`CanvasNetSvg`'s own precedent of adding a `dependency` edge only at the phase that actually
+first uses the referenced subsystem (for example `CanvasNetPdf`'s `Fonts` dependency was added
+only at its own Phase 4, not at Phase 1). Added this phase (Phase 1d), mirroring that same
+precedent: `dependency usesFonts from CanvasNetPptxSystem to Fonts;`. No other `CanvasNet`
+subsystem (`Codecs`) is used yet; a future phase that implements picture fill may add a further
+`dependency` edge at that time. Added at Phase 4 (this release), mirroring that same per-phase
+pattern but targeting a sibling _system_ rather than a `CanvasNet` subsystem:
+`dependency usesCharts from CanvasNetPptxSystem to CanvasNetCharts;` - the SysML2 edge for the
+new, one-directional `ProjectReference` from `DemaConsulting.CanvasNet.Pptx.csproj` to
+`DemaConsulting.CanvasNet.Charts.csproj` described above.
+
+This is an ordinary, same-repository, system-to-system dependency: both `CanvasNet` and
+`CanvasNetPptx` are produced by this repository, so it is neither an OTS Software Item (not a
+third-party/external-program dependency) nor a Shared Package (that category is scoped to a
+package produced by a _different_ repository within the same program). `CanvasNetPptx`
+introduces no new OTS Software Item beyond those already used to build and verify the
+`CanvasNet` system (BuildMark, FileAssert, Pandoc, ReqStream, ReviewMark, SarifMark, SonarMark,
+VersionMark, WeasyPrint, xUnit) — see _OTS Integration Design_ (`docs/design/ots.md`).
+
+## Risk Control Measures
+
+`PptxDocument.Open` parses an externally-supplied, potentially untrusted `.pptx` package (a ZIP
+archive whose structure, `[Content_Types].xml`, relationship parts, and - as of Phase 1b -
+presentation/theme/master/layout/slide XML content may be malformed, incomplete, or
+adversarially constructed). Risk control remains deliberately simple and entirely fail-closed,
+with no tolerated-recovery path (unlike `CanvasNetPdf`'s linear-scan fallback): an
+unreadable/corrupt ZIP, a missing `[Content_Types].xml` or `_rels/.rels`, an unresolvable
+relationship ID, an external-target relationship, a relationship target that would traverse past
+the package root, a package with no navigable presentation part, or a presentation with no valid
+slide size/slide list are all immediately rejected with a thrown `InvalidDataException` rather
+than silently producing an incomplete or incorrect package view. Phase 1b's per-slide/layout/
+master/theme validation is deliberately lazy (only performed when that specific part is first
+accessed via `GetSlide`/`GetLayout`/`GetMaster`/`GetTheme`), matching Phase 1a's own eager
+(package-level)/lazy (per-part) validation split - a documented scope boundary, not an oversight.
+Phase 1c's geometry/paint resolvers extend this same fail-closed philosophy to shape content: a
+malformed ``/``/color-definition element throws `InvalidDataException`
+exactly like a malformed Phase 1a/1b part, and a recognized-but-unsupported DrawingML construct
+(an unsupported preset geometry name, color-definition kind, gradient kind, or pattern/picture
+fill) throws the new `PptxUnsupportedFeatureException` rather than being silently substituted
+with incorrect geometry or paint - the same fail-closed-over-silent-approximation choice Phase
+1a/1b already made for the package layer.
+
+## Data Flow
+
+**`.pptx` open/parse path:**
+
+1. **Input**: A `.pptx` package (stream or file path)
+2. **Validation**: `Open` rejects a null `stream`/`path` with `ArgumentNullException` and an
+ empty/whitespace `path` with `ArgumentException`
+3. **Processing**: Fully buffers the input into memory, opens it as a read-only `ZipArchive`
+ (rejecting an unreadable/corrupt archive with `InvalidDataException`), parses
+ `[Content_Types].xml` into its default extension and part-specific override content-type
+ mappings, eagerly parses the package-level `_rels/.rels` relationships part (rejecting a
+ missing or malformed part with `InvalidDataException`), then locates and parses
+ `ppt/presentation.xml` (via the package's `/officeDocument` relationship), reading its
+ declared slide size and resolving its ordered slide list (rejecting a non-navigable or
+ structurally invalid presentation with `InvalidDataException`)
+4. **Output**: A new `PptxDocument` instance exposing `SlideCount`/`SlideSize`/
+ `GetSlideSizeInPixels` directly, and whose internal `ResolvePart`/`ResolveRelationship`/
+ `GetTheme`/`GetMaster`/`GetLayout`/`GetSlide`/`ResolvePlaceholderProperties` methods can now
+ navigate the package's content-type, relationship, and placeholder-inheritance graph — the
+ presentation-level state is parsed once at `Open()` time; each slide/layout/master/theme is
+ parsed lazily, on first access, and cached, reused across every subsequent call on that
+ instance
+
+## Design Constraints
+
+- **Simplicity**: Minimal functionality kept easy to understand and extend
+- **Compliance**: All functionality must be traceable to requirements
+- **Quality**: Zero warnings, full test coverage, complete documentation
+- **Portability**: Compatible across supported .NET platforms
+- **One-time parse, reused across calls**: `Open` performs the full package-layer parse exactly
+ once; later phases' presentation-specific members will all read from that already-resolved
+ state rather than re-parsing on every call
+- **Decode-only**: `PptxDocument` provides no encode/`Save` direction, matching every other
+ decode-only codec in this repository
+
+### Platform Support
+
+The library targets the following frameworks, identical to the `CanvasNet` system it depends on,
+enabling compatibility across modern, currently supported .NET runtimes:
+
+| Target Framework | Runtime / Environment |
+| ---------------- | --------------------- |
+| `net8.0` | .NET 8 LTS |
+| `net9.0` | .NET 9 |
+| `net10.0` | .NET 10 |
+
+The library is supported on the following operating systems:
+
+- **Windows** — primary developer and CI platform
+- **Linux** — CI/CD and containerized environments
+- **macOS** — developer workstations using Apple platforms
+
+Portability is achieved by restricting the implementation exclusively to Base Class Library (BCL)
+APIs and the `CanvasNet` system's own public API, available across all target frameworks. No
+platform-specific native interop, OS-specific APIs, or framework-version-specific features are
+used.
+
+### Integration Patterns
+
+- **NuGet Packaging**: Standard .NET library packaging and distribution, referencing the
+ `CanvasNet` package as an ordinary NuGet dependency
+- **CI/CD Integration**: Automated build, test, and quality validation
+- **Requirements Traceability**: All features linked to passing tests
diff --git a/docs/design/canvas-net-pptx/pptx-document.md b/docs/design/canvas-net-pptx/pptx-document.md
new file mode 100644
index 00000000..3d849a45
--- /dev/null
+++ b/docs/design/canvas-net-pptx/pptx-document.md
@@ -0,0 +1,3126 @@
+## PptxDocument
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`PptxDocument` is distributed as the separate `DemaConsulting.CanvasNet.Pptx` NuGet package
+(namespace `DemaConsulting.CanvasNet.Pptx`), which references the core `DemaConsulting.CanvasNet`
+package.
+
+The `PptxDocument` class is the sole software unit of the `CanvasNetPptx` system. As of Phase 1a,
+its dependencies were limited to the .NET base class library's `System.IO.Compression.ZipArchive`
+(reading the `.pptx` ZIP container) and `System.Xml.Linq` (parsing `[Content_Types].xml` and each
+`.rels` relationships part) — no `CanvasNet` core-system type was used yet. As of Phase 1c, the
+unit additionally depends on several core `DemaConsulting.CanvasNet` types - `Geometry.Path`/
+`PathBuilder`, `Drawing.PathStroker`/`StrokeStyle`, and `Canvas.Rgba32`/`Gradient`/
+`LinearGradient` - reusing the already-tested core geometry/drawing machinery rather than
+reimplementing it in this package (see _CanvasNetPptx System Design_'s Dependencies section,
+`../canvas-net-pptx.md`, and the _Geometry and Paint (Phase 1c)_ section below). As of Phase 1d
+(this release), the unit additionally depends on core `Fonts.TrueTypeFont`/`SystemFontCatalog`
+(font resolution and glyph-outline extraction) and `Drawing.PathFiller` (glyph-ink fill), reusing
+the same font-resolution/glyph-painting pattern `DemaConsulting.CanvasNet.Pdf` already established
+(see the _Text Layout and Rendering (Phase 1d)_ section below). As of Phase 1e (this release),
+the unit additionally reuses the raster codec dispatch pattern established by
+`DemaConsulting.CanvasNet.Svg`'s `SvgCodec.ResolveRasterDecoder` to decode a `` shape's
+embedded image (see the _Images, Tables, and Shape Tree (Phase 1e)_ section below); no new core
+`CanvasNet` type is introduced by Phase 1e beyond what Phase 1c/1d already established.
+
+### Package Layer (Phase 1a)
+
+A `.pptx` file is an OOXML (Office Open XML) package: a ZIP archive whose parts are discovered by
+following a chain of relationships, starting from the package root. `PptxDocument.Package.cs`
+implements this generic package layer, independent of any presentation-specific semantics:
+
+- **Opening the ZIP archive**: the private constructor buffers the entire input stream into a
+ byte array, wraps it in a non-writable `MemoryStream`, and opens that stream as a read-only
+ `ZipArchive`. Any failure to do so - an unreadable or corrupt (non-ZIP) stream - is wrapped in
+ `InvalidDataException` (`ZipArchive`'s own constructor already throws this type for a corrupt
+ archive; other failure shapes, such as `IOException`/`ArgumentException`/`NotSupportedException`/
+ `EndOfStreamException`, are caught and re-thrown wrapped in the same type, so every
+ package-layer failure is uniformly `InvalidDataException`).
+- **`[Content_Types].xml` resolution**: parsed once, at construction time, into two lookups - a
+ case-insensitive `Default` extension-to-content-type map (for example `"xml"` ->
+ `"application/xml"`) and an exact-match `Override` part-path-to-content-type map (for example
+ `"ppt/presentation.xml"` -> `"application/vnd.openxmlformats-officedocument.presentationml.
+ presentation.main+xml"`). `ResolvePart(string partPath)` consults the override map first,
+ falling back to the default map keyed by the part's file extension, and throws
+ `InvalidDataException` when the package does not contain the requested part, or when neither
+ map resolves a content type for it.
+- **Relationship resolution**: the package-level relationships (`_rels/.rels`) are parsed eagerly
+ at construction time (so a malformed package-level relationships part fails fast at `Open()`
+ time), while every other part's own relationships (`{dir}/_rels/{partName}.rels`) are parsed
+ lazily, on first access, and cached. `ResolveRelationship(string sourcePartPath, string
+ relationshipId)` (the empty string for `sourcePartPath` denotes the package root) looks up the
+ given relationship ID within the source part's own relationships, then resolves its `Target`
+ attribute per OPC's relative-reference rules: a target beginning with `'/'` is resolved relative
+ to the package root; any other target is resolved relative to the source part's own directory,
+ with `"."`/`".."` segments in the combined path normalized away exactly like a familiar
+ filesystem path (so, for example, `ppt/slides/slide1.xml`'s relationship to
+ `"../slideLayouts/slideLayout1.xml"` resolves to `"ppt/slideLayouts/slideLayout1.xml"`, not
+ `"ppt/slides/slideLayouts/slideLayout1.xml"`). An unknown relationship ID, a relationship whose
+ `TargetMode` is `"External"` (not supported this phase), or a target that would traverse past
+ the package root (more `".."` segments than preceding directory segments) all throw
+ `InvalidDataException`.
+
+**Phase 1a scope boundary**: no presentation-specific part (`ppt/presentation.xml`, a slide, a
+slide layout/master) is itself interpreted - `ResolvePart`/`ResolveRelationship` only resolve a
+part's content type and a relationship's target path, both of which are generic OPC package
+concepts. The `_entriesByPath`/`_defaultContentTypes`/`_overrideContentTypes`/
+`_relationshipCache` fields hold the entirety of this phase's parsed state.
+
+**Phase 1b additions**: `PackageRelationship` gained a `Type` field (the relationship's own
+`Type` attribute, captured by `ParseRelationships` alongside the existing `Target`/`TargetMode`
+fields). Four OOXML navigations used by Phase 1b (package root -> `ppt/presentation.xml`, slide
+-> layout, layout -> master, master -> theme) have no explicit `r:id` anywhere in the referencing
+part's own body XML - each is, by OOXML convention, simply "the referencing part's one
+relationship of a given `Type`". `ResolveRelationshipByType(string sourcePartPath, string
+relationshipTypeSuffix)` resolves such a relationship by an ordinal `EndsWith` match against each
+non-external relationship's `Type` URI (for example `"/officeDocument"`, `"/slideLayout"`,
+`"/slideMaster"`, `"/theme"`), throwing `InvalidDataException` when none match.
+`LoadPartXmlRoot(string partPath)` loads and parses a part's XML root element (checking the part
+exists first), reused by every Phase 1b parser (`InitializePresentation`/`GetTheme`/`GetMaster`/
+`GetLayout`/`GetSlide`) instead of each duplicating the same existence-check-then-parse logic.
+
+### Public API (Phase 1a)
+
+`PptxDocument.cs` implements the sealed, `IDisposable` public surface:
+
+- **`Open(Stream stream)`**: validates `stream` is non-null (`ArgumentNullException`), buffers it
+ fully into a byte array via `CopyTo` (so the caller's stream need not remain open or seekable
+ afterward, exactly like `PdfDocument.Open`'s own buffering pattern), and constructs a new
+ `PptxDocument` from the buffered bytes.
+- **`Open(string path)`**: validates `path` is non-null (`ArgumentNullException`) and not
+ empty/whitespace-only (`ArgumentException`), then opens a `FileStream` and delegates to
+ `Open(Stream)`.
+- **`Dispose()`**: idempotent (a `_disposed` flag guards a second call from re-disposing already-
+ released resources); disposes the underlying `ZipArchive` and its backing `MemoryStream`.
+
+### Presentation Parsing (Phase 1b)
+
+`PptxDocument.Presentation.cs` locates and parses `ppt/presentation.xml`, called once from the
+constructor (`InitializePresentation`), immediately after the Phase 1a package layer has parsed
+the package-level relationships:
+
+- **Locating the presentation part**: `ppt/presentation.xml` is not referenced by an explicit
+ `r:id` anywhere - it is the package's single `/officeDocument` relationship target, resolved
+ via `ResolveRelationshipByType(string.Empty, "/officeDocument")` (see the Package Layer
+ section's "Phase 1b additions" above). Its root element is validated to be a PresentationML
+ `` element, throwing `InvalidDataException` otherwise.
+- **Slide size**: `` is parsed into a `PptxSlideSize` (an EMU-unit
+ width/height record struct), exposed as the public `SlideSize` property. A missing ``,
+ or a missing/non-numeric/non-positive `cx`/`cy` attribute, throws `InvalidDataException`.
+- **Slide list**: ``'s `` children are resolved, in document
+ order, into a list of slide part paths (via the presentation part's own relationships),
+ exposed as the public `SlideCount` property (the resolved list's count). A missing
+ ``, an empty slide list, or any child with a missing or unresolvable `r:id` throws
+ `InvalidDataException`.
+- **Pixel conversion**: `GetSlideSizeInPixels(float dpi)` converts `SlideSize`'s EMU dimensions to
+ pixels at the caller's chosen resolution (914400 EMU per inch), rounding each dimension to the
+ nearest pixel (`MidpointRounding.AwayFromZero`) - the EMU analogue of `PdfDocument.Render(int,
+ float)`'s own points-to-pixels conversion, provided ahead of any Phase 1b rendering surface so a
+ later phase reuses this exact, already-tested arithmetic. Rejects a non-positive or non-finite
+ `dpi` with `ArgumentOutOfRangeException`.
+
+**Eager/lazy validation split**: only the presentation part itself, its slide size, and its slide
+list's relationship resolvability are validated eagerly, at `Open()` time. Each resolved slide
+part's own existence, well-formedness, and layout/master/theme chain are validated lazily, on
+first access via `GetSlide(int)` - mirroring Phase 1a's own eager (package-level relationships) /
+lazy (per-part relationships) split.
+
+### Theme Parsing (Phase 1b)
+
+`PptxDocument.Theme.cs` resolves a theme part (`ppt/theme/themeN.xml`, reached via a slide
+master's `/theme` relationship - see _Master/Layout/Slide Placeholder Models_ below) into a
+`PptxTheme`, lazily and cached by part path (`GetTheme(string themePartPath)`):
+
+- **Color scheme**: ``'s 12 named slots (`dk1`/`lt1`/`dk2`/`lt2`/`accent1`-`accent6`/
+ `hlink`/`folHlink`) are each resolved to a concrete `DemaConsulting.CanvasNet.Canvas.Rgba32`
+ value - an `` resolves directly via `Rgba32.Parse`; an
+ `` resolves to its cached `lastClr` RGB equivalent
+ (no actual OS system-color resolution is performed or possible offline). `Rgba32.Parse`'s own
+ `FormatException` on invalid hex is caught and re-thrown wrapped in `InvalidDataException`, so
+ every theme-parsing failure is uniformly `InvalidDataException` like every other structural
+ failure in this unit.
+- **Font scheme**: ``'s major (heading) and minor (body) typeface collections are
+ each resolved into a `PptxFontCollection` naming the Latin (``), East
+ Asian (``), and complex-script (``) typeface - a
+ missing `typeface` attribute defaults to an empty string (tolerated), but a missing ``/
+ ``/`` element itself throws `InvalidDataException`.
+
+This is `internal` state (`PptxTheme`/`PptxColorScheme`/`PptxFontScheme`/`PptxFontCollection` are
+all `internal` records), carried alongside the placeholder property-inheritance chain as context
+for a later rendering phase to resolve a scheme-color/font token - not itself part of the
+placeholder matching chain (see _Placeholder Inheritance Resolution_ below).
+
+### Master/Layout/Slide Placeholder Models (Phase 1b)
+
+`PptxDocument.Masters.cs`/`PptxDocument.Layouts.cs`/`PptxDocument.Slides.cs` structurally parse a
+slide master/layout/slide part, respectively, each lazily resolved and cached (`GetMaster(string
+masterPartPath)`, `GetLayout(string layoutPartPath)`, `GetSlide(int slideIndex)`):
+
+- **`GetMaster`**: validates the part's root is `` with a `/`
+ descendant, resolves the master's own `/theme` relationship (via `ResolveRelationshipByType`,
+ see the Package Layer section's "Phase 1b additions" above), and parses its immediate
+ placeholder shapes into a `PptxMaster` (part path, theme part path, placeholder list).
+- **`GetLayout`**: validates the part's root is ``, resolves the layout's own
+ `/slideMaster` relationship, and parses its immediate placeholder shapes into a `PptxLayout`
+ (part path, master part path, placeholder list).
+- **`GetSlide(int slideIndex)`**: range-checks `slideIndex` against `SlideCount`
+ (`ArgumentOutOfRangeException`), validates the slide part's root is ``, resolves the
+ slide's own `/slideLayout` relationship, and parses its immediate placeholder shapes into a
+ `PptxSlide` (part path, layout part path, placeholder list).
+
+All three share `PptxPlaceholder.cs`'s `PptxPlaceholderParser.ParsePlaceholderShapes`: it walks a
+``'s immediate `` children only (no group/recursive descent - Phase 1b does not
+parse freeform or grouped shapes), filters to those with a `//`
+descendant, and builds one `PptxPlaceholder` (type, idx, raw `` element) per match, applying
+the OOXML schema defaults (`type="obj"`, `idx="0"`) when the corresponding attribute is omitted.
+A non-placeholder `` (no `` descendant) is silently excluded, not an error.
+
+### Placeholder Inheritance Resolution (Phase 1b)
+
+`PptxDocument.Inheritance.cs`'s `ResolvePlaceholderProperties` implements the verified ECMA-376
+(ISO/IEC 29500) §19.3.1.36 placeholder matching algorithm - **two different, level-specific,
+single-criterion matches**, not a uniform "type+idx exact match, then type-only fallback":
+
+1. **Slide -> layout**: matched by `idx` ALONE. `type` is not consulted at this hop. A miss (no
+ layout placeholder shares the slide placeholder's `idx`) is a genuine miss - it is not retried
+ by type.
+2. **Layout -> master**: matched by `type` ALONE, through a fixed remapping table (`body`/`chart`/
+ `bitmap`/`orgChart`/`mediaClip`/`obj`/`pic`/`subTitle`/`tbl`/`clipArt`/`dgm`/`media` all remap
+ to `"body"`; `ctrTitle` remaps to `"title"`; every other type maps to itself), using the
+ _matched layout placeholder's own type_ (not the slide placeholder's type). `idx` is not
+ consulted at this hop. This hop only runs when hop 1 found a layout match - a hop 1 miss
+ short-circuits the chain entirely (no fallback to matching the master directly against the
+ slide's own type).
+
+Once the matched layout/master placeholders (if any) are determined, each of exactly two named
+property categories - `` and `/` - is independently resolved via a
+first-non-null-in-chain walk: slide -> matched layout -> matched master, taking the first element
+present at all; no deeper per-attribute merging is performed in Phase 1b. For `` itself
+(fill/line resolution), an empty element still counts as present, stopping the fallback for that
+category. `/` is the one named exception (see "Genuine bug 4" below): a
+tier's own empty, self-closing `` (no ``..`` level-override
+child) is walked past rather than "winning", mirroring the same empty-element-skipping precedent
+``/geometry resolution already establishes (see "Genuine bug 2" below). The resolved
+theme is carried through unchanged as context (`PptxPlaceholderProperties.Theme`) - it is not
+itself part of the matching chain, since a theme supplies scheme-level tokens a property fragment
+may reference, rather than containing placeholder-shaped XML to match against.
+
+### Public API (Phase 1b)
+
+`PptxDocument.cs` gains three new public members, each guarded by
+`ObjectDisposedException.ThrowIf(_disposed, this)`:
+
+- **`SlideCount`** (`int`): the presentation's declared slide count (`_slidePartPaths.Count`).
+- **`SlideSize`** (`PptxSlideSize`): the presentation's declared slide size, in EMU.
+- **`GetSlideSizeInPixels(float dpi)`** (`(int Width, int Height)`): converts `SlideSize` to
+ pixels at the given resolution - see _Presentation Parsing (Phase 1b)_ above.
+
+All other Phase 1b members (`GetTheme`/`GetMaster`/`GetLayout`/`GetSlide`/
+`ResolvePlaceholderProperties`, and every new record type) are `internal`, exercised by the test
+project via `InternalsVisibleTo`, matching Phase 1a's own `ResolvePart`/`ResolveRelationship`
+precedent - no public shape/rendering surface is introduced this phase.
+
+### Geometry and Paint (Phase 1c)
+
+Phase 1c adds DrawingML shape geometry and paint resolution: resolving a shape's transform,
+geometric path, fill, and stroke into the core `DemaConsulting.CanvasNet.Geometry`/
+`DemaConsulting.CanvasNet.Drawing`/`DemaConsulting.CanvasNet.Canvas` types a future rendering
+phase paints directly, rather than reinventing path/stroke/gradient machinery in this package.
+Every new member introduced this phase is `internal` - no public shape/rendering surface is
+introduced yet (matching Phase 1b's own precedent); this is confirmed by inspection, not merely
+asserted.
+
+#### Shape Frame Transform
+
+`PptxDocument.Geometry.cs`'s `ResolveShapeFrame(XElement xfrm)` resolves a shape's ``
+element into a `PptxShapeFrame` (an `internal sealed record` pairing a `System.Numerics.Matrix3x2`
+transform with the shape's own declared width/height in EMU):
+
+- **Position/size**: `` and `` are both
+ required; either missing throws `InvalidDataException`.
+- **Rotation**: `rot` (an optional attribute, 60,000ths of a degree, defaulting to `0` when
+ omitted) rotates the shape about its own center - `(w/2, h/2)` in local coordinates - matching
+ DrawingML's own rotation semantics (never about the parent origin). A non-numeric `rot` throws
+ `InvalidDataException`.
+ OOXML's `rot` is a clockwise angle in screen (y-down) coordinates; `Matrix3x2.CreateRotation`'s
+ standard mathematical rotation matrix, applied in this package's y-down coordinate space,
+ already turns a positive angle clockwise on screen (the y-axis flip inherent in screen
+ coordinates reverses the usual y-up counter-clockwise sense) - so `rot`'s 60,000ths-of-a-degree
+ value converts directly to radians with no extra sign negation needed.
+- **Flip**: `flipH`/`flipV` (optional boolean attributes, `"1"`/`"true"` truthy, defaulting to
+ `false`) mirror the shape about its own center's vertical/horizontal axis respectively, applied
+ _before_ rotation (matching DrawingML's own documented order of operations: flip, then
+ rotate, then translate).
+- **Composition order**: the final transform is `flip * rotate * translate(off)`, composed so
+ that `Vector2.Transform(localPoint, frame.Transform)` (row-vector convention, matching
+ `Path.Transform(Matrix3x2)`'s own convention) maps a point in the shape's own `(0,0)`-`(w,h)`
+ local coordinate space directly into its parent's coordinate space in one call.
+
+#### Group Shape Child Transform Composition
+
+A group shape (``) has its own `` carrying _two_ coordinate boxes: `off`/`ext`
+(the group's own placement, in its parent's coordinate space - resolved exactly like any other
+shape's frame via `ResolveShapeFrame`) and `chOff`/`chExt` (the child coordinate space every
+shape nested directly inside the group is authored in, which generally differs in both offset
+and scale from the group's own `off`/`ext` box).
+
+`ResolveGroupChildTransform(XElement groupXfrm)` resolves only the `chOff`/`chExt`-to-`off`/`ext`
+half of this composition - a `Matrix3x2` mapping a point in the group's _child_ coordinate space
+into the group's _parent_ coordinate space (not merely its own local box - the group's own
+resolved `ResolveShapeFrame` transform, including its own flip/rotation about its own center, is
+folded in as the final step). `chOff` defaults to `(0,0)` and `chExt` defaults to the group's own
+`ext` (`WidthEmu`/`HeightEmu`) when either is omitted - a common case for an unscaled,
+un-translated child coordinate space. A zero-width/height `chExt` (which cannot be meaningfully
+scaled from) falls back to an identity scale factor of `1` along that axis rather than dividing by
+zero.
+
+A child shape nested in the group resolves its _own_ `ResolveShapeFrame(childXfrm).Transform`
+exactly as it would as a top-level shape (its `` is still expressed in the group's child
+coordinate space, not the group's own parent space), then composes
+`childFrame.Transform * groupChildTransform` to obtain the child's full transform into the
+group's parent space (row-vector composition order, child-local-first - matching every other
+transform composition in this phase). For a shape nested in multiple levels of groups, this
+composition chains: each level's `ResolveGroupChildTransform` result is itself post-multiplied by
+its own parent group's child transform before being combined with a descendant's own
+`ResolveShapeFrame` result.
+
+**Group-semantics deferral boundary**: this phase implements transform composition only. Full
+group-shape semantics beyond that - inherited/overridden fill or line style cascading from a
+group to its children, and group-level effects - remain deferred to a later phase (see
+_Deferred to a Later Phase_ below). The ``'s own enumeration/recursion through a slide's
+full shape tree (Phase 1b's placeholder parser still only walks a ``'s _immediate_
+`` children, not recursing into ``/parsing freeform shapes at all) was deferred to
+Phase 1e at the time this phase was written, and is now implemented - see
+_Images, Tables, and Shape Tree (Phase 1e)_'s _Shape Tree_ subsection below.
+
+#### Preset Geometry
+
+`PptxPresetGeometry.cs`'s `Build(string prst, float w, float h)` builds the core `Path` for a
+named preset geometry (``), sized to exactly `(0,0)`-`(w,h)` local
+coordinates (the caller then applies the shape's resolved `PptxShapeFrame.Transform` via
+`Path.Transform(Matrix3x2)` to place it into parent coordinates).
+
+**Supported presets (28)**: `rect`, `roundRect`, `ellipse`, `triangle`, `rtTriangle`, `diamond`,
+`parallelogram`, `trapezoid`, `hexagon`, `octagon`, `pentagon`, `chevron`, `homePlate`, `pie`,
+`donut`, `plus`, `rightArrow`, `leftArrow`, `upArrow`, `downArrow`, `leftRightArrow`,
+`upDownArrow`, `star4`, `star5`, `curvedRightArrow`, `curvedLeftArrow`, `curvedUpArrow`,
+`curvedDownArrow`. This set was chosen to cover the small handful of presets
+(`rect`/`roundRect`/`ellipse`/`triangle`) PowerPoint itself defaults new shapes to, plus a
+practical cross-section of the arrow, star, and other basic-shape categories real-world
+presentations most commonly use, per the approved plan's "aim for ~24 total" guidance. The four
+curved-arrow presets were added in a later real-world-corpus hardening pass (see _Phase 2: Real-
+World Corpus Hardening_'s "Genuine bug 3" entry below) once `curvedUpArrow` was found to crash
+rendering of a real-world deck; the remaining three (`curvedDownArrow`/`curvedLeftArrow`/
+`curvedRightArrow`) were added alongside it because the existing `Mirror`/`RotateQuarter`
+derivation pattern already used for `upArrow`/`downArrow`/`leftArrow` (each derived from
+`rightArrow`/`leftRightArrow`) made deriving all four curved arrows from one canonical
+`CurvedRightArrow` builder equally cheap, not because they were independently reported.
+
+**Adjustment-value scope**: OOXML preset shapes are parameterized by named "adjustment values"
+(`/`) a document may override to reshape a preset (for
+example a `roundRect`'s corner radius, or a `homePlate`'s notch depth). This phase does not parse
+`` at all - every preset is built using a single, fixed, documented proportion
+approximating that preset's own OOXML schema default adjustment value(s) (for example
+`roundRect`'s corner radius is fixed at `min(w,h)/6`, matching the schema default of `16667`
+(16.667%) rather than reading any `` override). This is a deliberate
+simplification: a shape whose author customized its adjustment handles renders using the default
+proportion instead, not pixel-exact to every possible customization, but still a recognizable
+instance of the named preset shape. Parsing `` is deferred to a later phase.
+
+**Unsupported presets**: an unrecognized `prst` name throws `PptxUnsupportedFeatureException`
+(feature token `"pptx-preset-geometry"`) rather than silently falling back to an incorrect shape
+(for example rendering an unknown preset as a plain rectangle) - this fail-closed approach lets a
+caller distinguish "this shape's geometry could not be resolved" from "this shape resolved to the
+wrong geometry", which a silent fallback would make indistinguishable.
+
+`ResolveShapeGeometry(XElement spPr, float widthEmu, float heightEmu)` dispatches between
+`` (delegating to `PptxPresetGeometry.Build`) and `` (delegating to
+`ResolveCustomGeometry` below), throwing `InvalidDataException` when `` has neither
+element, or when a present `` is missing its required `prst` attribute.
+
+#### Custom Geometry
+
+`ResolveCustomGeometry(XElement custGeom, float widthEmu, float heightEmu)` parses a shape's
+`` element - every `` under its `` (not merely the first; multiple
+`` elements each contribute their own independent subpath/subpaths to the result) - into
+the core `Path` type, interpreting each path command:
+
+- **``/``**: `PathBuilder.MoveTo`/`.LineTo`.
+- **``** (two control points + an endpoint): `PathBuilder.CubicBezierTo`.
+- **``** (one control point + an endpoint): `PathBuilder.QuadraticBezierTo`.
+- **``**: `PathBuilder.Close`.
+
+Each ``'s own `w`/`h` attributes declare that path's local coordinate space (defaulting to
+the shape's own `widthEmu`/`heightEmu` - an identity scale - when either is omitted); every point
+within that `` is scaled by `widthEmu / w` and `heightEmu / h` respectively before being
+added to the builder, so a path authored in an arbitrary coordinate space still fills the shape's
+declared extent exactly. A `` with no `` resolves to `Path.Empty` (not an
+error - a degenerate but well-formed case).
+
+**Best-effort command handling**: an unrecognized path-command element (for example OOXML's own
+distinct, center-parameterized `` command, which this phase does not convert to the core
+`Path`'s SVG-endpoint-parameterized arc representation) is silently skipped rather than thrown -
+a deliberate, documented difference from preset geometry's fail-closed philosophy, chosen because
+a custom geometry's overall silhouette is still usably close even with one skipped arc segment,
+whereas an entire unsupported preset shape has no usable partial rendering at all.
+
+#### Color Resolution and Transforms
+
+`PptxDocument.Paint.cs`'s `ResolveColor(XElement colorElement, PptxTheme theme)` resolves any
+DrawingML color-definition element to a concrete `Rgba32`:
+
+- **``**: resolved directly via `Rgba32.Parse`.
+- **``**: resolved via its `lastClr` attribute, identical to Phase 1b's own theme
+ `sysClr` handling.
+- **``**: resolved by looking the named slot up in the _supplied_
+ `PptxTheme`'s `PptxColorScheme` - reusing Phase 1b's already-verified theme resolver rather than
+ re-implementing its 12-slot lookup table a second time. The 12 ordinary slot names
+ (`dk1`/`lt1`/`dk2`/`lt2`/`accent1`-`accent6`/`hlink`/`folHlink`) map directly to their
+ like-named `PptxColorScheme` property; the four background/text aliases `bg1`/`tx1`/`bg2`/`tx2`
+ map to `lt1`/`dk1`/`lt2`/`dk2` respectively (OOXML's own documented aliasing of the "background"
+ and "text" roles onto the light/dark scheme slots); `phClr` (a placeholder-fill color, only
+ meaningful inside a shape-style-reference context this phase does not thread through) falls
+ back to `dk1` as a documented simplification. An unrecognized slot name throws
+ `InvalidDataException`.
+- **Any other color-definition element** (for example ``, ``,
+ ``) throws `PptxUnsupportedFeatureException` (feature token
+ `"pptx-color-kind"`) rather than guessing at an approximate RGB conversion.
+
+**Color-transform pipeline**: a color-definition element's child transforms
+(``/``/``/``/``, each a percentage expressed in
+OOXML's thousandths-of-a-percent unit, where `100000` = 100%) are applied to the resolved base
+color in a **fixed pipeline order**, deliberately independent of each transform element's actual
+position in the source XML (real-world documents do not reliably order these consistently, so a
+fixed, documented order is simpler and more predictable than attempting to honor document order):
+
+1. **`lumMod`/`lumOff`** (luminance modulation/offset), applied together first, in HSL space:
+ the base color is converted to HSL, `L' = clamp(L * lumMod + lumOff, 0, 1)` is computed (each
+ of `lumMod`/`lumOff` defaulting to `1.0`/`0.0` respectively when absent), and the result is
+ converted back to RGB.
+2. **`shade`** (darken toward black), in RGB space: `c' = c * (val / 100000)` per channel.
+3. **`tint`** (lighten toward white), in RGB space: `c' = c * (val / 100000) + 255 * (1 - val /
+ 100000)` per channel.
+4. **`alpha`**, applied last: replaces the color's alpha channel outright with
+ `val / 100000 * 255` (not multiplied against any existing alpha - every base color this phase
+ resolves is fully opaque before this step).
+
+Each RGB channel conversion casts its computed `float` result directly to `byte` (truncating, not
+rounding, toward zero) after `Math.Clamp`-ing to `[0, 255]` - for example a `50%` `alpha` applied
+to a fully-opaque color resolves to alpha `127`, not `128` (`0.5 * 255 = 127.5`, truncated).
+
+#### Fill Resolution
+
+`ResolveFill(XElement? spPr, PptxTheme theme, float widthEmu, float heightEmu)` resolves a
+shape's fill from its `` (or lack thereof) into one of three closed `PptxPaint` subtypes:
+
+- **`PptxNoFill`** (a singleton, `PptxNoFill.Instance`): resolved when `spPr` is `null`, when
+ `` has an explicit `` child, or when it has no recognized fill child element
+ at all (a documented simplification: no placeholder/theme format-scheme fill inheritance is
+ implemented this phase - an inherited fill a real presentation would show is resolved as
+ unfilled instead).
+- **`PptxSolidFill(Rgba32 Color)`**: resolved from ``'s single color-definition
+ child, via `ResolveColor` above.
+- **`PptxGradientFill(Gradient Gradient)`**: resolved from ``, via
+ `ResolveGradientFill` below.
+- **`PptxImageFill(Surface Image, Matrix3x2 ImageToLocalTransform)`**: resolved from
+ `` when a blip-image resolver is supplied by the caller - see _Picture Fill
+ (``) as a Shape/Paragraph Fill_ below (a Phase 2 follow-up to this phase's own,
+ originally fail-closed, posture).
+- **`PptxPatternFill(PptxPresetPattern Preset, Rgba32 Foreground, Rgba32 Background)`**: resolved
+ from `` for a covered preset-pattern name - see _Pattern Fill
+ (``) (Phase 2 Follow-Up)_ below (a later Phase 2 follow-up to this phase's own,
+ originally fail-closed, posture). An uncovered `prst` still throws
+ `PptxUnsupportedFeatureException` (feature token `"pptx-pattern-fill"`).
+
+#### Gradient Fill
+
+`ResolveGradientFill(XElement gradFill, PptxTheme theme, float widthEmu, float heightEmu)`
+resolves ``'s `` gradient stops (each ``'s `pos` - OOXML's
+own thousandths-of-a-percent unit - mapped to the core `GradientStop`'s `[0,1]` position, and its
+single color-definition child resolved via `ResolveColor`) combined with its ``
+linear-direction angle (60,000ths of a degree) into a core `LinearGradient`:
+
+- `angleRadians = (ang / 60000) * (pi / 180)`; `direction = (cos(angleRadians), sin(angleRadians))`.
+- `extent = (|direction.X| * widthEmu + |direction.Y| * heightEmu) / 2` (the gradient's half-length
+ along its own direction, sized so the gradient exactly spans the shape's bounding box corner to
+ corner along that direction).
+- `center = (widthEmu / 2, heightEmu / 2)`; `start = center - direction * extent`;
+ `end = center + direction * extent`.
+
+**Scope**: only linear (``) gradients are supported this phase - the most common gradient
+fill in practice. A `` with no `` child (for example a path/radial gradient,
+`` or similar) throws `PptxUnsupportedFeatureException` (feature token
+`"pptx-gradient-path"`) rather than silently approximating it as a linear gradient, which would
+visibly misrender the shape's fill. A `` missing its required `` throws
+`InvalidDataException`. Radial/path gradients are deferred to a later phase, pending a core
+`RadialGradient`/path-gradient paint type this package can reuse.
+
+#### Stroke/Line Style Resolution
+
+`ResolveLineStyle(XElement? lnElement, PptxTheme theme)` resolves a shape's `` stroke
+element into a `PptxLineStyle` (an `internal sealed record` capturing width in EMU, a resolved
+`PptxPaint`, and an optional dash array), or `null` (meaning "no stroke is drawn") when:
+
+- `lnElement` is `null` (no `` element at all - a documented simplification: this does
+ _not_ resolve a theme-inherited default line width/style, matching this phase's broader
+ decision not to implement placeholder/theme format-scheme property inheritance for shape
+ styling);
+- ``'s `w` attribute (width, in EMU) is _explicitly present_ with a non-positive
+ (`<= 0`) value (covering both `w="0"` and any negative `w` - OOXML's own "explicit zero/negative
+ width means no stroke" idiom); or
+- the resolved fill (typically ``, via `ResolveFill`) is `PptxNoFill` (an explicit
+ `` line, or a line with no recognized fill child).
+
+A `w` attribute that is genuinely _absent_ (as opposed to explicitly zero/negative) instead
+resolves to a documented default stroke width of `9525` EMU (`0.75`pt), provided the ``
+still resolves a recognized (non-`PptxNoFill`) fill. This matches a real-world, commonly seen
+document pattern - an `` that declares only a color/fill (for example
+``), relying on PowerPoint's own
+default stroke weight for an un-set `w` - which previously rendered with no visible outline at
+all, collapsing to the exact same "no stroke" outcome as an explicit `w="0"`. The `9525` EMU
+(`0.75`pt) value is PowerPoint's own observed default new-shape outline weight; this is an
+**application-level convention**, not a formally declared ECMA-376/ISO-29500 XSD schema default -
+the `CT_LineProperties` schema type declares no `default="..."` for `w` - so the value is
+cross-checked against community/library documentation (for example python-pptx's own documented
+default) and this codebase's own existing `1pt = 12700` EMU conversion, rather than cited as a
+schema default.
+
+This default-width fallback applies **only** to `ResolveLineStyle` itself (and therefore to every
+call site that defers its width/fill decision to it directly - an ordinary shape's own ``
+with a recognized fill, table-cell borders, and a `/` theme style-list entry). It
+does **not** change `ResolveShapeLineStyle`'s case-3 "fill-less, width-less ``, no
+style-color" behavior, nor `ResolveConnectorLineStyle`'s own per-attribute merge logic - both
+deliberately, separately retain their existing "a present, width-less `` never gains a width
+from style or default" policy, each independently documented and tested below.
+
+**Dash resolution**: `` resolves a dash array proportional to the line's
+own `widthEmu`, for `dash`/`dashDot`/`dot`/`lgDash`/`lgDashDot`/`sysDash`/`sysDot`; any other
+preset name (including the common `solid`, and any name this phase does not recognize) resolves
+to `null` (a solid line) - a documented, deliberate cosmetic degradation (an unrecognized dash
+preset renders as a solid line rather than throwing) distinct from preset-geometry's fail-closed
+philosophy, chosen because a solid line where a dashed one was intended is still a usable, visible
+stroke, unlike an entirely unresolved shape.
+
+**Realizing a stroke outline**: `ResolveStrokeOutline(Path shapePath, PptxLineStyle lineStyle,
+Matrix3x2 localToSurface)` mirrors the PDF renderer's own stroke-operator call pattern exactly: it
+constructs a core `StrokeStyle(lineStyle.WidthEmu, dashArray: lineStyle.DashArray)` and calls the
+core `PathStroker.Stroke(shapePath, style, flattenTolerance)` static entry point, producing the
+stroked outline path a later rendering phase fills (via `PathFiller.Fill(...,
+FillRule.NonZero)`) with `lineStyle.Paint`'s resolved color - reusing the exact same, already-tested
+stroking machinery the PDF renderer uses, rather than a second, divergent implementation for this
+package. This phase has no rendering surface yet, so only the outline geometry is produced here.
+
+- **Scale-aware flatten tolerance (regression fix).** Unlike `PdfDocument.PaintStroke`, which
+ strokes an already device-space-baked path, `ResolveStrokeOutline` strokes `shapePath` in the
+ shape's own native, pre-transform local coordinate space (PPTX's own EMU units - typically
+ hundreds of thousands to millions per shape), with the caller applying `localToSurface` only
+ afterward. Calling `PathStroker.Stroke` with its library default `flattenTolerance: 0.25f`
+ against that native EMU-space geometry produced a confirmed regression: a curved shape (for
+ example an `ellipse` or `roundRect` preset geometry) flattened to roughly 200x more tessellation
+ points than its eventual rendered pixel scale ever needed (around 8,000 points for a typical
+ slide-scale ellipse, versus the ~40 actually needed), which in turn triggered a false-positive
+ inner-ring collapse in the shared `StrokeOutliner` (see
+ `docs/design/canvas-net/drawing/path-stroker.md`'s "Inner-ring collapse" section) - rendering a
+ `` shape's thin `` outline as a solid-filled interior instead of a thin ring.
+ `ResolveStrokeOutline` now derives its own `flattenTolerance` from `localToSurface`'s own scale
+ (the larger of its two row-vector lengths, a conservative choice ensuring the more-magnified
+ axis still stays within the intended deviation), via a private `ResolveFlattenTolerance` helper
+ that returns `0.25f / scale` - keeping the _effective_, post-transform flattening deviation at
+ the library's intended ~0.25 device-pixel target regardless of the shape's own native EMU
+ magnitude, falling back to the unscaled `0.25f` default when the transform's scale is
+ non-finite or non-positive (for example, a degenerate zero-size shape). This both resolves the
+ false-collapse regression and incidentally fixes a ~200x stroke-tessellation performance cost
+ that affected every stroked PPTX shape, not only the reported bug. The shared `StrokeOutliner`
+ tolerance fix above was still made and is the primary defense (it does not depend on this
+ caller choosing a sane `flattenTolerance`); this change is a secondary, scale-appropriate
+ hardening of the specific caller that originally triggered it. See
+ `PptxRenderTests.Render_FullSlideScaleEllipseWithNoFillAndLnMissingWidth_RendersThinRingNotSolidDisc`
+ and `PptxRenderTests.Render_FullSlideScaleRoundRectWithNoFillAndLnMissingWidth_RendersThinOutlineNotSolidFill`.
+
+#### Deferred to a Later Phase
+
+The following are explicitly out of scope for Phase 1c, each because it depends on machinery not
+yet implemented anywhere in `CanvasNet` (not merely unimplemented in this package):
+
+- **Radial/path gradients** - require a core `RadialGradient`/path-gradient paint type this
+ package can reuse; deferred to a later phase.
+- **`` adjustment-value parsing** for preset geometries - deferred to a later phase; see
+ _Preset Geometry_ above.
+- **Full group-shape semantics** beyond transform composition and shape-tree recursion (style
+ cascading, group-level effects) - remains deferred to a later phase. The ``'s own
+ enumeration/recursion through a slide's full shape tree, listed here as deferred at the time
+ this phase was written, is now implemented - see _Shape Tree_ in
+ _Images, Tables, and Shape Tree (Phase 1e)_ below.
+
+### Text Layout and Rendering (Phase 1d)
+
+Phase 1d adds DrawingML text body parsing, attribute-level property inheritance, word-wrap/
+alignment/vertical-anchor/autofit text layout, and glyph-ink rendering onto a core
+`DemaConsulting.CanvasNet.Canvas.Surface`. It additionally depends on the core
+`DemaConsulting.CanvasNet.Fonts.TrueTypeFont`/`SystemFontCatalog` types (font resolution and
+glyph-outline extraction) and `DemaConsulting.CanvasNet.Drawing.PathFiller` (glyph-ink fill),
+reusing exactly the same font-resolution/glyph-painting pattern `DemaConsulting.CanvasNet.Pdf`'s
+`PdfDocument.Text.cs` already established, rather than a second, divergent implementation. Every
+new member introduced this phase is `internal` - no public shape/rendering surface is introduced
+yet (matching Phase 1b/1c's own precedent).
+
+#### Text Body Parsing
+
+`PptxDocument.Text.cs` parses a shape's `` element (`ParseTextBody`) into a
+`PptxTextBody` (an `internal sealed record`, defined alongside its sibling small record types in
+`PptxTextBody.cs`, mirroring `PptxShapeFrame.cs`/`PptxPaint.cs`'s one-file-per-small-type-family
+convention): resolved body properties plus an ordered list of paragraphs.
+
+- **`ParseBodyProperties(XElement? bodyPrElement)`** resolves ``'s vertical anchor
+ (`anchor="t"/"ctr"/"b"`, defaulting to `t`/`PptxTextAnchor.Top`), word-wrap mode
+ (`wrap="square"/"none"`, defaulting to `square`/`PptxTextWrap.Square`), and the four insets
+ (`lIns`/`tIns`/`rIns`/`bIns`, each in EMU), defaulting to the OOXML schema's own documented
+ defaults - `91440`/`45720`/`91440`/`45720` EMU respectively - when `` itself, or an
+ individual inset attribute, is absent. The raw autofit child element (``/
+ ``/``, if present) is retained **unparsed**, as an `XElement?`, for
+ the layout engine (`PptxDocument.TextLayout.cs`) to interpret - body-property parsing does not
+ itself implement autofit policy.
+- **`ParseParagraph(XElement pElement)`** resolves `` into a `PptxParagraph`: raw paragraph
+ properties (via `ParseParagraphProperties`) plus an ordered list of runs (via `ParseRun`). A
+ paragraph with no recognized child element resolves to an empty run list - a valid, empty
+ paragraph (a blank line), not an error; `InvalidDataException` is reserved for genuinely
+ malformed structure, not merely sparse/empty content.
+- **`ParseParagraphProperties(XElement? pPrElement)`** resolves ``'s `algn`, `marL`,
+ `indent`, `lnSpc`, `spcBef`, `spcAft`, and `defRPr` child/attributes, each retained **raw and
+ unresolved** (a `PptxRawParagraphProperties` record) - resolution against the inheritance chain
+ happens later, in `PptxDocument.TextInheritance.cs`, not here. The paragraph's own `lvl`
+ attribute (its placeholder/master style level) is clamped to the OOXML schema's documented
+ ten-level `[0,8]` range.
+- **`ParseRun(XElement rElement)`** resolves `` into a `PptxTextRun`: its own raw,
+ unresolved `` element (or `null`) plus its `` text, defaulting to `string.Empty`
+ when `` is absent, per the OOXML schema.
+
+#### Master `` Parsing
+
+`PptxDocument.Masters.cs`'s `GetMaster` additionally parses the slide master root element's own
+`` child - a direct sibling of `` per ECMA-376 (ISO/IEC 29500) §19.3.1.53's
+`CT_SlideMaster` content model, not nested inside it, exactly paralleling how `GetMaster` already
+reads `` as a direct child of the same root - into a new `PptxMasterTextStyles(XElement?
+TitleStyle, XElement? BodyStyle, XElement? OtherStyle)` record (`PptxPlaceholder.cs`), each an
+``-shaped element or `null` when the master omits that schema-optional style bucket.
+`PptxMaster` gains a new, non-nullable `TxStyles` field (always populated, even when all three
+buckets inside are `null`), and `PptxPlaceholderProperties` gains a new optional
+`MasterTextStyles` field threaded through unconditionally from `ResolvePlaceholderProperties`'s
+new optional `masterTextStyles` parameter - independent of whether a layout/master placeholder
+match was found, since a master's own default text styling exists even for a shape with no
+placeholder match at all (see _Property Inheritance_ below).
+
+#### Property Inheritance
+
+**Design decision: attribute-level, not element-level, fallback.** Phase 1b's placeholder
+property inheritance (`` and `/`) resolves at the level of the
+_whole element_: the first category-level element present anywhere in the chain wins outright,
+with no deeper merging. Run/paragraph text-property inheritance deliberately does **not** reuse
+that same element-level pattern. Real-world DrawingML very commonly overrides a single attribute
+at the run level (for example, bolding one word within an otherwise plain sentence) without
+overriding every other attribute at the same level; an element-level fallback would incorrectly
+discard every other, more specific attribute resolution available at a shallower level for that
+run. `PptxDocument.TextInheritance.cs` therefore resolves **each attribute independently**,
+walking the same conceptual chain - run/paragraph own value -> paragraph's own `defRPr` ->
+placeholder's level-indexed `` entry -> master's level-indexed `` bucket
+entry -> theme/hard-coded default - via six small private per-attribute helpers for run
+properties (`ResolveTypeface`, `ResolveFontSizeEmu`, `ResolveBold`, `ResolveItalic`,
+`ResolveUnderline`, `ResolveRunColor`) and an analogous set for paragraph properties, each
+independently taking its own first non-`null` hit.
+
+- **`ResolveEffectiveRunProperties(PptxTextRun run, PptxParagraph paragraph,
+ PptxPlaceholderProperties placeholderProperties, PptxTheme theme, string placeholderType)`**
+ resolves a `PptxEffectiveRunProperties` (font family, size in EMU, bold, italic, underline,
+ color). Font size (`` means 18pt, i.e. `sz / 100` points, further scaled by
+ `* 12700` to EMU) and every boolean flag (`"1"`/`"true"`) are parsed at whichever tier first
+ supplies a non-`null` value.
+- **`ResolveEffectiveParagraphProperties(...)`** resolves a `PptxEffectiveParagraphProperties`
+ (alignment, left margin, indent, line spacing). Alignment normalizes `algn="just"`/`"justLow"`
+ (full/low text justification) to `"l"` (left) - full justification requires redistributing
+ inter-word spacing per line, a separable refinement explicitly deferred (see _Deferred to a
+ Later Phase_ below). Line spacing (``) is either `` (percentage,
+ `val / 100000`) or `` (fixed points, `val / 100 * 12700` EMU) - mutually
+ exclusive per schema - modeled as a `PptxLineSpacing(float? Percent, float? FixedEmu)`
+ discriminated record (exactly one field non-`null`), with a static `Default` of 100%.
+- **`` bucket selection**: for a given shape's placeholder type, `title`/`ctrTitle`
+ selects the master's `TitleStyle`, the empty-string sentinel (used for a non-placeholder shape
+ - see `PptxDocument.Masters.cs`'s `RemapPlaceholderType`/placeholder-matching precedent in
+ Phase 1b) selects `OtherStyle`, and every other type (including `body` and every type the
+ Phase 1b remapping table folds into `body`) selects `BodyStyle`.
+
+#### Text Layout
+
+`PptxDocument.TextLayout.cs`'s `ResolveTextLayout(PptxTextBody textBody, PptxPlaceholderProperties
+placeholderProperties, PptxTheme theme, string placeholderType, float widthEmu, float heightEmu,
+Func fontResolver)` produces a `PptxTextLayout`
+(`PptxTextLayout.cs`): an ordered list of `PptxGlyphPlacement` (resolved font, glyph index,
+baseline-relative origin in the shape's own local, unrotated coordinate space, size, and color)
+plus the resolved total block height.
+
+1. Every run's/paragraph's effective properties are resolved via `PptxDocument.TextInheritance.cs`
+ (above).
+2. **Autofit** (the body's raw autofit element) is applied in three tiers, resolved once for the
+ whole text body before layout:
+ - **No scaling** - ``, an absent autofit element, or `` (shape-resize
+ autofit; deliberately not implemented - see _Deferred to a Later Phase (Phase 1d)_ below,
+ since it would require resizing the owning shape itself, a cross-cutting concern beyond this
+ unit's text-layout scope).
+ - **Explicit ``** - the stored factors are
+ applied verbatim (PowerPoint persists these once it has itself computed a fit).
+ - **Attribute-less ``** - a bounded, deterministic shrink loop: starting at
+ 100% and stepping down in 10% increments (at most 9 iterations, floored at 10%), the loop
+ stops at the first scale whose naturally laid-out height fits the available height. This
+ reproduces PowerPoint's own "shrink text on overflow" user-visible effect without depending
+ on PowerPoint's own unpublished exact algorithm (which lazily computes and persists its own
+ `fontScale`/`lnSpcReduction` once a user has interacted with the file - an attribute-less
+ `` is PowerPoint's own "not yet computed" state).
+3. **Tokenization and word-wrap**: each run's text is tokenized into whitespace/non-whitespace
+ tokens via a small manual scanner (not `string.Split(' ')`, so consecutive spaces/tabs are
+ preserved as their own token's own advance width, rather than collapsed). Each distinct
+ `(FontFamily, Bold, Italic)` combination is resolved to a `TrueTypeFont` once via
+ `fontResolver`, memoized per layout call (a document-level, cross-call cache is not needed
+ this phase, mirroring Phase 1b's own per-document, not static, theme/master/layout caches).
+ Tokens are packed greedily onto lines bounded by `widthEmu` less the body's horizontal insets;
+ a single token that alone exceeds the available width is placed alone on its own (overflowing)
+ line rather than looping indefinitely - a documented overflow policy, not a crash.
+4. **Line height** is computed from the _tallest_ run on that line:
+ `(Ascender - Descender + LineGap) / UnitsPerEm * SizeEmu * LineSpacingFactor`. **Horizontal
+ start X** is computed from the effective alignment (`l`/`ctr`/`r` - `just`/`justLow` already
+ normalized to `l` by property inheritance, above) against the available width, honoring
+ `MarginLeftEmu`/`IndentEmu` (first line of a paragraph only, per the OOXML indent model).
+5. **Vertical start Y** is computed from `PptxBodyProperties.Anchor` (`t`/`ctr`/`b`) and the
+ total resolved block height within `heightEmu` less the body's vertical insets. No clipping is
+ applied when the block overflows the available height - text is positioned exactly as
+ computed, even past the shape's own box (see _Deferred to a Later Phase (Phase 1d)_ below).
+6. **Per-character glyph-coverage fallback**: for each non-whitespace character, the run's own
+ primary font (resolved once per run - see step 3 above) is tried first
+ (`TrueTypeFont.GetGlyphIndex(ch)`); only when that returns glyph `0` (`.notdef`) is a single
+ bundled fallback font (one per `(bold, italic)` pair, memoized the same way the primary-font
+ cache is) consulted, and that character alone paints from whichever font actually resolved a
+ non-`.notdef` glyph for it. A run is never rebound to the fallback font as a whole - each
+ character's resolution is independent, so a character the primary font _does_ cover is
+ unaffected by a neighboring character's fallback substitution.
+7. One `PptxGlyphPlacement` is emitted per non-whitespace glyph, carrying whichever font (primary
+ or fallback) actually resolved that specific character per step 6.
+
+#### Rendering
+
+`PptxDocument.TextRender.cs`'s `PaintTextLayout(Surface surface, PptxTextLayout layout, Matrix3x2
+shapeToSurfaceTransform)` paints a resolved layout's glyphs onto a core `Surface`, mirroring
+`DemaConsulting.CanvasNet.Pdf`'s `PdfDocument.Text.cs`'s own glyph-painting pattern exactly: for
+each `PptxGlyphPlacement`, a `glyphMatrix = Matrix3x2.CreateScale(glyph.SizeEmu /
+glyph.Font.UnitsPerEm) * Matrix3x2.CreateTranslation(glyph.OriginXEmu, glyph.OriginYEmu) *
+shapeToSurfaceTransform` is composed (scale to EMU-per-unit, then translate to the shape-local
+baseline origin, then compose with the shape's own frame transform), the glyph's outline
+(`TrueTypeFont.GetGlyphOutline`) is re-issued through a fresh `Geometry.PathBuilder` with every
+point transformed by `glyphMatrix`, and the result filled via `Drawing.PathFiller.Fill(surface,
+path, glyph.Color, Drawing.FillRule.NonZero)`. A glyph whose outline has zero subpaths (a
+whitespace or control character) is skipped entirely, rather than issuing an empty fill.
+
+`ResolveTextFont(string familyNameHint, bool bold, bool italic)` resolves a DrawingML typeface
+name hint to a concrete `TrueTypeFont` via the core `Fonts.SystemFontCatalog.FindBestMatch`
+(always passing `serif: false, fixedPitch: false`, since DrawingML never signals either
+classification - unlike the PDF renderer's Standard-14 font concept, DrawingML typeface names are
+a free-form string with no implied serif/fixed-pitch/symbolic classification), falling back to
+`Fonts.SystemFontCatalog.LoadBundledFallback(serif: false, fixedPitch: false, bold, italic)` when
+no installed font matches. This resolves only a run's **primary** font (one call per distinct
+`(FontFamily, Bold, Italic)` combination, before any character is inspected); it is the default
+`fontResolver` delegate `ResolveTextLayout` is driven with in production use. Per-character
+coverage fallback (when the primary font itself lacks a given codepoint) is a distinct, later
+step living entirely in `PptxDocument.TextLayout.cs` - see _Text Layout_ step 6 above - and is
+driven by a second, independent delegate (`fallbackFontResolver`, defaulting to
+`Fonts.SystemFontCatalog.LoadBundledFallback` with the same `serif: false, fixedPitch: false`
+convention, keyed/cached by `(bold, italic)` only, not by family name, since a fallback font is
+family-agnostic by design). Tests inject their own synthetic resolvers (for either delegate) for
+deterministic, installed-font-independent assertions.
+
+#### Deferred to a Later Phase (Phase 1d)
+
+The following are explicitly out of scope for Phase 1d, each because it depends on machinery not
+yet implemented anywhere in `CanvasNet`, or is a separable refinement with its own, independent
+design cost:
+
+- ~~**Bullets and numbering** (``/``/``) - require a separate
+ glyph/counter-rendering pass distinct from run-text layout; deferred to a later phase.~~ (closed
+ by the _Phase 2 Follow-Up: Bullets and Numbering Rendering_ section below.)
+- **Full text justification** (`algn="just"`/`"justLow"`) - requires redistributing inter-word
+ spacing per line to reach the line's own right margin exactly; normalized to left-alignment
+ this phase (see _Property Inheritance_ above).
+- **`` shape-resize autofit** - requires resizing the owning shape itself in response
+ to its own text content, a cross-cutting concern beyond this unit's text-layout scope; treated
+ as a pass-through (no scaling) this phase (see _Text Layout_ above).
+- **Kerning** - `` and font-pair kerning tables are not consulted; glyph advances
+ use each glyph's own unscaled advance width only.
+- **Text clipping on overflow** - an overflowing text block is positioned exactly as computed,
+ without being clipped to the shape's own bounding box (see _Text Layout_ above).
+- **Per-character glyph-coverage fallback is a single-tier, single-font mechanism, not a full
+ font-linking/script-itemization pipeline** - a character missing from the primary font falls
+ back to exactly one bundled font (step 6 above); DrawingML's own `` (East Asian) and
+ `` (complex script) typeface overrides, and a genuine multi-font fallback chain keyed by
+ Unicode script/block (as a real font-linking implementation would provide), are not consulted or
+ implemented - a character missing from both the primary font and the single bundled fallback
+ still paints as `.notdef` (unchanged from pre-fix behavior for that residual case).
+
+A full per-slide public `Render` API was deferred to a later phase alongside full group-shape
+semantics and picture/table support, and is now implemented (see _Images, Tables, and Shape Tree
+(Phase 1e)_ and _Full Slide Rendering (Phase 1f)_ below).
+
+### Images, Tables, and Shape Tree (Phase 1e)
+
+Phase 1e adds three additive capabilities: raster picture decoding/cropping/compositing for
+dedicated `` picture shapes (`PptxDocument.Images.cs`), `` table structural
+parsing/cell-rect computation/painting (`PptxDocument.Tables.cs`), and a full recursive
+shape-tree walker recognizing every shape kind this phase supports, including nested
+`` groups (`PptxDocument.ShapeTree.cs`). Every new member introduced this phase is
+`internal` - no public shape/rendering surface is introduced yet (matching Phase 1b/1c/1d's own
+precedent). This phase introduces no new core `CanvasNet` dependency beyond what Phase 1c/1d
+already established (`Geometry.Path`, `Drawing.PathFiller`, `Canvas.Surface`/`Rgba32`) - raster
+image decoding reuses the core codec infrastructure the `DemaConsulting.CanvasNet.Svg` package's
+`SvgCodec.ResolveRasterDecoder` already established for embedding a raster image inside another
+document format.
+
+**Scope decision**: only a dedicated `` picture shape is in scope this phase for image
+rendering. An ordinary shape's own _background_ fill as a picture or pattern
+(``/`` under ``/`ResolveFill`, as distinct from a `` shape)
+remains out of scope this phase and continues to be rejected by `ResolveFill` exactly as Phase 1c
+left it (see _Fill Resolution_ above) - this phase does not touch `ResolveFill` at all. (Both are
+implemented in later Phase 2 follow-ups - see _Picture Fill (``) as a Shape/Paragraph
+Fill_ and _Pattern Fill (``) (Phase 2 Follow-Up)_ below - which reuse this phase's own
+picture decoding/color resolution directly, unchanged.)
+
+#### Picture Decoding, Cropping, and Painting
+
+`PptxDocument.Images.cs`'s `ResolvePictureSurface(XElement picElement, PptxDocument document)`
+resolves a `` shape's `/` into a decoded core `Surface`:
+
+- **Relationship resolution**: ``'s `r:embed` attribute (an embedded-image relationship
+ ID) is resolved via the existing `ResolveRelationship`/`GetPartBytes` package-layer primitives
+ into the raw media part bytes. `GetPartBytes` bounds the part's decompressed size to
+ `MaxPartBytes` (256 MiB), rejecting an oversized/zip-bomb-shaped media part with
+ `InvalidDataException` rather than trusting its declared or compressed size. One with only
+ `r:link` (an external, non-embedded image
+ reference) throws `PptxUnsupportedFeatureException` (feature token `"pptx-image-link"`) - a
+ linked image has no embedded bytes this package can read without performing file-system I/O
+ outside the supplied package stream, a well-formed-but-deliberately-unsupported construct, not
+ malformed data. One with neither `r:embed` nor `r:link`, but carrying only a Microsoft SVG
+ extension fallback (`/` wrapping an
+ `` element - the "Insert Icon" SVG-with-no-raster-fallback pattern real PowerPoint
+ produces) throws `PptxUnsupportedFeatureException` (feature token `"pptx-image-svg-only"`) -
+ also a well-formed, valid OOXML construct this package does not yet decode, not malformed data.
+ Only when neither an embed/link attribute nor this recognized extension fallback is present does
+ `` throw `InvalidDataException` (genuinely malformed - ECMA-376 requires at least one of
+ these).
+- **Content-type dispatch**: the resolved media part's content type (via the existing
+ `ResolvePart` content-type lookup) dispatches to a private `ResolveRasterDecoder` helper
+ mirroring `DemaConsulting.CanvasNet.Svg.SvgCodec.ResolveRasterDecoder`'s own
+ content-type-to-decoder dispatch pattern (PNG/JPEG/BMP/GIF, via the core raster codecs already
+ used elsewhere in `CanvasNet`) rather than inventing a second, divergent raster-decode entry
+ point. An unrecognized content type throws `PptxUnsupportedFeatureException` (feature token
+ `"pptx-image-format"`).
+- **Crop rectangle**: `ResolveSrcRect(XElement blipFillElement)` resolves ``'s
+ optional `` into a `PptxSrcRect(float Left, float Top, float Right, float Bottom)`
+ record (`PptxPicture.cs`) - each edge a `[0,1]`-normalized fraction of the image's own
+ full extent, parsed from OOXML's thousandths-of-a-percent `l`/`t`/`r`/`b` attributes (each
+ defaulting to `0` - no crop on that edge - when its own attribute is absent), or `null` when
+ `` itself is absent entirely (meaning "no cropping at all" - the full image).
+- **Painting**: `PaintPicture(Surface destination, Surface image, PptxSrcRect? srcRect, Matrix3x2
+ shapeTransform, float widthEmu, float heightEmu, Path? clipPath = null)` composes the crop
+ rectangle's own crop-to-unit-square mapping with `shapeTransform` into a single
+ image-to-surface matrix, inverts it once, and for every destination pixel whose inverse-mapped
+ coordinate falls within `[0,1]x[0,1]` samples the source image with nearest-neighbor filtering
+ (no bilinear/anti-aliased resampling this phase - a documented simplification, consistent with
+ this package's existing "no anti-aliasing yet" posture established by `PaintTextLayout`'s own
+ glyph-ink fill). Each sampled source pixel is alpha-blended "over" the existing destination
+ pixel via the shared internal `Canvas.Rgba32.CompositeOver(Rgba32, Rgba32)` helper (standard
+ Porter-Duff "over" alpha compositing - straight/unassociated alpha in and out, `outA = fgA +
+ bgA * (1 - fgA)`, each color channel `outC = (fgC * fgA + bgC * bgA * (1 - fgA)) / outA` when
+ `outA != 0` else `0`, round-half-away-from-zero, clamped to `[0, 255]`) rather than overwritten
+ outright - a fully or partially transparent source pixel therefore lets the existing destination
+ content show through correctly instead of being replaced by whatever RGB value happens to be
+ stored alongside that non-opaque alpha (a real-world-confirmed bug fixed after this phase first
+ shipped: a logo PNG with an unassociated-alpha white matte around its letters previously painted
+ a solid white rectangle instead of a transparent background). This same helper is reused, not
+ duplicated, by `DemaConsulting.CanvasNet.Pdf`'s own `CompositeImageOntoSurface` (see that
+ package's own design documentation) - exposed cross-assembly via this package's own
+ `InternalsVisibleTo` grant from `DemaConsulting.CanvasNet`. A singular (non-invertible)
+ `shapeTransform` (for example a zero-area shape frame) paints nothing, rather than throwing or
+ dividing by zero. The optional `clipPath` parameter (default `null`, preserving the original
+ unclipped behavior) clips the painted image to the picture's own resolved non-rectangular
+ preset/custom geometry - see "Phase 2 Follow-Up: Picture Preset-Geometry Clipping" below for the
+ full rationale.
+
+**Deliberate divergence from the PDF renderer**: `PaintPicture` does **not** apply the `(1 - v)`
+row flip `DemaConsulting.CanvasNet.Pdf`'s own image-painting code applies. PDF's content stream
+coordinate space is y-up, so painting a top-down-row-ordered decoded image there requires
+flipping its sampled V coordinate; this package's shape-local/surface space is already y-down
+(matching every other transform in this unit - see _Shape Frame Transform_ above), so the image's
+own top-down row order already matches the surface's own row order with no flip needed. Applying
+the PDF renderer's flip here, by copy-paste habit, would paint every picture upside down - this
+is a deliberate, documented decision, not an oversight. Both renderers' per-pixel alpha-blending
+behavior is otherwise identical (see above).
+
+#### Picture Fill (``) as a Shape/Paragraph Fill (Phase 2 Follow-Up)
+
+A later Phase 2 follow-up lifted the "Scope decision" above's own original restriction and
+implemented `` used as a fill-definition child (an ordinary shape's own ``,
+table-cell ``, line/stroke ``, slide/layout/master background ``, and
+connector style fills - everywhere `ResolveFill`/`ResolveLineStyle` are consulted), as distinct
+from the `` picture _shape_ this phase (Phase 1e) already implements above. Pattern fill
+(``) remained separately out of scope at the time of this follow-up - see
+_Pattern Fill (``) (Phase 2 Follow-Up)_ below for the later follow-up that covers it,
+for a documented preset subset.
+
+- **`PptxImageFill(Surface Image, Matrix3x2 ImageToLocalTransform)`** (`PptxPaint.cs`) is the new
+ closed `PptxPaint` subtype `ResolveFill` returns for a resolvable `` - `Image` is
+ the fully decoded raster image (reusing `ResolvePictureSurface` above, completely unchanged -
+ structurally, a fill-context `` and a ``'s own `` have identical
+ `` children, so no new decoding logic was needed), and `ImageToLocalTransform` maps the
+ image's own pixel-space coordinates into the owning shape's local EMU coordinate space (composed
+ with a caller's own shape-to-surface transform at fill time, mirroring `PptxGradientFill`'s own
+ `Gradient.WithTransform` composition pattern).
+- **Threading a resolver through `ResolveFill`, backward-compatibly**: since `ResolveFill` and
+ the resolver methods that defer to it (`ResolveLineStyle`, `ResolveShapeStyleFill`,
+ `ResolveShapeStyleLineStyle`, `ResolveShapeLineStyle`, `ResolveConnectorLineStyle`,
+ `ResolveTableCellFill`, `ResolveBackgroundElement`, `ResolveBackgroundStyleReference`,
+ `ResolveSlideBackgroundFill`) are `internal static` methods directly unit-tested on bare
+ `XElement` fragments with no owning-part context, each gained one new, optional trailing
+ `Func? resolveBlipImage = null` parameter rather than a required one - an
+ ``'s `r:embed` relationship is part-scoped (it must resolve against the specific
+ slide/layout/master/table-owning part's own `.rels`), so only a caller with that context can
+ supply a concrete resolver; every pre-existing call site that does not supply one preserves its
+ exact prior behavior unchanged (`ResolveFill` still throws `PptxUnsupportedFeatureException`,
+ feature token `"pptx-picture-fill"`, when `resolveBlipImage` is `null` - now documented as "no
+ resolver supplied" rather than "picture fill is unsupported"). `PptxDocument.Render.cs`'s
+ `RenderShape`/`RenderConnector` (and each of `GetSlide`/`GetLayout`/`GetMaster`'s own
+ `ParseShapeTree` calls) supply the concrete resolver, a closure binding `ResolvePictureSurface`
+ to the owning part's own path (`blip => ResolvePictureSurface(ownerPartPath, blip)`) -
+ `ResolvePictureSurface` is an instance method, which is why those two `Render.cs` methods were
+ converted from `static` to instance methods to build that closure via `this`, while every
+ other, directly-tested resolver kept its own `static` modifier unchanged.
+- **`` and `` transform derivation**: `ResolveImageFillTransform(XElement
+ blipFillElement, Surface image, float widthEmu, float heightEmu)` (`PptxDocument.Images.cs`)
+ derives `ImageToLocalTransform` from whichever sub-element is present, consulting ``
+ first when both could theoretically coexist:
+ - **``** (the mandatory, common case - see
+ _Pythonpptx-DML-Fill Fixture_ below) - `tx`/`ty` are EMU offsets (each defaulting to `0`) and
+ `sx`/`sy` are 1/100000-of-a-percent scale factors (each defaulting to `100000`, 100%) applied
+ to the image's own native 96-DPI pixel size (`914400` EMU/inch over `96` pixels/inch =
+ `9525` EMU/pixel, a private `EmuPerPixelAt96Dpi` constant): `scaleX = sx * 9525`,
+ `scaleY = sy * 9525`, and the resulting `Matrix3x2` translates by `(tx, ty)` - every tile
+ repeats from the shape's own local origin, unflipped. This phase deliberately does not
+ resolve ``'s own `flip`/`algn` refinements (a documented scope reduction - the common
+ case, `flip="none" algn="tl"`, is the only one this package's own real fixture exercises, and
+ general flip/alignment support would require `TilePaint`/pattern-space changes beyond this
+ follow-up's own scope).
+ - **``** (consulted when no `` is present, including when neither element is
+ present at all - the OOXML schema default for an otherwise-unqualified ``, treated
+ identically to an empty ``) - its optional nested `` crops the image first (each edge a 1/100000-of-a-percent fraction of the
+ image's own width/height, each absent edge defaulting to `0`, reusing `ParseSrcRectEdge`'s
+ identical parsing convention), and the remaining cropped span is then linearly stretched to
+ exactly fill `widthEmu` x `heightEmu`. A degenerate crop (the left+right, or top+bottom,
+ edges summing to `100%` or more on an axis) falls back to an uncropped full stretch on that
+ axis, rather than producing a non-finite or divide-by-zero transform.
+- **Painting**: every `FillPaint`-style switch over a resolved `PptxPaint` (currently only
+ `PptxDocument.Tables.cs`'s own `FillPaint`, the sole such switch in this package - see
+ _Table Parsing_ below) gained a `case PptxImageFill imageFill` branch constructing
+ `new TilePaint(imageFill.Image, imageFill.ImageToLocalTransform, imageFill.Image.Width,
+ imageFill.Image.Height).WithTransform(shapeToSurfaceTransform)` and filling via the existing
+ `PathFiller.Fill(Surface, Path, TilePaint, FillRule, float)` overload - composing the
+ shape-to-surface transform exactly as `PptxGradientFill`'s own
+ `Gradient.WithTransform(shapeToSurfaceTransform)` branch already does, immediately above it in
+ the same switch, rather than introducing a divergent composition convention for this new paint
+ kind.
+- **Linked-only and SVG-only-fallback blips behave identically to ``**: since
+ `resolveBlipImage` is typically a closure directly over `ResolvePictureSurface`, a
+ ``'s `` that carries only `r:link` (no `r:embed`), or only the Microsoft
+ SVG-extension fallback pattern, throws exactly the same `PptxUnsupportedFeatureException`
+ (feature tokens `"pptx-image-link"`/`"pptx-image-svg-only"` respectively) `ResolvePictureSurface`
+ already throws for a `` in either case - no new, divergent exception behavior was
+ introduced for the fill-context case.
+- **Pythonpptx-DML-Fill Fixture**: this package's own `pythonpptx-dml-fill.pptx` fixture's slide 0
+ places six shapes in document order - an inherited fill, an explicit ``, a solid RGB
+ fill, a real embedded ``/`` picture
+ fill, a linear gradient fill, and finally an `` pattern fill, on this
+ _same_ slide. Before the picture-fill follow-up, rendering that slide threw `"pptx-picture-fill"`
+ as soon as the walk reached the fourth shape; the picture-fill follow-up made it proceed past
+ that shape and instead throw `"pptx-pattern-fill"` from the sixth shape. **Update (pattern-fill
+ follow-up)**: `divot` is now a covered preset (see _Pattern Fill (``) (Phase 2
+ Follow-Up)_ below), so this fixture's slide 0 now renders completely, end-to-end, without
+ throwing at all - see that section's own "Pythonpptx-DML-Fill Fixture" note for the full detail,
+ including slide 1's own three additional pattern-fill shapes. A genuine, full end-to-end
+ "renders without throwing, with a pixel-level sanity check" proof of picture fill specifically
+ instead uses a minimal, purpose-built single-shape package (mirroring this phase's own
+ `PptxSystemIntegrationTests`-style package-building helpers), since no single real fixture
+ shape-tree in this corpus isolates an ``/`` shape without another,
+ independently-interesting construct alongside it.
+
+#### Pattern Fill (``) (Phase 2 Follow-Up)
+
+A later Phase 2 follow-up (after the picture-fill follow-up above) lifted this package's own
+original fail-closed posture for `` used as a fill-definition child, for a documented
+subset of the 54 named ECMA-376 `ST_PresetPatternVal` preset-pattern values - the remaining names
+still throw `PptxUnsupportedFeatureException` (feature token `"pptx-pattern-fill"`), exactly as
+before this follow-up, rather than attempting the full preset table. Unlike picture fill, no new
+resolver parameter is threaded through `ResolveFill`'s own call sites - every covered preset is
+synthesized procedurally from its own resolved foreground/background colors, with no
+image/relationship resolution of any kind.
+
+- **`PptxPatternFill(PptxPresetPattern Preset, Rgba32 Foreground, Rgba32 Background)`**
+ (`PptxPaint.cs`) is the new closed `PptxPaint` subtype `ResolveFill` returns for a covered
+ ``. `PptxPresetPattern` (also `PptxPaint.cs`) is a plain enum naming
+ exactly the 30 of 54 covered preset values - see that enum's own `` for the full
+ covered/deferred lists, reproduced here: **covered** - the horizontal/vertical stripe family
+ (`horz`, `vert`, `ltHorz`, `ltVert`, `dkHorz`, `dkVert`), the diagonal-stripe family (`dnDiag`,
+ `upDiag`, `ltDnDiag`, `ltUpDiag`, `dkDnDiag`, `dkUpDiag`, `wdDnDiag`, `wdUpDiag`), the
+ cross-hatch family (`cross`, `diagCross`), the percentage/dot-density family (`pct5`, `pct10`,
+ `pct20`, `pct25`, `pct30`, `pct40`, `pct50`, `pct60`, `pct70`, `pct75`, `pct80`, `pct90`), and
+ the two fixture-mandatory names (`divot`, `wave`); **explicitly deferred** (still throwing) -
+ `narHorz`, `narVert`, `dashHorz`, `dashVert`, `dashDnDiag`, `dashUpDiag`, `diagBrick`,
+ `horzBrick`, `plaid`, `sphere`, `weave`, `shingle`, `trellis`, `zigZag`, `dotGrid`, `dotDmnd`,
+ `openDmnd`, `solidDmnd`, `smCheck`, `lgCheck`, `smGrid`, `lgGrid`, `smConfetti`, `lgConfetti`.
+- **A non-conformant, attribute-less `` (no `prst` at all)** - observed in this
+ package's own real `pythonpptx-dml-fill.pptx` fixture (slide 1's first shape, whose own text run
+ reads "autoshape with inherited fill") - is not a member of `PptxPresetPattern` at all:
+ `ResolveFill` resolves it directly to `PptxNoFill.Instance`, the same "no override" value a
+ shape with no fill-definition child at all would resolve to, rather than either falling back to
+ the shape's own `/` (its text run's own name notwithstanding - a deliberate
+ design decision, not an attempt to honor that name) or throwing. Resolving it to `PptxNoFill`
+ keeps this edge case consistent with every other "absent/unrecognized fill" case `ResolveFill`
+ already resolves to `PptxNoFill` (see _Fill Resolution_ above), rather than inventing new,
+ divergent handling for one non-conformant attribute-less element.
+- **Foreground/background color resolution**: ``/``'s own single
+ color-definition child is resolved via the existing `ResolveColor` helper - the same one
+ `` already uses - defaulting to black/white respectively when either child is
+ altogether absent. This package's own fixture's `bgClr` values use ``,
+ which required a narrowly-scoped companion fix (below) to `ResolveColor`'s own color-kind
+ dispatch, which previously threw for every `` unconditionally.
+- **`` companion fix (narrowly scoped)**: `ResolveColor`'s private
+ `ResolveBaseColor` helper gained a case for ``, resolving only `"white"`
+ (`Rgba32(255,255,255,255)`) and `"black"` (`Rgba32(0,0,0,255)`) - the two names this package's
+ own real fixture corpus requires - to their standard RGB values. Every other preset-color name
+ (ECMA-376's own `ST_PresetColorVal` names the full OOXML preset-color table beyond these two)
+ still throws `PptxUnsupportedFeatureException` (feature token `"pptx-color-kind"`), narrowed
+ rather than removed - this is a deliberately minimal fix, not an attempt at the full ~140-name
+ preset-color table.
+- **Tile synthesis**: `PptxPatternTileRenderer.RenderTile(PptxPresetPattern preset, Rgba32
+ foreground, Rgba32 background)` (new file `PptxPatternTileRenderer.cs`) procedurally synthesizes
+ a small (8x8-pixel) repeating-tile `Surface` per covered preset, painting each pixel foreground
+ or background per that preset's own geometric rule (for example, `horz`/`vert` alternate
+ whole rows/columns; `dnDiag`/`upDiag` follow a `(x +/- y) mod period` diagonal rule; the `pctNN`
+ family thresholds a per-pixel density test scaled to approximate the named percentage, not
+ pixel-perfect to PowerPoint's own exact dot placement; `divot`/`wave` approximate their own named
+ shapes closely enough to visually read as that pattern at tile scale). See that class's own
+ `` for the complete per-preset rule list.
+- **Painting, and a shape/connector/stroke/arrowhead transform-composition fix**: `FillPaint`'s
+ existing switch (`PptxDocument.Tables.cs`) gained a `case PptxPatternFill` branch synthesizing a
+ tile via the above, building a `TilePaint` from it (mirroring `PptxImageFill`'s own branch's
+ transform-composition math exactly), and filling via the existing
+ `PathFiller.Fill(Surface, Path, TilePaint, FillRule, float)` overload. Doing so exposed a real,
+ pre-existing gap in `PptxDocument.Render.cs`'s own `RenderShape`/`RenderPicture`/
+ `RenderConnector`/`PaintArrowhead`: each called the 3-parameter `FillPaint(Surface, Path,
+ PptxPaint)` overload for its own shape/connector fill and stroke-outline/arrowhead paint (unlike
+ `PaintTable`/`PaintCellBorder`'s own table-specific calls, which already compose the real
+ `shapeToSurfaceTransform`), which composes `Matrix3x2.Identity` into any `TilePaint`/`Gradient`'s
+ own transform instead of the shape's own local-to-surface transform - already documented (see
+ the 4-parameter `FillPaint` overload's own ``) as a pre-existing, codebase-wide gap
+ affecting every paint kind identically at those call sites, deliberately left as "a
+ separately-scoped follow-up" rather than fixed there. For a pattern fill's own small, fixed-size
+ repeat tile, this gap is not cosmetic: since a surface pixel coordinate divided by the fill's
+ own ~1/9525 EMU-per-pixel scale is always tiny relative to an 8-pixel tile step, every sampled
+ pixel across the _entire_ render surface (not just the one shape) collapses onto the same
+ single tile cell unless the shape's own position and scale are composed in first - flattening
+ the pattern to a single solid color (gradient/picture fill at these same call sites share the
+ identical gap, but it remains cosmetically invisible for a gradient spanning a shape's own
+ bounds, and picture fill's own existing test happens to position its shape at the origin).
+ Rather than generalizing a fix for every paint kind at these call sites (out of this follow-up's
+ own scope, and risking a behavior change to already-established gradient/picture-fill rendering
+ output), a narrowly-scoped `FillPaintComposingLocalTransform(Surface, Path, PptxPaint, Matrix3x2
+ localToSurface)` helper composes the real local-to-surface transform (via the 4-parameter
+ `FillPaint` overload) only when `paint is PptxPatternFill`, delegating to the unchanged
+ 3-parameter overload for every other paint kind.
+
+ `` (line/stroke) fills share the exact same shared `ResolveFill` resolver used for shape
+ fills (`ResolveLineStyle`, `PptxDocument.Paint.cs`, calls it identically for an ``
+ element's own fill-definition child) - so a covered-preset `
+ ...` line/stroke fill is equally resolvable, and was therefore equally
+ subject to this same transform-composition gap. This is now fixed too: `RenderShape`'s,
+ `RenderPicture`'s, and `RenderConnector`'s own stroke-outline `FillPaint` calls, and
+ `PaintArrowhead`'s two arrowhead-paint calls, all now use `FillPaintComposingLocalTransform`
+ instead of the 3-parameter overload directly - the shape/picture/connector calls pass their own
+ `localToSurface`; `PaintArrowhead`'s two calls pass its own already-computed `orientToSurface`
+ (the arrowhead's own rotated/translated local-to-surface composition), not the connector's plain
+ `localToSurface`, since an arrowhead's own local origin is itself offset/rotated relative to the
+ connector (see `PaintArrowhead`'s own remarks). Every one of these call sites is now covered for
+ `PptxPatternFill`, exactly like the pre-existing shape/connector _fill_ call sites.
+
+ The slide/layout/master background fill (`PptxDocument.Render.cs`'s own `Render` method) is now
+ also covered: `ResolveFill` is shared there too, so a background `` pattern fill is
+ equally resolvable, and the background rectangle's own local origin coincides with the slide's
+ own EMU origin, so the slide's own EMU-to-pixel `baseTransform` (already computed and in scope
+ at that call site, and already applied to the background path itself) plays exactly the same
+ role as a shape's own local-to-surface transform elsewhere. The background-fill call now uses
+ `FillPaintComposingLocalTransform(surface, backgroundPath, backgroundFill, baseTransform)`
+ instead of the 3-parameter overload directly - no remaining call site in production code calls
+ the 3-parameter overload directly any more.
+- **Pythonpptx-DML-Fill Fixture**: slide 0's sixth shape (see the _Picture Fill_ section's own
+ "Pythonpptx-DML-Fill Fixture" note above) is a `prst="divot"` pattern fill, now covered - the
+ slide renders completely, end-to-end, without throwing. Slide 1 adds three more shapes
+ exercising pattern fill specifically: a non-conformant, attribute-less `` (resolves
+ to `PptxNoFill`, see above), a `prst="divot"` shape, and a `prst="wave"` shape (both with
+ ``/``) - both
+ genuinely paint a visible mix of their own foreground and background colors once rendered,
+ proven by `PptxFixturesCorpusTests.cs`'s own pixel-sampling assertions.
+
+#### Table Parsing
+
+`PptxDocument.Tables.cs`'s `ParseTable(XElement graphicFrameElement, PptxTheme theme)` parses a
+``'s `//` into a `PptxTable` (`PptxTable.cs`):
+
+- **Graphic-frame kind dispatch**: a `` whose `uri` attribute does not end in
+ `"/table"` throws `PptxUnsupportedFeatureException` (feature token
+ `"pptx-graphic-frame-kind"`) - a `` can declare other graphic data kinds (for
+ example an embedded OLE object), each a well-formed-but-deliberately-unsupported construct
+ `ParseTable` itself does not implement. A missing ``/`` throws
+ `InvalidDataException` (structurally malformed). **`ParseTable` itself is unchanged as of
+ Phase 4**: a chart-kind `` (`uri` ending in `"/chart"`) is dispatched to the new
+ `ParseChart` by `ParseShapeTree` _before_ `ParseTable` is ever called for that graphic frame -
+ see _Chart Graphic Frames (Phase 4 — delegation to `CanvasNetCharts`)_ below - so `ParseTable`
+ only ever sees a chart-kind `` if a caller invokes it directly, bypassing
+ `ParseShapeTree`'s own dispatch.
+- **Column grid**: `/` elements resolve `PptxTable.ColumnWidthsEmu` (each
+ `w` attribute, in EMU); a missing `` or a non-numeric `w` throws
+ `InvalidDataException`.
+- **Rows**: each `` resolves a `PptxTableRow(HeightEmu, Cells)`; a missing/non-numeric `h`
+ attribute throws `InvalidDataException`.
+- **Cells**: `ParseTableCell(XElement tcElement, PptxTheme theme, float widthEmu, float
+ heightEmu)` resolves each `` into a `PptxTableCell`, reading `gridSpan`/`rowSpan`
+ (defaulting to `1`) and `hMerge`/`vMerge` (defaulting to `false`) attributes - each
+ merge-continuation placeholder cell (`hMerge`/`vMerge` set) is still structurally parsed (it is
+ a required, well-formed `` entry per ECMA-376), just excluded from
+ `ResolveCellRects`'s own output (see below). A non-numeric `gridSpan`/`rowSpan` throws
+ `InvalidDataException`.
+- **Cell fill/border/text reuse**: a cell's `` fill (via the already-verified Phase 1c
+ `ResolveFill`), left/right/top/bottom line styles (``/``/``/``, via
+ the already-verified Phase 1c `ResolveLineStyle`), and `` text body (via the
+ already-verified Phase 1d `ParseTextBody`) are each resolved **verbatim, unmodified** -
+ deliberately reusing the exact same resolvers an ordinary shape's own fill/border/text use,
+ rather than maintaining a second, divergent resolution path solely for table cells.
+
+**Signature deviation from the original plan**: the plan's one-line signature summary for
+`ParseTable` omitted a `theme` parameter; this implementation adds it (`ParseTable(XElement
+graphicFrameElement, PptxTheme theme)`), since `ParseTableCell`'s own fill resolution
+(`ResolveFill`) requires a `PptxTheme` to resolve theme-referenced colors - the plan's own
+detailed prose already implied this requirement even though its one-line signature summary did
+not spell it out explicitly.
+
+#### Cell-Rect Resolution
+
+`ResolveCellRects(PptxTable table)` resolves each of a table's non-merge-continuation cells (a
+cell with neither `hMerge` nor `vMerge` set) into a `PptxResolvedTableCell` carrying its own
+EMU-space rectangle (`X`, `Y`, `Width`, `Height`), by walking each row's `` entries in
+document order while accumulating a running column offset.
+
+**Merge-continuation column-advancement rule**: each `` XML entry - whether a real cell or
+an `hMerge`/`vMerge` continuation placeholder - represents (in the common, well-formed case) one
+physical grid column; a `gridSpan="N"` cell is typically followed by `(N-1)` separate continuation
+`` entries, each itself a single-column-wide placeholder. The per-iteration running column
+offset therefore normally advances by that **single column's own width**
+(`table.ColumnWidthsEmu[columnIndex]`), **never** by the governing cell's own full merged-span
+width - only the surviving (non-continuation) cell's own stored rectangle width uses the full
+merged-span sum (`SumColumnWidths(..., cell.GridSpan)`). Advancing by the merged width instead
+would double-count the columns already covered by that cell's own trailing continuation entries,
+corrupting every subsequent cell's computed left edge in the same row - a defect caught and fixed
+during this phase's own test-driven implementation (see `ResolveCellRects_HorizontalMerge_
+ComputesSpannedWidth`).
+
+**Corrected invariant - a schema-permitted gap, not a guarantee (Review Follow-Up)**: the
+preceding paragraph's original wording overstated ECMA-376's own actual guarantee. `CT_TableRow`
+declares its `tc` children as `minOccurs="0" maxOccurs="unbounded"` - the schema does **not**
+require a `gridSpan="N"` cell's `(N-1)` continuation placeholders to actually be present in the
+XML at all; a producer could omit them (or emit fewer than expected), and a conformant consumer
+must still render something sensible rather than corrupting every later cell's position in the
+row. `ResolveCellRects` now walks each row by index and, for every governing (non-continuation)
+cell, counts how many of the actually-present following entries are its own `hMerge`
+continuations (bounded by the cell's own declared `GridSpan - 1`); any shortfall between that
+actual count and the expected count is compensated defensively - both the running `xEmu` offset
+and `columnIndex` are advanced past the missing placeholders' own column widths before resuming
+the per-iteration loop - so a malformed row can no longer cause a later, unrelated cell to overlap
+the governing cell's own merged region. This mirrors the file's existing clamp-don't-throw
+convention elsewhere in this unit: a structurally-sparse row degrades to a best-effort, visually
+sensible layout rather than throwing (see
+`ResolveCellRects_GridSpanMissingHMergePlaceholder_CompensatesColumnAdvance`).
+
+#### Table Painting
+
+`PaintTable(Surface destination, PptxTable table, PptxTheme theme, Matrix3x2 shapeTransform,
+Func fontResolver)` paints each resolved cell rectangle in
+turn:
+
+- **Fill**: the cell's own resolved `PptxPaint` (solid or gradient) is painted via the core
+ `PathFiller`, after transforming the cell's own `Path.Rectangle` through `shapeTransform` (a
+ `PptxNoFill` cell paints no fill pixels at all).
+- **Border**: each of the cell's four resolved `PptxLineStyle`s (left/right/top/bottom, each
+ independently nullable) is stroked via the already-verified Phase 1c `ResolveStrokeOutline`
+ pattern, one side at a time.
+- **Text**: the cell's own text body (when present) is painted via the already-verified Phase 1d
+ `ResolveTextLayout`/`PaintTextLayout` pipeline, constrained to the cell's own rectangle
+ dimensions exactly as an ordinary shape's own text body would be.
+
+An empty cell (no fill, no border, no text) paints no pixels at all - a common, valid table shape.
+
+#### Shape Tree
+
+`PptxDocument.ShapeTree.cs`'s `ParseShapeTree(XElement spTreeOrGroupElement, Func
+themeResolver)` recursively parses a `