From 9e38d472b65d2c7f0e9fc988f2b08390ac44bd36 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 10 Nov 2025 18:09:55 +0000
Subject: [PATCH 1/4] Initial plan
From 0ceb27fc44239dc8a51c70d6b3f26255df94a042 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 10 Nov 2025 18:25:15 +0000
Subject: [PATCH 2/4] Extract image functionality into ImageHelper class
- Created new ImageHelper.cs class with all image-related methods
- Moved AddImagePart, CreateImageElement, GetNextDocPrId, TryGetJustification, TryParseLengthToEmu, ConvertJustificationToString, TryCalculateImageDimensions, TryGetPixelSize, TryReadPngDimensions, TryReadJpegDimensions, TryReadGifDimensions to ImageHelper
- Updated DocumentAssembler.cs to call methods from ImageHelper
- Removed duplicate image code from DocumentAssembler.cs
- All 789 tests passing
Co-authored-by: stesee <168659+stesee@users.noreply.github.com>
---
.gitignore | 3 +-
DOCUMENT_ASSEMBLER.md | 78 +++
.../DocumentAssemblerTests.cs | 288 ++++++++++-
OpenXmlPowerTools.sln | 9 +
.../DocumentAssembler/DocumentAssembler.cs | 100 +++-
.../DocumentAssembler/ImageHelper.cs | 484 ++++++++++++++++++
OpenXmlPowerTools/DocumentAssembler/PA.cs | 8 +-
OpenXmlPowerTools/HtmlToWmlConverterCore.cs | 16 +-
.../DocumentAssembler04.cs | 123 +++++
.../DocumentAssembler04.csproj | 21 +
.../DocumentAssembler04/SampleData.xml | 6 +
.../DocumentAssembler04/TemplateDocument.docx | Bin 0 -> 2535 bytes
12 files changed, 1121 insertions(+), 15 deletions(-)
create mode 100644 DOCUMENT_ASSEMBLER.md
create mode 100644 OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
create mode 100644 OpenXmlPowerToolsExamples/DocumentAssembler04/DocumentAssembler04.cs
create mode 100644 OpenXmlPowerToolsExamples/DocumentAssembler04/DocumentAssembler04.csproj
create mode 100644 OpenXmlPowerToolsExamples/DocumentAssembler04/SampleData.xml
create mode 100644 OpenXmlPowerToolsExamples/DocumentAssembler04/TemplateDocument.docx
diff --git a/.gitignore b/.gitignore
index 1f6ad508..2d302bc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,5 @@ TestResults/
.idea/
_ReSharper.Caches/
-TestResult/
\ No newline at end of file
+TestResult/
+ExampleOutput*
diff --git a/DOCUMENT_ASSEMBLER.md b/DOCUMENT_ASSEMBLER.md
new file mode 100644
index 00000000..14a96dc1
--- /dev/null
+++ b/DOCUMENT_ASSEMBLER.md
@@ -0,0 +1,78 @@
+# OpenXmlPowerTools
+
+OpenXmlPowerTools provides guidance and example code for programming with Open XML Documents (DOCX, XLSX, and PPTX). It is based on, and extends the functionality of the Open XML SDK.
+
+## Document Assembler
+
+The `DocumentAssembler` module is a powerful tool for generating documents from templates and data. It allows you to create `.docx` files with dynamic content, such as tables, conditional sections, and repeating elements, based on data from an XML file.
+
+### Key Features
+
+* **Template-Based Document Generation**: Create documents from Word templates (`.docx`) and populate them with data from XML files.
+* **Content Replacement**: Use simple placeholders in your template to insert data from your XML file.
+* **Dynamic Tables**: Automatically generate tables in your document based on data from your XML file.
+* **Conditional Content**: Include or exclude parts of your document based on conditions in your data.
+* **Repeating Content**: Repeat sections of your document for each item in a collection in your data.
+* **Error Handling**: The `DocumentAssembler` will report errors in the generated document if it encounters any issues with your template or data.
+
+### How it Works
+
+The `DocumentAssembler` works by processing a Word document that contains special markup in content controls or in paragraphs. This markup defines how the document should be assembled based on the provided XML data.
+
+The process is as follows:
+
+1. **Create a Template**: Start with a regular Word document (`.docx`).
+2. **Add Placeholders**: Use content controls or special syntax in paragraphs to define placeholders for your data.
+3. **Provide Data**: Create an XML file that contains the data you want to insert into the document.
+4. **Assemble the Document**: Use the `DocumentAssembler.AssembleDocument` method to merge the template and data, producing a new Word document.
+
+### Template Syntax
+
+The template syntax uses XML elements within content controls or as text in the format `<#ElementName ... #>`.
+
+#### Content Replacement
+
+To replace a placeholder with a value from your XML data, you can use the `Content` element. The `Select` attribute contains an XPath expression to select the data from the XML file.
+
+**Example:**
+
+`<#Content Select="Customer/Name" #>`
+
+#### Tables
+
+To generate a table, you use the `Table` element. The `Select` attribute specifies the collection of data to iterate over. The table in the template must have a prototype row, which will be repeated for each item in the data.
+
+**Example:**
+
+`<#Table Select="Customers/Customer" #>`
+
+#### Conditional Content
+
+You can conditionally include content using the `Conditional` element. The `Select` attribute specifies the data to test, and the `Match` or `NotMatch` attribute specifies the value to compare against.
+
+**Example:**
+
+`<#Conditional Select="Customer/Country" Match="USA" #>
+... content to include if the customer is from the USA ...
+<#EndConditional #>`
+
+#### Repeating Content
+
+To repeat a section of the document, you can use the `Repeat` element. The `Select` attribute specifies the collection of data to iterate over.
+
+**Example:**
+
+`<#Repeat Select="Customers/Customer" #>
+... content to repeat for each customer ...
+<#EndRepeat #>`
+
+### Getting Started
+
+To use the `DocumentAssembler`, you will need to:
+
+1. Add a reference to the `OpenXmlPowerTools` library in your project.
+2. Create a Word template with the appropriate placeholders.
+3. Create an XML data file.
+4. Call the `DocumentAssembler.AssembleDocument` method to generate your document.
+
+For more detailed examples and documentation, please refer to the `DocumentAssembler`, `DocumentAssembler01`, `DocumentAssembler02`, and `DocumentAssembler03` projects in the `OpenXmlPowerToolsExamples` directory.
\ No newline at end of file
diff --git a/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs b/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs
index e69616a0..5432cd35 100644
--- a/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs
+++ b/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs
@@ -1,5 +1,6 @@
using Codeuctivity.OpenXmlPowerTools;
using DocumentFormat.OpenXml.Packaging;
+using DocumentFormat.OpenXml.Wordprocessing;
using DocumentFormat.OpenXml.Validation;
using System;
using System.Collections.Generic;
@@ -9,6 +10,7 @@
using System.Xml;
using System.Xml.Linq;
using Xunit;
+using System.Globalization;
namespace Codeuctivity.Tests
{
@@ -200,6 +202,290 @@ public void DA103_UseXmlDocument(string name, string data, bool err)
Assert.Equal(err, returnedTemplateError);
}
+ [Fact]
+ public void AssembleDocument_ImageMetadataCreatesImageParts()
+ {
+ var template = CreateTemplateDocument("DA-ImageTemplate.docx",
+ "<# #>",
+ "<# #>");
+ var data = new XElement("Images",
+ new XElement("Image", TinyPngBase64),
+ new XElement("Image", TinyPngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ using var ms = new MemoryStream(assembled.DocumentByteArray);
+ using var wDoc = WordprocessingDocument.Open(ms, false);
+ var mainPart = wDoc.MainDocumentPart;
+
+ Assert.Equal(2, mainPart.ImageParts.Count());
+
+ var docPrIds = mainPart
+ .GetXDocument()
+ .Descendants(WP.docPr)
+ .Select(d => (int)d.Attribute("id"))
+ .ToList();
+ Assert.Equal(new[] { 1, 2 }, docPrIds);
+
+ var blipIds = mainPart
+ .GetXDocument()
+ .Descendants(A.blip)
+ .Select(b => (string)b.Attribute(R.embed))
+ .ToList();
+ Assert.Equal(2, blipIds.Count);
+ Assert.All(blipIds, id => Assert.False(string.IsNullOrEmpty(id)));
+ }
+
+ [Fact]
+ public void AssembleDocument_InvalidBase64RaisesTemplateError()
+ {
+ var template = CreateTemplateDocument("DA-ImageInvalidTemplate.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", "not-base64"));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.True(templateError);
+
+ using var ms = new MemoryStream(assembled.DocumentByteArray);
+ using var wDoc = WordprocessingDocument.Open(ms, false);
+ var text = string.Concat(wDoc.MainDocumentPart.GetXDocument().Descendants(W.t).Select(t => (string)t));
+ Assert.Contains("Image:", text);
+ }
+
+ [Fact]
+ public void AssembleDocument_OptionalImageIsSkippedWhenMissing()
+ {
+ var template = CreateTemplateDocument("DA-OptionalImageTemplate.docx", "<# #>");
+ var data = new XElement("Images");
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ using var ms = new MemoryStream(assembled.DocumentByteArray);
+ using var wDoc = WordprocessingDocument.Open(ms, false);
+ var drawings = wDoc.MainDocumentPart.GetXDocument().Descendants(W.drawing);
+ Assert.Empty(drawings);
+ Assert.Empty(wDoc.MainDocumentPart.ImageParts);
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageWidthScalesHeight()
+ {
+ var template = CreateTemplateDocument("DA-ImageWidth.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", LargeSamplePngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ var (paragraph, extent) = ExtractImageParagraph(assembled);
+ Assert.Null(paragraph.Element(W.pPr));
+ Assert.Equal(EmusPerInch.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cx"));
+ Assert.Equal((EmusPerInch / 2).ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cy"));
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageHeightScalesWidth()
+ {
+ var template = CreateTemplateDocument("DA-ImageHeight.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", LargeSamplePngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ var (paragraph, extent) = ExtractImageParagraph(assembled);
+ Assert.Null(paragraph.Element(W.pPr));
+ Assert.Equal((EmusPerInch * 2).ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cx"));
+ Assert.Equal(EmusPerInch.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cy"));
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageMaxWidthCentersParagraph()
+ {
+ var template = CreateTemplateDocument("DA-ImageMaxWidth.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", LargeSamplePngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ var (paragraph, extent) = ExtractImageParagraph(assembled);
+ Assert.Equal("center", (string)paragraph.Element(W.pPr)?.Element(W.jc)?.Attribute(W.val));
+ var expectedWidth = 3 * EmusPerInch;
+ var expectedHeight = expectedWidth / 2;
+ Assert.Equal(expectedWidth.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cx"));
+ Assert.Equal(expectedHeight.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cy"));
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageMaxHeightJustifiesParagraph()
+ {
+ var template = CreateTemplateDocument("DA-ImageMaxHeight.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", TallPngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ var (paragraph, extent) = ExtractImageParagraph(assembled);
+ Assert.Equal("both", (string)paragraph.Element(W.pPr)?.Element(W.jc)?.Attribute(W.val));
+ var maxHeight = 150 * EmusPerPixel;
+ var expectedWidth = (200 * EmusPerPixel) * (maxHeight / (400 * EmusPerPixel));
+ Assert.Equal(expectedWidth.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cx"));
+ Assert.Equal(maxHeight.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cy"));
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageMaxHeightAndWidthAppliesLargestConstraint()
+ {
+ var template = CreateTemplateDocument("DA-ImageMaxBoth.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", LargeSamplePngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ var (paragraph, extent) = ExtractImageParagraph(assembled);
+ Assert.Equal("right", (string)paragraph.Element(W.pPr)?.Element(W.jc)?.Attribute(W.val));
+ Assert.Equal((2 * EmusPerInch).ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cx"));
+ Assert.Equal(EmusPerInch.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cy"));
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageInvalidAlignProducesTemplateError()
+ {
+ var template = CreateTemplateDocument("DA-ImageInvalidAlign.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", LargeSamplePngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.True(templateError);
+
+ var text = GetDocumentText(assembled);
+ Assert.Contains("Align attribute must be one of Left, Center, Right, or Justify.", text);
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageInvalidWidthReportsError()
+ {
+ var template = CreateTemplateDocument("DA-ImageInvalidWidth.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", LargeSamplePngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.True(templateError);
+ var text = GetDocumentText(assembled);
+ Assert.Contains("Unable to parse length 'abc'.", text);
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageZeroHeightReportsError()
+ {
+ var template = CreateTemplateDocument("DA-ImageZeroHeight.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", LargeSamplePngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.True(templateError);
+ var text = GetDocumentText(assembled);
+ Assert.Contains("Length value '0in' must be greater than zero.", text);
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageGifDimensionsFallback()
+ {
+ var template = CreateTemplateDocument("DA-ImageGifFallback.docx", "<# #>");
+ var data = new XElement("Images", new XElement("Image", TruncatedGifBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ var (paragraph, extent) = ExtractImageParagraph(assembled);
+ Assert.Null(paragraph.Element(W.pPr));
+ Assert.Equal((200 * EmusPerPixel).ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cx"));
+ Assert.Equal((80 * EmusPerPixel).ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cy"));
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageRespectsMaxDimensionsAndAlign()
+ {
+ var template = CreateTemplateDocument("DA-ImageConstraintTemplate.docx", "<# #>");
+ var data = new XElement("ImageData", new XElement("Image", LargeSamplePngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ using var ms = new MemoryStream(assembled.DocumentByteArray);
+ using var wDoc = WordprocessingDocument.Open(ms, false);
+ var paragraph = wDoc.MainDocumentPart.GetXDocument().Descendants(W.p).FirstOrDefault(p => p.Descendants(W.drawing).Any());
+ Assert.NotNull(paragraph);
+ var jc = paragraph!.Element(W.pPr)?.Element(W.jc)?.Attribute(W.val)?.Value;
+ Assert.Equal("center", jc);
+
+ var extent = paragraph.Descendants(WP.extent).First();
+ Assert.Equal(2857500d.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cx"));
+ Assert.Equal(1428750d.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cy"));
+ }
+
+ [Fact]
+ public void AssembleDocument_ImageUsesExplicitDimensions()
+ {
+ var template = CreateTemplateDocument("DA-ImageExplicitSizeTemplate.docx", "<# #>");
+ var data = new XElement("ImageData", new XElement("Image", LargeSamplePngBase64));
+
+ var assembled = DocumentAssembler.AssembleDocument(template, data, out var templateError);
+ Assert.False(templateError);
+
+ using var ms = new MemoryStream(assembled.DocumentByteArray);
+ using var wDoc = WordprocessingDocument.Open(ms, false);
+ var paragraph = wDoc.MainDocumentPart.GetXDocument().Descendants(W.p).FirstOrDefault(p => p.Descendants(W.drawing).Any());
+ Assert.NotNull(paragraph);
+ var jc = paragraph!.Element(W.pPr)?.Element(W.jc)?.Attribute(W.val)?.Value;
+ Assert.Equal("right", jc);
+
+ var extent = paragraph.Descendants(WP.extent).First();
+ Assert.Equal(1828800d.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cx"));
+ Assert.Equal(914400d.ToString("0", CultureInfo.InvariantCulture), (string)extent.Attribute("cy"));
+ }
+
+ private static WmlDocument CreateTemplateDocument(string fileName, params string[] paragraphTexts)
+ {
+ using var ms = new MemoryStream();
+ using (var wDoc = WordprocessingDocument.Create(ms, DocumentFormat.OpenXml.WordprocessingDocumentType.Document))
+ {
+ var mainPart = wDoc.AddMainDocumentPart();
+ var body = new Body(paragraphTexts.Select(text =>
+ new Paragraph(
+ new Run(
+ new Text(text) { Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve }))));
+ mainPart.Document = new Document(body);
+ mainPart.Document.Save();
+ }
+
+ return new WmlDocument(fileName, ms.ToArray());
+ }
+
+ private static (XElement Paragraph, XElement Extent) ExtractImageParagraph(WmlDocument document)
+ {
+ using var ms = new MemoryStream();
+ ms.Write(document.DocumentByteArray, 0, document.DocumentByteArray.Length);
+ using var wDoc = WordprocessingDocument.Open(ms, false);
+ var main = wDoc.MainDocumentPart.GetXDocument();
+ var paragraph = main.Descendants(W.p).First(p => p.Descendants(W.drawing).Any());
+ var extent = paragraph.Descendants(WP.extent).First();
+ return (paragraph, extent);
+ }
+
+ private static string GetDocumentText(WmlDocument document)
+ {
+ using var ms = new MemoryStream();
+ ms.Write(document.DocumentByteArray, 0, document.DocumentByteArray.Length);
+ using var wDoc = WordprocessingDocument.Open(ms, false);
+ var main = wDoc.MainDocumentPart.GetXDocument();
+ return string.Concat(main.Descendants(W.t).Select(t => (string)t));
+ }
+
+ private const double EmusPerInch = 914400d;
+ private const double EmusPerPixel = EmusPerInch / 96d;
+ private const string TinyPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
+ private const string LargeSamplePngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAZAAAADICAIAAABJdyC1AAACvElEQVR4nO3WsQ3DQBAEsXvB/bf8akHZYQyygo0Ge2buABQ82wMAvhIsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyPhtDyi5c7Yn8J/O3O0JDR4WkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWScmbu9AeATDwvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQKm4gVVxAWP2c47WwAAAABJRU5ErkJggg==";
+
+ private const string WidePngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAZAAAADICAIAAABJdyC1AAACuUlEQVR4nO3UMQ7CQBAEwT3EvxEvXz/BZKalqniCifrs7gAUvGfmnO/TNwBu7H5edxuAfyFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFFzMzu2y8A5u4PQZkIj89BEMEAAAAASUVORK5CYII=";
+ private const string TallPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAMgAAAGQCAIAAABkkLjnAAAEF0lEQVR4nO3S0QkCURAEwX1i3mLke0lcI3hVAQzz0Wd3B+72npnzPbfv8mT72devP/CfhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkzu42y8yTXVnDDBu1Y983AAAAAElFTkSuQmCC";
+ private const string TruncatedGifBase64 = "R0lGODlhyABQAA==";
private static readonly List s_ExpectedErrors = new List()
{
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:evenHBand' attribute is not declared.",
@@ -218,4 +504,4 @@ public void DA103_UseXmlDocument(string name, string data, bool err)
"The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:oddVBand' attribute is not declared.",
};
}
-}
\ No newline at end of file
+}
diff --git a/OpenXmlPowerTools.sln b/OpenXmlPowerTools.sln
index 57fdb2bd..77971c1b 100644
--- a/OpenXmlPowerTools.sln
+++ b/OpenXmlPowerTools.sln
@@ -78,6 +78,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkupSimplifierApp", "OpenXmlPowerToolsExamples\MarkupSimplifierApp\MarkupSimplifierApp.csproj", "{6731E031-9C81-48FB-97A7-0E945993BCE2}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenXmlPowerToolsExamples", "OpenXmlPowerToolsExamples", "{AA7E2DBC-70B3-4F8A-AC47-4416CDA9F3DA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentAssembler04", "OpenXmlPowerToolsExamples\DocumentAssembler04\DocumentAssembler04.csproj", "{94E64B7D-BB4A-4478-B3BF-69F83C2FD379}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -208,6 +212,10 @@ Global
{6731E031-9C81-48FB-97A7-0E945993BCE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6731E031-9C81-48FB-97A7-0E945993BCE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6731E031-9C81-48FB-97A7-0E945993BCE2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {94E64B7D-BB4A-4478-B3BF-69F83C2FD379}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {94E64B7D-BB4A-4478-B3BF-69F83C2FD379}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94E64B7D-BB4A-4478-B3BF-69F83C2FD379}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {94E64B7D-BB4A-4478-B3BF-69F83C2FD379}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -242,6 +250,7 @@ Global
{D4078011-2611-46A7-8A30-55E4AB8FA786} = {A83D6B58-6D38-46AF-8C20-5CFC170A1063}
{DCE8EC51-1E58-49A0-82CF-5BE269FA0A9D} = {A83D6B58-6D38-46AF-8C20-5CFC170A1063}
{6731E031-9C81-48FB-97A7-0E945993BCE2} = {A83D6B58-6D38-46AF-8C20-5CFC170A1063}
+ {94E64B7D-BB4A-4478-B3BF-69F83C2FD379} = {AA7E2DBC-70B3-4F8A-AC47-4416CDA9F3DA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E623EFF5-2CA4-4FA0-B3AB-53F921DA212E}
diff --git a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs
index c838e7bf..94d4a7a0 100644
--- a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs
+++ b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs
@@ -1,4 +1,5 @@
using DocumentFormat.OpenXml.Packaging;
+using DocumentFormat.OpenXml.Wordprocessing;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -66,7 +67,7 @@ private static void ProcessTemplatePart(XElement data, TemplateError te, OpenXml
ProcessOrphanEndRepeatEndConditional(xDocRoot, te);
// do the actual content replacement
- xDocRoot = ContentReplacementTransform(xDocRoot, data, te) as XElement;
+ xDocRoot = ContentReplacementTransform(xDocRoot, data, te, part) as XElement;
xDoc.Elements().First().ReplaceWith(xDocRoot);
part.PutXDocument();
@@ -508,6 +509,25 @@ private class RunReplacementInfo
",
}
},
+ {
+ PA.Image,
+ new PASchemaSet() {
+ XsdMarkup =
+ @"
+
+
+
+
+
+
+
+
+
+
+
+ ",
+ }
+ },
{
PA.Table,
new PASchemaSet() {
@@ -601,7 +621,7 @@ private class RunReplacementInfo
private static Dictionary s_PASchemaSets;
- private static object? ContentReplacementTransform(XNode node, XElement data, TemplateError templateError)
+ private static object? ContentReplacementTransform(XNode node, XElement data, TemplateError templateError, OpenXmlPart owningPart)
{
if (node is XElement element)
{
@@ -612,7 +632,7 @@ private class RunReplacementInfo
var xPath = (string)element.Attribute(PA.Select);
var optionalString = (string)element.Attribute(PA.Optional);
- var optional = (optionalString != null && optionalString.ToLower() == "true");
+ var optional = bool.TryParse(optionalString, out var optionalValue) && optionalValue;
string newValue;
try
@@ -649,11 +669,73 @@ private class RunReplacementInfo
return list;
}
}
+ if (element.Name == PA.Image)
+ {
+ var xPath = (string)element.Attribute(PA.Select);
+ var optionalString = (string)element.Attribute(PA.Optional);
+ var optional = bool.TryParse(optionalString, out var optionalValue) && optionalValue;
+ var alignString = (string)element.Attribute(PA.Align);
+ var widthAttr = (string)element.Attribute(PA.Width);
+ var heightAttr = (string)element.Attribute(PA.Height);
+ var maxWidthAttr = (string)element.Attribute(PA.MaxWidth);
+ var maxHeightAttr = (string)element.Attribute(PA.MaxHeight);
+
+ string base64Content;
+ try
+ {
+ base64Content = EvaluateXPathToString(data, xPath, optional);
+ }
+ catch (XPathException e)
+ {
+ return CreateContextErrorMessage(element, "XPathException: " + e.Message, templateError);
+ }
+
+ if (string.IsNullOrEmpty(base64Content))
+ {
+ return null;
+ }
+
+ byte[] imageBytes;
+ try
+ {
+ imageBytes = Convert.FromBase64String(base64Content);
+ }
+ catch (FormatException e)
+ {
+ return CreateContextErrorMessage(element, "Image: " + e.Message, templateError);
+ }
+
+ if (!ImageHelper.TryGetJustification(alignString, out var justification, out var justificationError))
+ {
+ return CreateContextErrorMessage(element, justificationError, templateError);
+ }
+
+ if (owningPart == null)
+ {
+ throw new OpenXmlPowerToolsException("Image: owning part is not available.");
+ }
+
+ if (!ImageHelper.TryCalculateImageDimensions(imageBytes, widthAttr, heightAttr, maxWidthAttr, maxHeightAttr, out var widthEmu, out var heightEmu, out var sizeError))
+ {
+ return CreateContextErrorMessage(element, sizeError, templateError);
+ }
+
+ var imagePart = ImageHelper.AddImagePart(owningPart);
+ using (var stream = new MemoryStream(imageBytes))
+ {
+ imagePart.FeedData(stream);
+ }
+
+ var relationshipId = owningPart.GetIdOfPart(imagePart);
+ var docPrId = ImageHelper.GetNextDocPrId(owningPart);
+ var imageElement = ImageHelper.CreateImageElement(relationshipId, docPrId, widthEmu, heightEmu, justification);
+ return imageElement;
+ }
if (element.Name == PA.Repeat)
{
var selector = (string)element.Attribute(PA.Select);
var optionalString = (string)element.Attribute(PA.Optional);
- var optional = (optionalString != null && optionalString.ToLower() == "true");
+ var optional = bool.TryParse(optionalString, out var optionalValue) && optionalValue;
IEnumerable repeatingData;
try
@@ -676,7 +758,7 @@ private class RunReplacementInfo
{
var content = element
.Elements()
- .Select(e => ContentReplacementTransform(e, d, templateError))
+ .Select(e => ContentReplacementTransform(e, d, templateError, owningPart))
.ToList();
return content;
})
@@ -706,7 +788,7 @@ private class RunReplacementInfo
.Skip(2)
.ToList();
var footerRows = footerRowsBeforeTransform
- .Select(x => ContentReplacementTransform(x, data, templateError))
+ .Select(x => ContentReplacementTransform(x, data, templateError, owningPart))
.ToList();
if (protoRow == null)
{
@@ -784,14 +866,14 @@ private class RunReplacementInfo
if ((match != null && testValue == match) || (notMatch != null && testValue != notMatch))
{
- var content = element.Elements().Select(e => ContentReplacementTransform(e, data, templateError));
+ var content = element.Elements().Select(e => ContentReplacementTransform(e, data, templateError, owningPart));
return content;
}
return null;
}
return new XElement(element.Name,
element.Attributes(),
- element.Nodes().Select(n => ContentReplacementTransform(n, data, templateError)));
+ element.Nodes().Select(n => ContentReplacementTransform(n, data, templateError, owningPart)));
}
return node;
}
@@ -884,4 +966,4 @@ private static string EvaluateXPathToString(XElement element, string xPath, bool
return xPathSelectResult.ToString();
}
}
-}
\ No newline at end of file
+}
diff --git a/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs b/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
new file mode 100644
index 00000000..35637243
--- /dev/null
+++ b/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
@@ -0,0 +1,484 @@
+using DocumentFormat.OpenXml.Packaging;
+using DocumentFormat.OpenXml.Wordprocessing;
+using SkiaSharp;
+using System.Globalization;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace Codeuctivity.OpenXmlPowerTools
+{
+ ///
+ /// Helper class for processing images in DocumentAssembler.
+ /// Handles image insertion, dimension calculation, and format detection.
+ ///
+ internal static class ImageHelper
+ {
+ private const double EmusPerInch = 914400d;
+ private const double EmusPerPoint = 12700d;
+ private const double EmusPerPixel = 914400d / 96d;
+ private const double EmusPerMillimeter = EmusPerInch / 25.4d;
+ private const double EmusPerCentimeter = EmusPerMillimeter * 10d;
+
+ ///
+ /// Adds an image part to the appropriate OpenXml part type.
+ ///
+ internal static ImagePart AddImagePart(OpenXmlPart part)
+ {
+ return part switch
+ {
+ MainDocumentPart mainDocumentPart => mainDocumentPart.AddImagePart(ImagePartType.Png),
+ HeaderPart headerPart => headerPart.AddImagePart(ImagePartType.Png),
+ FooterPart footerPart => footerPart.AddImagePart(ImagePartType.Png),
+ FootnotesPart footnotesPart => footnotesPart.AddImagePart(ImagePartType.Png),
+ EndnotesPart endnotesPart => endnotesPart.AddImagePart(ImagePartType.Png),
+ _ => throw new OpenXmlPowerToolsException($"Image: unsupported part type {part.GetType().Name}."),
+ };
+ }
+
+ ///
+ /// Creates an XML element representing an image in a Word document.
+ ///
+ internal static XElement CreateImageElement(string relationshipId, int docPrId, double widthEmu, double heightEmu, JustificationValues? justification)
+ {
+ var widthAttribute = widthEmu.ToString("0", CultureInfo.InvariantCulture);
+ var heightAttribute = heightEmu.ToString("0", CultureInfo.InvariantCulture);
+ XElement? paragraphProperties = null;
+ if (justification.HasValue && justification.Value != JustificationValues.Left)
+ {
+ paragraphProperties = new XElement(W.pPr,
+ new XElement(W.jc, new XAttribute(W.val, ConvertJustificationToString(justification.Value))));
+ }
+
+ var pictureName = $"Picture {docPrId}";
+ var element =
+ new XElement(W.p,
+ paragraphProperties,
+ new XElement(W.r,
+ new XElement(W.drawing,
+ new XElement(WP.inline,
+ new XElement(WP.extent, new XAttribute("cx", widthAttribute), new XAttribute("cy", heightAttribute)),
+ new XElement(WP.effectExtent, new XAttribute("l", "0"), new XAttribute("t", "0"), new XAttribute("r", "0"), new XAttribute("b", "0")),
+ new XElement(WP.docPr, new XAttribute("id", docPrId), new XAttribute("name", pictureName)),
+ new XElement(WP.cNvGraphicFramePr,
+ new XElement(A.graphicFrameLocks, new XAttribute("noChangeAspect", "1"))),
+ new XElement(A.graphic,
+ new XElement(A.graphicData, new XAttribute("uri", "http://schemas.openxmlformats.org/drawingml/2006/picture"),
+ new XElement(Pic._pic,
+ new XElement(Pic.nvPicPr,
+ new XElement(Pic.cNvPr, new XAttribute("id", "0"), new XAttribute("name", pictureName)),
+ new XElement(Pic.cNvPicPr)),
+ new XElement(Pic.blipFill,
+ new XElement(A.blip, new XAttribute(R.embed, relationshipId)),
+ new XElement(A.stretch,
+ new XElement(A.fillRect))),
+ new XElement(Pic.spPr,
+ new XElement(A.xfrm,
+ new XElement(A.off, new XAttribute("x", "0"), new XAttribute("y", "0")),
+ new XElement(A.ext, new XAttribute("cx", widthAttribute), new XAttribute("cy", heightAttribute))),
+ new XElement(A.prstGeom, new XAttribute("prst", "rect"),
+ new XElement(A.avLst))))))))));
+ return element;
+ }
+
+ ///
+ /// Gets the next unique document property ID for an image.
+ ///
+ internal static int GetNextDocPrId(OpenXmlPart part)
+ {
+ var tracker = part.Annotation();
+ if (tracker == null)
+ {
+ var existingIds = part
+ .GetXDocument()
+ .Descendants(WP.docPr)
+ .Select(dp => (int?)dp.Attribute("id") ?? 0);
+ var maxId = existingIds.Any() ? existingIds.Max() : 0;
+ tracker = new ImageIdTracker { NextId = maxId + 1 };
+ part.AddAnnotation(tracker);
+ }
+
+ return tracker.NextId++;
+ }
+
+ ///
+ /// Tracks the next available image ID for a document part.
+ ///
+ private sealed class ImageIdTracker
+ {
+ public int NextId { get; set; }
+ }
+
+ ///
+ /// Tries to parse an alignment string into a JustificationValues enum.
+ ///
+ internal static bool TryGetJustification(string? align, out JustificationValues? justification, out string errorMessage)
+ {
+ justification = null;
+ errorMessage = string.Empty;
+ if (string.IsNullOrWhiteSpace(align))
+ {
+ return true;
+ }
+
+ switch (align.Trim().ToLowerInvariant())
+ {
+ case "left":
+ justification = JustificationValues.Left;
+ return true;
+ case "center":
+ case "centre":
+ justification = JustificationValues.Center;
+ return true;
+ case "right":
+ justification = JustificationValues.Right;
+ return true;
+ case "justify":
+ case "both":
+ justification = JustificationValues.Both;
+ return true;
+ default:
+ errorMessage = "Image: Align attribute must be one of Left, Center, Right, or Justify.";
+ return false;
+ }
+ }
+
+ ///
+ /// Tries to parse a length string with units (px, pt, cm, mm, in, emu) into EMUs.
+ ///
+ internal static bool TryParseLengthToEmu(string? rawValue, out double? emuValue, out string errorMessage)
+ {
+ emuValue = null;
+ errorMessage = string.Empty;
+ if (string.IsNullOrWhiteSpace(rawValue))
+ {
+ return true;
+ }
+
+ var value = rawValue.Trim().ToLowerInvariant();
+ double multiplier;
+ if (value.EndsWith("px"))
+ {
+ multiplier = EmusPerPixel;
+ value = value[..^2];
+ }
+ else if (value.EndsWith("pt"))
+ {
+ multiplier = EmusPerPoint;
+ value = value[..^2];
+ }
+ else if (value.EndsWith("cm"))
+ {
+ multiplier = EmusPerCentimeter;
+ value = value[..^2];
+ }
+ else if (value.EndsWith("mm"))
+ {
+ multiplier = EmusPerMillimeter;
+ value = value[..^2];
+ }
+ else if (value.EndsWith("in"))
+ {
+ multiplier = EmusPerInch;
+ value = value[..^2];
+ }
+ else if (value.EndsWith("emu"))
+ {
+ multiplier = 1d;
+ value = value[..^3];
+ }
+ else
+ {
+ multiplier = EmusPerPixel;
+ }
+
+ if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
+ {
+ errorMessage = $"Image: Unable to parse length '{rawValue}'.";
+ return false;
+ }
+
+ if (parsed <= 0)
+ {
+ errorMessage = $"Image: Length value '{rawValue}' must be greater than zero.";
+ return false;
+ }
+
+ emuValue = parsed * multiplier;
+ return true;
+ }
+
+ ///
+ /// Converts a JustificationValues enum to its string representation.
+ ///
+ internal static string ConvertJustificationToString(JustificationValues value)
+ {
+ if (value == JustificationValues.Left)
+ {
+ return "left";
+ }
+
+ if (value == JustificationValues.Center)
+ {
+ return "center";
+ }
+
+ if (value == JustificationValues.Right)
+ {
+ return "right";
+ }
+
+ if (value == JustificationValues.Both)
+ {
+ return "both";
+ }
+
+ if (value == JustificationValues.Distribute)
+ {
+ return "distribute";
+ }
+
+ if (value == JustificationValues.Start)
+ {
+ return "start";
+ }
+
+ if (value == JustificationValues.End)
+ {
+ return "end";
+ }
+
+ return value.ToString().ToLowerInvariant();
+ }
+
+ ///
+ /// Calculates image dimensions in EMUs, applying width, height, and max constraints while preserving aspect ratio.
+ ///
+ internal static bool TryCalculateImageDimensions(
+ byte[] imageBytes,
+ string? widthAttr,
+ string? heightAttr,
+ string? maxWidthAttr,
+ string? maxHeightAttr,
+ out double widthEmu,
+ out double heightEmu,
+ out string errorMessage)
+ {
+ widthEmu = 0;
+ heightEmu = 0;
+ errorMessage = string.Empty;
+
+ if (!TryGetPixelSize(imageBytes, out var pixelWidth, out var pixelHeight, out errorMessage))
+ {
+ return false;
+ }
+
+ var actualWidthEmu = pixelWidth * EmusPerPixel;
+ var actualHeightEmu = pixelHeight * EmusPerPixel;
+
+ if (!TryParseLengthToEmu(widthAttr, out var widthOverrideEmu, out errorMessage))
+ {
+ return false;
+ }
+
+ if (!TryParseLengthToEmu(heightAttr, out var heightOverrideEmu, out errorMessage))
+ {
+ return false;
+ }
+
+ if (!TryParseLengthToEmu(maxWidthAttr, out var maxWidthEmu, out errorMessage))
+ {
+ return false;
+ }
+
+ if (!TryParseLengthToEmu(maxHeightAttr, out var maxHeightEmu, out errorMessage))
+ {
+ return false;
+ }
+
+ widthEmu = actualWidthEmu;
+ heightEmu = actualHeightEmu;
+
+ if (widthOverrideEmu.HasValue && heightOverrideEmu.HasValue)
+ {
+ widthEmu = widthOverrideEmu.Value;
+ heightEmu = heightOverrideEmu.Value;
+ }
+ else if (widthOverrideEmu.HasValue)
+ {
+ widthEmu = widthOverrideEmu.Value;
+ heightEmu = widthOverrideEmu.Value * actualHeightEmu / actualWidthEmu;
+ }
+ else if (heightOverrideEmu.HasValue)
+ {
+ heightEmu = heightOverrideEmu.Value;
+ widthEmu = heightOverrideEmu.Value * actualWidthEmu / actualHeightEmu;
+ }
+
+ if (maxWidthEmu.HasValue && widthEmu > maxWidthEmu.Value)
+ {
+ var scale = maxWidthEmu.Value / widthEmu;
+ widthEmu = maxWidthEmu.Value;
+ heightEmu *= scale;
+ }
+
+ if (maxHeightEmu.HasValue && heightEmu > maxHeightEmu.Value)
+ {
+ var scale = maxHeightEmu.Value / heightEmu;
+ heightEmu = maxHeightEmu.Value;
+ widthEmu *= scale;
+ }
+
+ if (widthEmu <= 0 || heightEmu <= 0)
+ {
+ errorMessage = "Image: Calculated dimensions are invalid.";
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Gets pixel dimensions of an image, trying SkiaSharp first and falling back to header inspection.
+ ///
+ internal static bool TryGetPixelSize(byte[] imageBytes, out int width, out int height, out string errorMessage)
+ {
+ width = 0;
+ height = 0;
+ errorMessage = string.Empty;
+
+ try
+ {
+ using var bitmap = SKBitmap.Decode(imageBytes);
+ if (bitmap != null && bitmap.Width > 0 && bitmap.Height > 0)
+ {
+ width = bitmap.Width;
+ height = bitmap.Height;
+ return true;
+ }
+ }
+ catch
+ {
+ // ignore and fall back to header-based detection
+ }
+
+ if (TryReadPngDimensions(imageBytes, out width, out height))
+ {
+ return true;
+ }
+
+ if (TryReadJpegDimensions(imageBytes, out width, out height))
+ {
+ return true;
+ }
+
+ if (TryReadGifDimensions(imageBytes, out width, out height))
+ {
+ return true;
+ }
+
+ errorMessage = "Image: Unable to determine image dimensions.";
+ return false;
+ }
+
+ ///
+ /// Reads PNG image dimensions from the file header.
+ ///
+ private static bool TryReadPngDimensions(byte[] bytes, out int width, out int height)
+ {
+ width = 0;
+ height = 0;
+ if (bytes.Length < 24)
+ {
+ return false;
+ }
+
+ var signature = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 };
+ for (var i = 0; i < signature.Length; i++)
+ {
+ if (bytes[i] != signature[i])
+ {
+ return false;
+ }
+ }
+
+ if (bytes[12] != 0x49 || bytes[13] != 0x48 || bytes[14] != 0x44 || bytes[15] != 0x52)
+ {
+ return false;
+ }
+
+ width = (bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19];
+ height = (bytes[20] << 24) | (bytes[21] << 16) | (bytes[22] << 8) | bytes[23];
+ return width > 0 && height > 0;
+ }
+
+ ///
+ /// Reads JPEG image dimensions from the file header.
+ ///
+ private static bool TryReadJpegDimensions(byte[] bytes, out int width, out int height)
+ {
+ width = 0;
+ height = 0;
+ if (bytes.Length < 4 || bytes[0] != 0xFF || bytes[1] != 0xD8)
+ {
+ return false;
+ }
+
+ var index = 2;
+ while (index + 9 < bytes.Length)
+ {
+ if (bytes[index] != 0xFF)
+ {
+ index++;
+ continue;
+ }
+
+ var marker = bytes[index + 1];
+ index += 2;
+
+ if (marker == 0xD8 || marker == 0xD9)
+ {
+ continue;
+ }
+
+ if (index + 2 > bytes.Length)
+ {
+ break;
+ }
+
+ var length = (bytes[index] << 8) + bytes[index + 1];
+ if (length < 2 || index + length > bytes.Length)
+ {
+ break;
+ }
+
+ if (marker >= 0xC0 && marker <= 0xC3)
+ {
+ height = (bytes[index + 3] << 8) + bytes[index + 4];
+ width = (bytes[index + 5] << 8) + bytes[index + 6];
+ return width > 0 && height > 0;
+ }
+
+ index += length;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Reads GIF image dimensions from the file header.
+ ///
+ private static bool TryReadGifDimensions(byte[] bytes, out int width, out int height)
+ {
+ width = 0;
+ height = 0;
+ if (bytes.Length < 10 || bytes[0] != 'G' || bytes[1] != 'I' || bytes[2] != 'F')
+ {
+ return false;
+ }
+
+ width = bytes[6] | (bytes[7] << 8);
+ height = bytes[8] | (bytes[9] << 8);
+ return width > 0 && height > 0;
+ }
+ }
+}
diff --git a/OpenXmlPowerTools/DocumentAssembler/PA.cs b/OpenXmlPowerTools/DocumentAssembler/PA.cs
index a4482288..4271a402 100644
--- a/OpenXmlPowerTools/DocumentAssembler/PA.cs
+++ b/OpenXmlPowerTools/DocumentAssembler/PA.cs
@@ -7,6 +7,7 @@ public partial class DocumentAssembler
private static class PA
{
public static readonly XName Content = "Content";
+ public static readonly XName Image = "Image";
public static readonly XName Table = "Table";
public static readonly XName Repeat = "Repeat";
public static readonly XName EndRepeat = "EndRepeat";
@@ -14,9 +15,14 @@ private static class PA
public static readonly XName EndConditional = "EndConditional";
public static readonly XName Select = "Select";
public static readonly XName Optional = "Optional";
+ public static readonly XName Align = "Align";
+ public static readonly XName Width = "Width";
+ public static readonly XName Height = "Height";
+ public static readonly XName MaxWidth = "MaxWidth";
+ public static readonly XName MaxHeight = "MaxHeight";
public static readonly XName Match = "Match";
public static readonly XName NotMatch = "NotMatch";
public static readonly XName Depth = "Depth";
}
}
-}
\ No newline at end of file
+}
diff --git a/OpenXmlPowerTools/HtmlToWmlConverterCore.cs b/OpenXmlPowerTools/HtmlToWmlConverterCore.cs
index 02542519..f2dc92a6 100644
--- a/OpenXmlPowerTools/HtmlToWmlConverterCore.cs
+++ b/OpenXmlPowerTools/HtmlToWmlConverterCore.cs
@@ -3021,8 +3021,18 @@ private static XElement GetRunProperties(XText textNode, HtmlToWmlConverterSetti
XElement? szCs = null;
if (fontSizeTPoint != null)
{
- sz = new XElement(W.sz, new XAttribute(W.val, (int)((double)fontSizeTPoint * 2)));
- szCs = new XElement(W.szCs, new XAttribute(W.val, (int)((double)fontSizeTPoint * 2)));
+ var sizeInPoints = (double)fontSizeTPoint;
+ if (!double.IsNaN(sizeInPoints) && !double.IsInfinity(sizeInPoints))
+ {
+ var halfPoints = sizeInPoints * 2.0;
+ if (halfPoints >= 1 && halfPoints <= 1638)
+ {
+ var roundedHalfPoints = (int)Math.Round(halfPoints, MidpointRounding.AwayFromZero);
+ roundedHalfPoints = Math.Max(1, Math.Min(roundedHalfPoints, 1638));
+ sz = new XElement(W.sz, new XAttribute(W.val, roundedHalfPoints));
+ szCs = new XElement(W.szCs, new XAttribute(W.val, roundedHalfPoints));
+ }
+ }
}
XElement? strike = null;
@@ -5517,4 +5527,4 @@ public static void UpdateThemePart(WordprocessingDocument wDoc, XElement html)
wDoc.MainDocumentPart.ThemePart.PutXDocument();
}
}
-}
\ No newline at end of file
+}
diff --git a/OpenXmlPowerToolsExamples/DocumentAssembler04/DocumentAssembler04.cs b/OpenXmlPowerToolsExamples/DocumentAssembler04/DocumentAssembler04.cs
new file mode 100644
index 00000000..431d637b
--- /dev/null
+++ b/OpenXmlPowerToolsExamples/DocumentAssembler04/DocumentAssembler04.cs
@@ -0,0 +1,123 @@
+using Codeuctivity.OpenXmlPowerTools;
+using SkiaSharp;
+using System;
+using System.IO;
+using System.Xml.Linq;
+
+namespace DocumentAssembler04
+{
+ internal class Program
+ {
+ private static void Main()
+ {
+ var outputDir = CreateOutputDirectory();
+
+ var templatePath = LocateTemplate();
+ if (!File.Exists(templatePath))
+ {
+ Console.WriteLine($"TemplateDocument.docx not found at {templatePath}");
+ return;
+ }
+
+ var data = LoadOrCreateSampleData();
+
+ var templateDocument = new WmlDocument(templatePath);
+ var assembled = DocumentAssembler.AssembleDocument(templateDocument, data, out var templateError);
+ if (templateError)
+ {
+ Console.WriteLine("The template produced validation errors. Inspect the generated document for highlighted issues.");
+ }
+
+ var outputDocx = Path.Combine(outputDir.FullName, "DocumentWithImage.docx");
+ assembled.SaveAs(outputDocx);
+ Console.WriteLine($"Generated document: {outputDocx}");
+ }
+
+ private static DirectoryInfo CreateOutputDirectory()
+ {
+ var now = DateTime.Now;
+ var dirName = $"ExampleOutput-{now.Year - 2000:00}-{now.Month:00}-{now.Day:00}-{now.Hour:00}{now.Minute:00}{now.Second:00}";
+ var directory = new DirectoryInfo(dirName);
+ if (!directory.Exists)
+ {
+ directory.Create();
+ }
+ return directory;
+ }
+
+ private static string LocateTemplate()
+ {
+ var baseDir = AppContext.BaseDirectory;
+ var templateInOutput = Path.Combine(baseDir, "TemplateDocument.docx");
+ if (File.Exists(templateInOutput))
+ {
+ return templateInOutput;
+ }
+
+ // fallback to project directory (useful when running from source)
+ return Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "TemplateDocument.docx");
+ }
+
+ private static XElement LoadOrCreateSampleData()
+ {
+ var samplePath = Path.Combine(AppContext.BaseDirectory, "SampleData.xml");
+ if (File.Exists(samplePath))
+ {
+ return XElement.Load(samplePath);
+ }
+
+ var samples = new XElement("Images",
+ new XElement("Image", GenerateImageBase64(400, 200, SKColors.SteelBlue, "IMG1")),
+ new XElement("Image", GenerateImageBase64(800, 400, SKColors.Teal, "IMG2")),
+ new XElement("Image", GenerateImageBase64(320, 640, SKColors.Purple, "IMG3")),
+ new XElement("Image", GenerateImageBase64(600, 160, SKColors.IndianRed, "IMG4"))
+ );
+ return samples;
+ }
+
+ private static string GenerateImageBase64(int width, int height, SKColor background, string label)
+ {
+ using var surface = SKSurface.Create(new SKImageInfo(width, height));
+ var canvas = surface.Canvas;
+ canvas.Clear(background);
+
+ using (var paint = new SKPaint())
+ {
+ paint.IsAntialias = true;
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(0, 0),
+ new SKPoint(width, height),
+ new[] { background, background.WithAlpha(200), SKColors.White },
+ null,
+ SKShaderTileMode.Clamp);
+ canvas.DrawRect(new SKRect(0, 0, width, height), paint);
+ }
+
+ using (var borderPaint = new SKPaint { Color = SKColors.White, StrokeWidth = Math.Max(width, height) / 60f, IsStroke = true, IsAntialias = true })
+ {
+ canvas.DrawRect(new SKRect(borderPaint.StrokeWidth, borderPaint.StrokeWidth, width - borderPaint.StrokeWidth, height - borderPaint.StrokeWidth), borderPaint);
+ }
+
+ using (var circlePaint = new SKPaint { Color = SKColors.OrangeRed, IsAntialias = true })
+ {
+ canvas.DrawCircle(width / 2f, height / 2f, Math.Min(width, height) / 4f, circlePaint);
+ }
+
+ using var font = new SKFont { Size = Math.Min(width, height) / 5f, Edging = SKFontEdging.Antialias };
+ using (var textPaint = new SKPaint
+ {
+ Color = SKColors.White,
+ IsAntialias = true
+ })
+ {
+ canvas.DrawText(label, width / 2f, (height / 2f) + (font.Size / 3f), SKTextAlign.Center, font, textPaint);
+ }
+
+ canvas.Flush();
+
+ using var image = surface.Snapshot();
+ using var data = image.Encode(SKEncodedImageFormat.Png, 100);
+ return Convert.ToBase64String(data.ToArray());
+ }
+ }
+}
diff --git a/OpenXmlPowerToolsExamples/DocumentAssembler04/DocumentAssembler04.csproj b/OpenXmlPowerToolsExamples/DocumentAssembler04/DocumentAssembler04.csproj
new file mode 100644
index 00000000..6ed993fb
--- /dev/null
+++ b/OpenXmlPowerToolsExamples/DocumentAssembler04/DocumentAssembler04.csproj
@@ -0,0 +1,21 @@
+
+
+ Exe
+ net8.0;net10.0
+ 8.0
+ true
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
diff --git a/OpenXmlPowerToolsExamples/DocumentAssembler04/SampleData.xml b/OpenXmlPowerToolsExamples/DocumentAssembler04/SampleData.xml
new file mode 100644
index 00000000..2c5a482b
--- /dev/null
+++ b/OpenXmlPowerToolsExamples/DocumentAssembler04/SampleData.xml
@@ -0,0 +1,6 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAZAAAADICAIAAABJdyC1AAAFVklEQVR4nO3aX2iVdRzH8d/Z5jaYZZMC0ctGCBUU6BCSGGL+gXlhUBlZrYmgXRhdaBtdzF2ZdedAwZuspAuFgnURqOChNkHrxj91E0ERXSgUCs4/c/NELPpjpa7W1kder6vnnPOcH99z8+b3PM+plLK9ACSom+kBAG6XYAExBAuIIVhADMECYggWEEOwgBiCBcQQLCBGw80/rtX6pmsSgJ9VKv3lb9hhATEEC4ghWEAMwQJiCBZwpzwlnNQNfIDJmtRfEeywgBiCBcQQLCCGYAExBAuIIVhADMECYggWEEOwgBiCBcQQLCCGYAExBAuIIVhADMECYggWEEOwgBiCBcQQLCCGYAExBAuIIVhADMECYggWEEOwgBiCBcQQLCCGYAExBAuIIVhADMECYggWEEOwgBiCBcQQLCCGYAExBAuIIVhADMECYggWEEOwgBiCBcQQLCCGYAExBAuIIVhADMECYggWEEOwgBiCBcQQLCCGYAExBAuIIVhADMECYggWEEOwgBiCBcQQLCCGYAExBAuIIVhADMECYggWEEOwgBiCBcQQLCCGYAExBAuIIVhADMECYggWEEOwgBiCBcQQrDvWwoX3vvbaY3PmNM30IDBlBCvPpUuvV6tdR4++ODzc3dX1yN+d9uGHz1y+PDY2dv121jx/vmfioKdn6dRNClOsYaoX5D83Ojre0bGvlNLSMmtw8NmRkdGDB7/882nz5s3etev4ZBfv6Vn6xhtDUzQpTDE7rGAjI9e2bTv8yitLWlub9+9/8siRFz755KX29gWllM2bF911V1O12rV48fyhoe4zZ15+9dUlN2ymbjgupfT3d8ye3Xjo0PPT/lPgtghWtlOnzra1zX3rrRUDA8eXL393/foP9u5dU0rZs+fzixdHOzr2dXc/2tt75PHH39669bFbrtbXV714cXTFivemZXaYNJeE2Roa6q5dG1+58v62trkT77S0zKqvr4yP1yZebtt2eN26hzo7H7j77r+4+15XV5neeeFfscPK1t6+4PTpcw0NdatW7e/o2Lds2TsbNgz+WqtSysGDT5dSBgZOXL9euyFS99zT3NhYP0ODwz8hWMFaW5t37nzizTeHh4e/W7t2YSll9eq23t4/POZbtGj+gQNfNDc3NDX90qYLF648+OB9pZTnnnu4VvstbRPq6iq2XfxvuSTM09hYX6121Wq1WbPqd+4cqla/+frrH/fuXbNp06KxsesbN370+5N37/7s2LENJ0+ePX/+SlNT/dWr41u2fHzgwFPnzo2cOPH91avjE6d99dUPvb1Ld+wY+vTTbwcHn+3sfH+GfhzcTKWU7Tf5uFbru/ELlf6bLggwCZOKjEtCIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWEAMwQJiCBYQQ7CAGIIFxBAsIIZgATEEC4ghWECMhsl+oVbr+28mAbgFOywghmABMQQLiCFYQAzBAmJUStk+0zMA3BY7LCCGYAExBAuIIVhADMECYggWEEOwgBiCBZQUPwF8CtMHqcxOEQAAAABJRU5ErkJggg==
+iVBORw0KGgoAAAANSUhEUgAAAyAAAAGQCAIAAADZR5NjAAAJr0lEQVR4nO3cX4hldQHA8XPnzs4ILWwqyD7Nw7qBkoKIgtiAIrSVoKxPNQ+SL6bSQwQ+bL3MDPQQ9JYVEkkD+lD4sKKL2YYwkD3MSOiD4hK6IMuiVKJoa+syf8KVInLHHPjuH4bP5+mee36/c895+97fOfeOhoWFAQCAzkR4LAAABBYAQE9gAQDEBBYAQExgAQDEBBYAQExgAQDEBBYAQExgAQDEJj979+b8fP2JAAA7wWhxcatdVrAAAGICCwAgJrAAAGICCwAgJrAAAC7srwi39cA8AMBOta2/VrCCBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQE1gAADGBBQAQm6wPCOwo999440M33/zBRx/948yZB48cOfH++59/7qHZ2R+/8ML5PDuAS5QVLGBLX923b+7667/y2GO3LS39bHV16eDBbU0/NDt73k4N4JJmBQvY0sO33vrD55//59raMAy/e/31e669dtfExO6pqUfuvHPv7t1T4/HDR4+unjw5DMN7hw79fHV1dmbmi5ddtrC8fPjYscXbb989NXX03nu/+eST5xx/+LXXXnr77Z+urFzsqwToCSxgS1++6qqX3nrrP5vfeeaZYRh+cuDAIysrKydPzuzZ8/Tc3A2PPjoMw9R4/PcPP7xtaWnf5Zcv33ff4WPH5peXv3fLLQcef/xXd9/96fHT4/FvXnnl92+8cVGvD+B8EVjAlsaj0aff/NrVV++/4opPXn9h167xaLS+uTkxGv365ZeHYTj+7rt7pqf/7/j1zc0/HD9+QS4C4CIQWMCW/vLOOzfs3bty9qbeaBiWDh789lNPTU5MfP2JJ06vrU2MRrMzM+ubm8MwnFlff+/06U9mfbz9X845fm1jY+PsC4AdyUPuwJZ+8eKLP7rjjunxeBiGb1133fTkx1/J/nTixD3XXDMMwzf27//Bvx9jP2ctTYxGE6PROccD7GxWsIAt/fbVV7905ZV/fuCBv5069ddTp7777LPDMHz/ued+edddD95009rGxv1nn8rayh/ffPPpubmHjhz5nOMBdozRsLDwGbs35+f/d8Li4nk+JQCAS862osgtQgCAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACAIhNbnfC5vx8fQ4AADuKFSwAgJjAAgCICSwAgJjAAgCICSwAgAv7K8LR4mL9iQAAO5wVLACAmMACAIgJLACAmMACAIgJLACAmMACAIgJLACAmMACABha/wIkpugFaVq0kwAAAABJRU5ErkJggg==
+iVBORw0KGgoAAAANSUhEUgAAAUAAAAKACAIAAABi8jbUAAAJlUlEQVR4nO3ZTailcxzA8efcO+4sEAtMTKJYTIkkpmRws5jCQtOsZqHOlNLIymKasrjuQs3Cxkss79REUTYTCqXxkrIgMZIpSRbyMmXB0NxmRtNwkre6NPfcL5/P6jn/8zyn3+bb/3meM3poeGgAmmamPQDwzwkYwgQMYQKGMAFDmIAhTMAQJmAIEzCErfv7rxdOLqzWJMCfWxwt/sU3dmAoEzCECRjCBAxhAob/7lvoFb0QA/69Ff31YweGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBLx6Hjz64PjgePz6+N737r3s1ssuvOrC63dd/1cn7/luzx8Xt+zZcoZnJGbdtAf4Hzl+7Pi++X3DMFx09UXbn97+1DVPffPRNyv6hS17try1960zNiA9duAp+PrQ1+duPHeyzZ694ewdB3bsfHPntv3bdn+7e3LabQ/fNn59vOvDXZu2bRqGYX5xfu6cubtfuXuqs7O2CHgKrth6xWevfTb5uPWRrYeePbR089LHz388d87c6cXZ9bNHvz2679Z9z21/7vZHbx+G4eDCwWPfH9u/df/0BmfNcQu9embnZscHxzNnzVyw6YInr3pysn75/OUH7jkwDMPhFw6fOH7i9OJoNHp/6f1hGI4cPrL+vPXTm5o1TcBTeAa+afdN146vnTzNzs7Nnj4YzYxGo9Hk5J++++mXK09OZ2DWPrfQU/Dpq59u3Lxx8vGLt7/YdNepp9xTz7q/9DucPPEn1Z4qfObXM0DAU3HkkyMbrtkwSfHlB17efP/mnW/svPi6i5d/WP6bCz9/8/MdB3as1pgEuIVePXvP33v6YPno8mNXPjZZmV+Yf+n+l7764KtLbrjk0hsv/d3Jvz1+5s5npjE4a5eAp++dx9+544k7ln9cnp2bffG+F6c9DiUCnr4v3/1y6ZalaU9BkmdgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUPYupVesHBy4cxMAqyYHRjCBAxhAoYwAUOYgOG/+xZ6cbS4WpMAK2YHhjABQ5iAIUzAECZgCBMwhAkYwgQMQ9fPUGilmngaqyQAAAAASUVORK5CYII=
+iVBORw0KGgoAAAANSUhEUgAAAlgAAACgCAIAAABfWoXWAAAE6klEQVR4nO3bTYhVZRzH8edO4hTojGWCIAOChgNDraxNYS5kQpHRFlG+MkFgs9CF4AuIjIN7xTaznDBsUEllECWD8GWhifiCWtisQgKxjRMjppk3YsBF2ujA3Lnh7/NZXc5z7uF/4MKX53BuZUcBgFwN9R4AAOpJCAGIJoQARBNCAKIJIQDRhBCAaEIIQDQhBCCaEAIQbdLoy93V6kRNAgC10lOp/NeSHSEA0YQQgGhCCEA0IQQgmhACEO0Zb42O6cUbAPg/GNNfHuwIAYgmhABEE0IAogkhANGEEIBoQghANCEEIJoQAhBNCAGIJoQARBNCAKIJIQDRhBCAaEIIQDQhBCCaEAIQTQgBiCaEAEQTQgCiCSEA0YQQgGhCCEA0IQQgmhACEE0IAYgmhABEE0IAogkhANGEEIBoQghANCEEIJoQAhBNCAGIJoQARBNCAKIJIQDRhBCAaEIIQDQhBCCaEAIQTQgBiCaEAEQTQgCiCSEA0YQQgGhCCEA0IQQgmhACEE0IAYgmhABEE0IAogkhANGEEIBoQghANCGEZ3i9tfXdLVsam5vrPQhQE0JIhK137jznme9t3VpKmdHWNr+ra+TIx4cPP7x379HDh7UcEKgbIYSnhPC369cv9PaOHJkyc+YPX3zx59279R4NqAkhJHdrOPL5nfXr1126tO7ixTnt7Qt7eiZPmbLmxInHq/O7uhqnTu08efLzK1demzu3lNLY1LR+cLBUKnW9D2DcCCHp3t++vW/Bgm9Wrnxr9eqT3d0Phoe/am9/vHqht/fB8PCXCxde3bevdfnyUsrcxYt/OnSoVKt1nRoYN0JIrkrDP7//wWPHPty7t6ml5fDataOcfLW/f15HRymlddmya/39EzgmUFtCyIvs09Onnxq/UsrL06a9NHlyKeVIZ+e53bvf7upa1tc3yqV+v3mz+ujR1Fmzps2efevy5VpODUwoIeRF9sr06U0tLa/OmTN869bIkT+Ghma0tZVS3ly1qlqtNjY3d546dfPs2UNr1ryxZMlIKR/H8l+u7d//wa5dg8ePT+xNALU1qcbXh3r6bvPmT44cKdXqtxs3jhw5vmHDRwcO3L19+9fz5/+6f//+0NDPR49+du5cpaHh9M6dpZRfzpxZMTDw9dKlT17tx4MHF+/Z8/22bRN+H0ANVXaMutz9xBsBPV6WI1VTS8vyvr69ixbVexDgGcYUL49G4bnM6+hYMTBwYtOmeg8CjDOPRuG53BgYuDEwUO8pgPFnRwhANCEEIJoQAhBNCAGIJoQARBNCAKIJIQDRhBCAaEIIQDQhBCCaEAIQTQgBiCaEAEQTQgCiCSEA0YQQgGhCCEA0IQQgmhACEE0IAYgmhABEE0IAogkhANGEEIBoQghANCEEIJoQAhBNCAGIJoQARBNCAKIJIQDRhBCAaEIIQDQhBCCaEAIQTQgBiCaEAEQTQgCiCSEA0YQQgGhCCEA0IQQgmhACEE0IAYgmhABEE0IAogkhANGEEIBoQghANCEEIJoQAhBNCAGIJoQARBNCAKIJIQDRhBCAaEIIQDQhBCCaEAIQTQgBiCaEAEQTQgCiTRrrF7qr1dpMAgB1YEcIQDQhBCCaEAIQTQgBiCaEAESr7Kj3BABQR3aEAEQTQgCiCSEA0YQQgGhCCEA0IQQgmhACEE0IASjJ/gZ66b8kd26y0gAAAABJRU5ErkJggg==
+
\ No newline at end of file
diff --git a/OpenXmlPowerToolsExamples/DocumentAssembler04/TemplateDocument.docx b/OpenXmlPowerToolsExamples/DocumentAssembler04/TemplateDocument.docx
new file mode 100644
index 0000000000000000000000000000000000000000..e099f010eb8b2213e1aa11699800e36c72fd4c5d
GIT binary patch
literal 2535
zcmcIm&2G~`5Kak2qMQ&ta6%(js)L|$5?ZLzI91hx2&E9UMI5TC+S+Spi~qE{Nz-!a
z8}J&u0*}C*3p@aCz=0dHYv-qF+M-Ia<)0bv&NuVT&a9i8`P;KOb$tFp_n`bX{5qA(
zsp|pi_SWM_(nucco+MQ47rS9lTg4l6C>Jb_Y_nV}nLr~ac3I@x=7-%^3(MweeWs>o
z0FI4>ZMLLLs+J|377d9g#tDt^ix=~dNEEqmCB*qed}>unrRSEDqAo~U(5_GnYb>aQTje=!|t7bJucXryU}Nin}W;jqx`2Jm%2;d}S@+u|tJGs>7hz{}K`w
z^`fX*n)v!m^IT&8{`qzE2$QO=dzjb*MV_UI5dWLd?P1bxnAmf~r->al3k}ycdBZK6
zkmmFHb(4~`k&&D(pK~bI`4MQDaGy%syqFza1T|~eA2CW}<45E7_vu_tUH6g33pTDM
zvn{=>IwDc3_VSl62$jTA4vz0jr{|3amtG
z8wsa^@(wlXz49&)N47}-3h*fe)RT{^Fcxnln(=1m1GzE>>fX?y?a+Wa(l)fTSKc?k
zn)l@j%&GfWm{fVwu-38gkyq?zLUK$Ib)m3Dx*wT~)IMX;1cR$!aBC`gw&7?m4Co=-
z20~9d^lj$1B(5dZkeSp8g+hfz(1mhwX`-P&8-&i47Y1m;Z3E#AcukegHnbO$X5md+
zNaiujdq}#_($G*SFO`z+1arSQAmB(laH5mnk6gjuByxep)6D9<*IWFZQ&8>T7#Xcp
z7E6YfS|}|oqog<1K_~LJ2-gE-oZyhf-gup-VZ~uAWgHG`R5KWsTa>tz+s2DZhUvvI
z7Tb`*Z%eJ{dyy9NAc|B@yJh;`>&f-z=G3j({N;Z}*!=Zzh8ybG|D;g^qdRaqJ(oM1
k`iUln_T$lsqkGO^BIL*HK+VnRJ8DQC&u{pAcvngP0Fv+ua{vGU
literal 0
HcmV?d00001
From 831bc507443c41ed1e2782b2cd8328ebcd014873 Mon Sep 17 00:00:00 2001
From: Stefan Seeland <168659+stesee@users.noreply.github.com>
Date: Tue, 11 Nov 2025 00:58:47 +0100
Subject: [PATCH 3/4] Update OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs b/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
index 35637243..9a430444 100644
--- a/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
+++ b/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
@@ -91,7 +91,13 @@ internal static int GetNextDocPrId(OpenXmlPart part)
var existingIds = part
.GetXDocument()
.Descendants(WP.docPr)
- .Select(dp => (int?)dp.Attribute("id") ?? 0);
+ .Select(dp =>
+ {
+ var idAttr = dp.Attribute("id");
+ if (idAttr != null && int.TryParse(idAttr.Value, out int id))
+ return id;
+ return 0;
+ });
var maxId = existingIds.Any() ? existingIds.Max() : 0;
tracker = new ImageIdTracker { NextId = maxId + 1 };
part.AddAnnotation(tracker);
From d4acce4c0776a5fa33d51932cc9ae0bde09169e8 Mon Sep 17 00:00:00 2001
From: Stefan Seeland <168659+stesee@users.noreply.github.com>
Date: Sat, 15 Nov 2025 21:02:52 +0100
Subject: [PATCH 4/4] Remove header-based image dimension detection fallback
Replaced custom PNG, JPEG, and GIF header-based dimension detection with exception handling. Now, if image dimensions cannot be determined via SkiaSharp, the error message is set from the exception, simplifying the code and error reporting.
---
.../DocumentAssembler/ImageHelper.cs | 122 +-----------------
1 file changed, 4 insertions(+), 118 deletions(-)
diff --git a/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs b/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
index 9a430444..dfd5f751 100644
--- a/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
+++ b/OpenXmlPowerTools/DocumentAssembler/ImageHelper.cs
@@ -1,6 +1,7 @@
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using SkiaSharp;
+using System;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
@@ -362,129 +363,14 @@ internal static bool TryGetPixelSize(byte[] imageBytes, out int width, out int h
return true;
}
}
- catch
- {
- // ignore and fall back to header-based detection
- }
-
- if (TryReadPngDimensions(imageBytes, out width, out height))
- {
- return true;
- }
-
- if (TryReadJpegDimensions(imageBytes, out width, out height))
- {
- return true;
- }
-
- if (TryReadGifDimensions(imageBytes, out width, out height))
- {
- return true;
- }
-
- errorMessage = "Image: Unable to determine image dimensions.";
- return false;
- }
-
- ///
- /// Reads PNG image dimensions from the file header.
- ///
- private static bool TryReadPngDimensions(byte[] bytes, out int width, out int height)
- {
- width = 0;
- height = 0;
- if (bytes.Length < 24)
- {
- return false;
- }
-
- var signature = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 };
- for (var i = 0; i < signature.Length; i++)
- {
- if (bytes[i] != signature[i])
- {
- return false;
- }
- }
-
- if (bytes[12] != 0x49 || bytes[13] != 0x48 || bytes[14] != 0x44 || bytes[15] != 0x52)
- {
- return false;
- }
-
- width = (bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19];
- height = (bytes[20] << 24) | (bytes[21] << 16) | (bytes[22] << 8) | bytes[23];
- return width > 0 && height > 0;
- }
-
- ///
- /// Reads JPEG image dimensions from the file header.
- ///
- private static bool TryReadJpegDimensions(byte[] bytes, out int width, out int height)
- {
- width = 0;
- height = 0;
- if (bytes.Length < 4 || bytes[0] != 0xFF || bytes[1] != 0xD8)
+ catch (Exception excepiton)
{
+ errorMessage = excepiton.Message;
return false;
}
- var index = 2;
- while (index + 9 < bytes.Length)
- {
- if (bytes[index] != 0xFF)
- {
- index++;
- continue;
- }
-
- var marker = bytes[index + 1];
- index += 2;
-
- if (marker == 0xD8 || marker == 0xD9)
- {
- continue;
- }
-
- if (index + 2 > bytes.Length)
- {
- break;
- }
-
- var length = (bytes[index] << 8) + bytes[index + 1];
- if (length < 2 || index + length > bytes.Length)
- {
- break;
- }
-
- if (marker >= 0xC0 && marker <= 0xC3)
- {
- height = (bytes[index + 3] << 8) + bytes[index + 4];
- width = (bytes[index + 5] << 8) + bytes[index + 6];
- return width > 0 && height > 0;
- }
-
- index += length;
- }
-
+ errorMessage = "Image: Unable to determine image dimensions.";
return false;
}
-
- ///
- /// Reads GIF image dimensions from the file header.
- ///
- private static bool TryReadGifDimensions(byte[] bytes, out int width, out int height)
- {
- width = 0;
- height = 0;
- if (bytes.Length < 10 || bytes[0] != 'G' || bytes[1] != 'I' || bytes[2] != 'F')
- {
- return false;
- }
-
- width = bytes[6] | (bytes[7] << 8);
- height = bytes[8] | (bytes[9] << 8);
- return width > 0 && height > 0;
- }
}
}