Skip to content

Commit

Permalink
Merge pull request #10 from Amberg/workitems/ImageRotation
Browse files Browse the repository at this point in the history
Image formatter rotation feature
  • Loading branch information
Amberg authored Feb 26, 2024
2 parents 765bbf2 + 65b48e8 commit 4347270
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 58 deletions.
104 changes: 66 additions & 38 deletions DocxTemplater.Images/ImageFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Metadata;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using A = DocumentFormat.OpenXml.Drawing;
Expand All @@ -15,6 +16,10 @@ namespace DocxTemplater.Images
{
public class ImageFormatter : IFormatter
{
private sealed record ImageInfo(int PixelWidth, int PixelHeight, string ImagePartRelationId);
private readonly Dictionary<byte[], ImageInfo> m_imagePartRelIdCache = new();
private OpenXmlPartRootElement m_currentRoot;

public bool CanHandle(Type type, string prefix)
{
var prefixUpper = prefix.ToUpper();
Expand All @@ -36,47 +41,65 @@ public void ApplyFormat(FormatterContext context, Text target)
}
try
{
using var image = Image.Load(imageBytes);
var imagePartType = DetectPartTypeInfo(context.Placeholder, image.Metadata);
var root = target.GetRoot();
string imagePartRelId = null;
uint maxPropertyId = 0;
if (root is OpenXmlPartRootElement openXmlPartRootElement && openXmlPartRootElement.OpenXmlPart != null)
{
maxPropertyId = openXmlPartRootElement.OpenXmlPart.GetMaxDocPropertyId();
if (openXmlPartRootElement.OpenXmlPart is HeaderPart headerPart)
{
imagePartRelId = CreateImagePart(headerPart, imageBytes, imagePartType);
}
else if (openXmlPartRootElement.OpenXmlPart is FooterPart footerPart)
var maxPropertyId = openXmlPartRootElement.OpenXmlPart.GetMaxDocPropertyId();

if (!TryGetImageIdFromCache(imageBytes, openXmlPartRootElement, out var imageInfo))
{
imagePartRelId = CreateImagePart(footerPart, imageBytes, imagePartType);
using var image = Image.Load(imageBytes);
string imagePartRelId = null;
var imagePartType = DetectPartTypeInfo(context.Placeholder, image.Metadata);
if (openXmlPartRootElement.OpenXmlPart is HeaderPart headerPart)
{
imagePartRelId = CreateImagePart(headerPart, imageBytes, imagePartType);
}
else if (openXmlPartRootElement.OpenXmlPart is FooterPart footerPart)
{
imagePartRelId = CreateImagePart(footerPart, imageBytes, imagePartType);
}
else if (openXmlPartRootElement.OpenXmlPart is MainDocumentPart mainDocumentPart)
{
imagePartRelId = CreateImagePart(mainDocumentPart, imageBytes, imagePartType);
}
if (imagePartRelId == null)
{
throw new OpenXmlTemplateException("Could not find a valid image part");
}
imageInfo = new ImageInfo(image.Width, image.Height, imagePartRelId);
m_imagePartRelIdCache[imageBytes] = imageInfo;
}
else if (openXmlPartRootElement.OpenXmlPart is MainDocumentPart mainDocumentPart)

// case 1. Image ist the only child element of a <wps:wsp> (TextBox)
if (TryHandleImageInWordprocessingShape(target, imageInfo, context.Args.FirstOrDefault() ?? string.Empty, maxPropertyId))
{
imagePartRelId = CreateImagePart(mainDocumentPart, imageBytes, imagePartType);
return;
}
}

if (imagePartRelId == null)
{
throw new OpenXmlTemplateException("Could not find a valid image part");
AddInlineGraphicToRun(target, imageInfo, maxPropertyId);
}

// case 1. Image ist the only child element of a <wps:wsp> (TextBox)
if (TryHandleImageInWordprocessingShape(target, imagePartRelId, image, context.Args.FirstOrDefault() ?? string.Empty, maxPropertyId))
else
{
return;
throw new OpenXmlTemplateException("Could not find root to insert image");
}

AddInlineGraphicToRun(target, imagePartRelId, image, maxPropertyId);
}
catch (Exception e) when (e is InvalidImageContentException or UnknownImageFormatException)
{
throw new OpenXmlTemplateException("Could not detect image format", e);
}
}

private bool TryGetImageIdFromCache(byte[] imageBytes, OpenXmlPartRootElement root, out ImageInfo imageInfo)
{
if (m_currentRoot != root)
{
m_imagePartRelIdCache.Clear();
m_currentRoot = root;
}
return m_imagePartRelIdCache.TryGetValue(imageBytes, out imageInfo);
}

private static PartTypeInfo DetectPartTypeInfo(string modelPath, ImageMetadata imageMetadata)
{
return imageMetadata switch
Expand All @@ -94,7 +117,7 @@ private static PartTypeInfo DetectPartTypeInfo(string modelPath, ImageMetadata i
/// If the image is contained in a "wsp" element (TextBox), the text box is used as a container for the image.
/// the size of the text box is adjusted to the size of the image.
/// </summary>
private static bool TryHandleImageInWordprocessingShape(Text target, string impagepartRelationShipId, Image image,
private static bool TryHandleImageInWordprocessingShape(Text target, ImageInfo imageInfo,
string firstArgument, uint maxPropertyId)
{
var drawing = target.GetFirstAncestor<Drawing>();
Expand All @@ -108,8 +131,8 @@ private static bool TryHandleImageInWordprocessingShape(Text target, string impa
if (targetExtent != null)
{
double scale = 0;
var imageCx = image.Width * 9525;
var imageCy = image.Height * 9525;
var imageCx = imageInfo.PixelWidth * 9525;
var imageCy = imageInfo.PixelHeight * 9525;
if (firstArgument.Equals("KEEPRATIO", StringComparison.CurrentCultureIgnoreCase))
{
scale = Math.Min(targetExtent.Cx / (double)imageCx, targetExtent.Cy / (double)imageCy);
Expand All @@ -129,7 +152,7 @@ private static bool TryHandleImageInWordprocessingShape(Text target, string impa
targetExtent.Cy = (long)(imageCy * scale);
}

ReplaceAnchorContentWithPicture(impagepartRelationShipId, maxPropertyId, drawing);
ReplaceAnchorContentWithPicture(imageInfo.ImagePartRelationId, maxPropertyId, drawing);
}

target.Remove();
Expand All @@ -144,7 +167,8 @@ private static void ReplaceAnchorContentWithPicture(string impagepartRelationShi
var inlineOrAnchor = (OpenXmlElement)original.GetFirstChild<DW.Anchor>() ??
(OpenXmlElement)original.GetFirstChild<DW.Inline>();
var originaleExtent = inlineOrAnchor.GetFirstChild<DW.Extent>();

var transform = inlineOrAnchor.Descendants<A.Transform2D>().FirstOrDefault();
int rotation = transform?.Rotation ?? 0;
var clonedInlineOrAnchor = inlineOrAnchor.CloneNode(false);

if (inlineOrAnchor is DW.Anchor anchor)
Expand Down Expand Up @@ -176,9 +200,9 @@ private static void ReplaceAnchorContentWithPicture(string impagepartRelationShi
});
}

#pragma warning disable IDE0300
clonedInlineOrAnchor.Append(new OpenXmlElement[]
{

new DW.DocProperties
{
Id = propertyId,
Expand All @@ -188,7 +212,7 @@ private static void ReplaceAnchorContentWithPicture(string impagepartRelationShi
new A.GraphicFrameLocks {NoChangeAspect = true}),
new A.Graphic(
new A.GraphicData(
CreatePicture(impagepartRelationShipId, propertyId, originaleExtent.Cx, originaleExtent.Cy)
CreatePicture(impagepartRelationShipId, propertyId, originaleExtent.Cx, originaleExtent.Cy, rotation)
)
{Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture"})
});
Expand All @@ -197,12 +221,11 @@ private static void ReplaceAnchorContentWithPicture(string impagepartRelationShi
original.Remove();
}

private static void AddInlineGraphicToRun(Text target, string impagepartRelationShipId, Image image,
uint maxDocumentPropertyId)
private static void AddInlineGraphicToRun(Text target, ImageInfo imageInfo, uint maxDocumentPropertyId)
{
var propertyId = maxDocumentPropertyId + 1;
var cx = image.Width * 9525;
var cy = image.Height * 9525;
var cx = imageInfo.PixelWidth * 9525;
var cy = imageInfo.PixelHeight * 9525;
// Define the reference of the image.
var drawing =
new Drawing(
Expand All @@ -224,7 +247,7 @@ private static void AddInlineGraphicToRun(Text target, string impagepartRelation
new A.GraphicFrameLocks { NoChangeAspect = true }),
new A.Graphic(
new A.GraphicData(
CreatePicture(impagepartRelationShipId, propertyId, cx, cy)
CreatePicture(imageInfo.ImagePartRelationId, propertyId, cx, cy, 0)
)
{ Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" })
)
Expand All @@ -239,7 +262,7 @@ private static void AddInlineGraphicToRun(Text target, string impagepartRelation
target.Remove();
}

private static PIC.Picture CreatePicture(string impagepartRelationShipId, uint propertyId, long cx, long cy)
private static PIC.Picture CreatePicture(string impagepartRelationShipId, uint propertyId, long cx, long cy, int rotation)
{
return new PIC.Picture(
new PIC.NonVisualPictureProperties(
Expand All @@ -265,11 +288,16 @@ private static PIC.Picture CreatePicture(string impagepartRelationShipId, uint p
new PIC.ShapeProperties(
new A.Transform2D(
new A.Offset { X = 0L, Y = 0L },
new A.Extents { Cx = cx, Cy = cy }),
new A.Extents { Cx = cx, Cy = cy })
{
Rotation = rotation
},
new A.PresetGeometry(
new A.AdjustValueList()
)
{ Preset = A.ShapeTypeValues.Rectangle }));
{
Preset = A.ShapeTypeValues.Rectangle
}));
}

private static string CreateImagePart<T>(T parent, byte[] imageBytes, PartTypeInfo partType)
Expand Down
50 changes: 50 additions & 0 deletions DocxTemplater.Test/DocxTemplateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,30 @@ public void ImplicitIterator()
Assert.That(body.InnerText, Is.EqualTo(" OuterValue0 InnerValue00 OuterValue0 OuterValue1 InnerValue10 OuterValue1 InnerValue11 OuterValue1 OuterValue2 InnerValue20 OuterValue2 InnerValue21 OuterValue2 "));
}

[TestCase("d", ExpectedResult = "2/22/2024")]
[TestCase("D", ExpectedResult = "Thursday, February 22, 2024")]
public string DateTimeFormatterTest(string format)
{

using var memStream = new MemoryStream();
using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document);
MainDocumentPart mainPart = wpDocument.AddMainDocumentPart();
mainPart.Document = new Document(new Body(new Paragraph(new Run(new Text($"{{{{.}}:f({format})}}")))));
wpDocument.Save();
memStream.Position = 0;

var docTemplate = new DocxTemplate(memStream, new ProcessSettings() { Culture = new CultureInfo("en-US") });
docTemplate.BindModel("ds", new DateTime(2024, 2, 22, 10, 51, 35));

var result = docTemplate.Process();
docTemplate.Validate();
Assert.IsNotNull(result);
// check result text
var document = WordprocessingDocument.Open(result, false);
var body = document.MainDocumentPart.Document.Body;
return body.InnerText;
}

[TestCase("<html><body><h1>Test</h1></body></html>", "<html><body><h1>Test</h1></body></html>")]
[TestCase("<body><h1>Test</h1></body>", "<html><body><h1>Test</h1></body></html>")]
[TestCase("<h1>Test</h1>", "<html><h1>Test</h1></html>")]
Expand Down Expand Up @@ -223,6 +247,32 @@ public void InsertHtmlInLoop()
Assert.That(altChunks.Count, Is.EqualTo(2));
}


[Test]
public void InsertTextWithNewline()
{
using var memStream = new MemoryStream();
using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document);
MainDocumentPart mainPart = wpDocument.AddMainDocumentPart();
mainPart.Document = new Document(new Body(new Paragraph(new Run(new Text("Start {{.}} End")))));
wpDocument.Save();
memStream.Position = 0;

var docTemplate = new DocxTemplate(memStream);
docTemplate.BindModel("ds", "FirstLine\r\nSecondLine\nThirdLine");
var result = docTemplate.Process();
docTemplate.Validate();
Assert.IsNotNull(result);
// check document contains newline
var document = WordprocessingDocument.Open(result, false);
var body = document.MainDocumentPart.Document.Body;
Assert.That(body.InnerXml, Is.EqualTo("<w:p xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"><w:r><w:t xml:space=\"preserve\">" +
"Start </w:t><w:t>FirstLine</w:t>" +
"<w:br /><w:t>SecondLine</w:t>" +
"<w:br /><w:t>ThirdLine</w:t>" +
"<w:br /><w:t xml:space=\"preserve\"> End</w:t></w:r></w:p>"));
}

[Test]
public void ConditionalBlockInLoop()
{
Expand Down
6 changes: 6 additions & 0 deletions DocxTemplater.Test/DocxTemplater.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
<None Update="Resources\MultipleRowsBoundToCollection.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\MultipleTableRowsBoundToCollection.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\NestedCollectionsInTable.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\testImage.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Loading

0 comments on commit 4347270

Please sign in to comment.