Exporting Document Fragments as Images

Hi,

This might be a bit of a weird scenario, but is it possible to export headers and footers as images using Aspose. The case being I would like to use a sample word document as a template and then using the header/footer in there, save them as an image in a temporary location so that they can be used as watermarks or image stamps later on.

I’ve been able to extract the footers as HTML strings but when I try and use SaveFormat.Png (or any other image file) I catch an exception telling me that saving the document fragment in this format isn’t supported.

Would anyone be able to shed some light on if this is possible or not?

Many thanks!

Hi there,

Thanks for your inquiry. There is no direct way to export Header/Footer of MS Word document to image. However, you can convert header/footer to image using following workaround. Hope this helps you.

Please let us know if you have any more queries.

Document doc = new Document(MyDir + "in.docx");
HeaderFooter hf = doc.FirstSection.HeadersFooters[HeaderFooterType.HeaderPrimary];
Image image = RenderNode(hf, new ImageSaveOptions(SaveFormat.Png));
image.Save(MyDir + "Out.png");
///
/// Renders any node in a document to the path specified using the image save options.
///
public static Image RenderNode(Node node, ImageSaveOptions imageOptions)
{
    // Run some argument checks.
    if (node == null)
        throw new ArgumentException("Node cannot be null");
    // If no image options are supplied, create default options.
    if (imageOptions == null)
        imageOptions = new ImageSaveOptions(SaveFormat.Png);
    // Store the paper color to be used on the final image and change to transparent.
    // This will cause any content around the rendered node to be removed later on.
    Color savePaperColor = imageOptions.PaperColor;
    imageOptions.PaperColor = Color.Transparent;
    // There a bug which affects the cache of a cloned node. To avoid this we instead clone the entire document including all nodes,
    // find the matching node in the cloned document and render that instead.
    Document doc = (Document)node.Document.Clone(true);
    node = doc.GetChild(NodeType.Any, node.Document.GetChildNodes(NodeType.Any, true).IndexOf(node), true);
    // Create a temporary shape to store the target node in. This shape will be rendered to retrieve
    // the rendered content of the node.
    Shape shape = new Shape(doc, ShapeType.TextBox);
    Section parentSection = (Section)node.GetAncestor(NodeType.Section);
    // Assume that the node cannot be larger than the page in size.
    shape.Width = parentSection.PageSetup.PageWidth;
    shape.Height = parentSection.PageSetup.PageHeight;
    shape.FillColor = Color.Transparent; // We must make the shape and paper color transparent.
                                         // Don't draw a surronding line on the shape.
    shape.Stroked = false;
    Node currentNode = node;
    // If the node contains block level nodes then just add a copy of these nodes to the shape.
    if (currentNode is InlineStory || currentNode is Story)
    {
        CompositeNode composite = (CompositeNode)currentNode;
        foreach (Node childNode in composite.ChildNodes)
        {
            shape.AppendChild(childNode.Clone(true));
        }
    }
    else
    {
        // Move up through the DOM until we find node which is suitable to insert into a Shape (a node with a parent can contain paragraph, tables the same as a shape).
        // Each parent node is cloned on the way up so even a descendant node passed to this method can be rendered.
        // Since we are working with the actual nodes of the document we need to clone the target node into the temporary shape.
        while (!(currentNode.ParentNode is InlineStory || currentNode.ParentNode is Story || currentNode.ParentNode is ShapeBase || currentNode.NodeType == NodeType.Paragraph))
        {
            CompositeNode parent = (CompositeNode)currentNode.ParentNode.Clone(false);
            currentNode = currentNode.ParentNode;
            parent.AppendChild(node.Clone(true));
            node = parent; // Store this new node to be inserted into the shape.
        }
        // Add the node to the shape.
        shape.AppendChild(node.Clone(true));
    }
    // We must add the shape to the document tree to have it rendered.
    parentSection.Body.FirstParagraph.AppendChild(shape);
    // Render the shape to stream so we can take advantage of the effects of the ImageSaveOptions class.
    // Retrieve the rendered image and remove the shape from the document.
    MemoryStream stream = new MemoryStream();
    shape.GetShapeRenderer().Save(stream, imageOptions);
    shape.Remove();
    Bitmap croppedImage;
    // Load the image into a new bitmap.
    using (Bitmap renderedImage = new Bitmap(stream))
    {
        // Extract the actual content of the image by cropping transparent space around
        // the rendered shape.
        Rectangle cropRectangle = FindBoundingBoxAroundNode(renderedImage);
        croppedImage = new Bitmap(cropRectangle.Width, cropRectangle.Height);
        croppedImage.SetResolution(imageOptions.Resolution, imageOptions.Resolution);
        // Create the final image with the proper background color.
        using (Graphics g = Graphics.FromImage(croppedImage))
        {
            g.Clear(savePaperColor);
            g.DrawImage(renderedImage, new Rectangle(0, 0, croppedImage.Width, croppedImage.Height), cropRectangle.X, cropRectangle.Y, cropRectangle.Width, cropRectangle.Height, GraphicsUnit.Pixel);
        }
    }
    return croppedImage;
}
///
/// Finds the minimum bounding box around non-transparent pixels in a Bitmap.
///
public static Rectangle FindBoundingBoxAroundNode(Bitmap originalBitmap)
{
    Point min = new Point(int.MaxValue, int.MaxValue);
    Point max = new Point(int.MinValue, int.MinValue);
    for (int x = 0; x < originalBitmap.Width; ++x)
    {
        for (int y = 0; y < originalBitmap.Height; ++y)
        {
            // Note that you can speed up this part of the algorithm by using LockBits and unsafe code instead of GetPixel.
            Color pixelColor = originalBitmap.GetPixel(x, y);
            // For each pixel that is not transparent calculate the bounding box around it.
            if (pixelColor.ToArgb() != Color.Empty.ToArgb())
            {
                min.X = Math.Min(x, min.X);
                min.Y = Math.Min(y, min.Y);
                max.X = Math.Max(x, max.X);
                max.Y = Math.Max(y, max.Y);
            }
        }
    }
    // Add one pixel to the width and height to avoid clipping.
    return new Rectangle(min.X, min.Y, (max.X - min.X) + 1, (max.Y - min.Y) + 1);
}

Hi Tahir,

That’s worked perfectly! Would never have come up with that on my own so thank you very much indeed, much appreciated!

Hi there,

Thanks for your feedback. Please feel free to ask if you have any question about Aspose.Words, we will be happy to help you.

Hi,

I am trying to using this same in the C# application. but looks like I am not able to get the image if you have multiple header and multiple footers.

Can someone please help me with that? also With the latest C# version the converting image is not woking too.

@m.darshan.shah The problem can cause because you have different sections or different headers. Following articles can be usefull:

We have 3 types of headers/footers for the first/odd/even pages.

@vyacheslav.deryushev : that is correct.
is there a way you can provide for the snapshot code to store all of them in seperate image just the headers and footer content.

that would be great. all three type of header and footer want to store.

@m.darshan.shah It’s not quite clear from your descriprion what you want to achieve. Do you need to add images to all headers/footers?

@vyacheslav.deryushev , I am trying to get headers and footers in image with separated images from the docx.

separated images meaning, just header and just footer image.
if you have two headers and footers, So I am expecting to get 4 images as output.

Can you please help me with that?

@m.darshan.shah You can use following code:

int i = 1;
foreach (Section section in doc.Sections)
{
    foreach (HeaderFooter hf in section.HeadersFooters)
    {
        Image image = RenderNode(hf, new ImageSaveOptions(SaveFormat.Png));
        image.Save($"output_{i}.png");
        i++;
    }
}

RenderNode is not working for me.
getting me error for Image and Bitmap, Graphics,

not sure how to fix them even though I have imported
using System.Drawing;

this whole par is not working for me.
getting an error for image:

“The type name ‘Image’ could not be found in the namespace ‘System.Drawing’. This type has been forwarded to assembly ‘System.Drawing.Common, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51’ Consider adding a reference to that assembly.”

for RenderNode
“The name ‘RenderNode’ does not exist in the current context”

since I am not able to use the RenderNode method.

@m.darshan.shah Could you please provide information about your environment?

dotnet sdk 6.0.0
I am on Mac

it has started giving me an error

This call site is reachable on all platforms. ‘Bitmap’ is only supported on: ‘windows’ 6.1 and later.

@m.darshan.shah You need to use SkiaSharp and SKBitmap on other platforms. I created a WORDSNET-27177 task to make this code cross-platform.

Hi @vyacheslav.deryushev , do you have any timeline when would I you be able to get back to me?


    public static SKBitmap RenderNode(Node node, ImageSaveOptions imageOptions)
    {
        // Run some argument checks.
        if (node == null)
        {
            throw new ArgumentException("Node cannot be null");
        }

        // If no image options are supplied, create default options.
        imageOptions ??= new ImageSaveOptions(SaveFormat.Png);

        // Store the paper color to be used on the final image and change to transparent.
        // This will cause any content around the rendered node to be removed later on.
        var savePaperColor = new SKColor((uint)imageOptions.PaperColor.ToArgb());
        imageOptions.PaperColor = Color.Transparent;

        // There a bug which affects the cache of a cloned node. To avoid this we instead clone the entire document including all nodes,
        // find the matching node in the cloned document and render that instead.
        var doc = (Document)node.Document.Clone(true);
        node = doc.GetChild(NodeType.Any, node.Document.GetChildNodes(NodeType.Any, true).IndexOf(node), true);

        // Create a temporary shape to store the target node in. This shape will be rendered to retrieve
        // the rendered content of the node.
        var shape = new Shape(doc, ShapeType.TextBox);
        var parentSection = (Section)node.GetAncestor(NodeType.Section);

        // Assume that the node cannot be larger than the page in size.
        shape.Width = parentSection.PageSetup.PageWidth;
        shape.Height = parentSection.PageSetup.PageHeight;
        shape.FillColor = Color.Transparent; // We must make the shape and paper color transparent.

        // Don't draw a surrounding line on the shape.
        shape.Stroked = false;
        var currentNode = node;

        // If the node contains block level nodes then just add a copy of these nodes to the shape.
        if (currentNode is InlineStory or Story)
        {
            var composite = (CompositeNode)currentNode;
            foreach (var childNode in composite.ChildNodes)
            {
                shape.AppendChild(childNode.Clone(true));
            }
        }
        else
        {
            // Move up through the DOM until we find a node which is suitable to insert into a Shape (a node with a parent can contain paragraph, tables the same as a shape).
            // Each parent node is cloned on the way up so even a descendant node passed to this method can be rendered.
            // Since we are working with the actual nodes of the document we need to clone the target node into the temporary shape.
            while (!(currentNode.ParentNode is InlineStory || currentNode.ParentNode is Story || currentNode.ParentNode is ShapeBase || currentNode.NodeType == NodeType.Paragraph))
            {
                var parent = (CompositeNode)currentNode.ParentNode.Clone(false);
                currentNode = currentNode.ParentNode;
                parent.AppendChild(node.Clone(true));
                node = parent; // Store this new node to be inserted into the shape.
            }

            // Add the node to the shape.
            shape.AppendChild(node.Clone(true));
        }

        // We must add the shape to the document tree to have it rendered.
        parentSection.Body.FirstParagraph.AppendChild(shape);

        // Render the shape to stream so we can take advantage of the effects of the ImageSaveOptions class.
        // Retrieve the rendered image and remove the shape from the document.
        using (var stream = new MemoryStream())
        {
            shape.GetShapeRenderer().Save(stream, imageOptions);
            shape.Remove();

            // Load the image into a new SKBitmap.
            stream.Seek(0, SeekOrigin.Begin);
            using (var renderedImage = SKBitmap.Decode(stream))
            {
                // Extract the actual content of the image by cropping transparent space around
                // the rendered shape.
                var cropRectangle = FindBoundingBoxAroundNode(renderedImage);
                using (var croppedImage = new SKBitmap(cropRectangle.Width, cropRectangle.Height))
                {
                    croppedImage.SetPixels(renderedImage.GetPixels());

                    // croppedImage.SetResolution(imageOptions.HorizontalResolution, imageOptions.VerticalResolution);
                    using (var canvas = new SKCanvas(croppedImage))
                    {
                        canvas.Clear(savePaperColor);
                        canvas.DrawBitmap(renderedImage, new SKRect(cropRectangle.Left, cropRectangle.Top, cropRectangle.Right, cropRectangle.Bottom), new SKRect(0, 0, croppedImage.Width, croppedImage.Height));
                    }

                    return croppedImage;
                }
            }
        }
    }

    public static SKRectI FindBoundingBoxAroundNode(SKBitmap originalBitmap)
    {
        ArgumentNullException.ThrowIfNull(originalBitmap);
        int minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;
        for (var x = 0; x < originalBitmap.Width; ++x)
        {
            for (var y = 0; y < originalBitmap.Height; ++y)
            {
                // Get the pixel color.
                var pixelColor = originalBitmap.GetPixel(x, y);

                // For each pixel that is not transparent calculate the bounding box around it.
                if (pixelColor.Alpha != 0)
                {
                    minX = Math.Min(x, minX);
                    minY = Math.Min(y, minY);
                    maxX = Math.Max(x, maxX);
                    maxY = Math.Max(y, maxY);
                }
            }
        }

        // Add one pixel to the width and height to avoid clipping.
        return new SKRectI(minX, minY, maxX + 1, maxY + 1);
    }

 public static void SaveBitmap(SKBitmap bitmap, Stream stream, float dpiX, float dpiY)
    {
        try
        {
            // Your code that throws the exception
            using (var image = SKImage.FromBitmap(bitmap))
            {
                using (var data = image.Encode(SKEncodedImageFormat.Png, 100))
                {
                    data.SaveTo(stream);
                }
            }
        }
        catch (Exception ex)
        {
            // Log or print exception details for debugging
            Console.WriteLine($"Exception: {ex.Message}");
            Console.WriteLine($"Stack Trace: {ex.StackTrace}");
            throw;
        }
    }

I have tried with this code but looks like it abruptly fail without any error on console.
var image = SKImage.FromBitmap(bitmap) here.

would be great if you can help.

@m.darshan.shah Here is the code:

Document doc = new Document("input.docx");
int i = 1;
foreach (Section section in doc.Sections)
{
    foreach (HeaderFooter hf in section.HeadersFooters)
    {
        RenderNode(hf, new ImageSaveOptions(SaveFormat.Png), $"output{i}.png");
        i++;
    }
}

///
/// Renders any node in a document to the path specified using the image save options.
///
public static void RenderNode(Node node, ImageSaveOptions imageOptions, string savePath)
{
    // Run some argument checks.
    if (node == null)
        throw new ArgumentException("Node cannot be null");
    // If no image options are supplied, create default options.
    if (imageOptions == null)
        imageOptions = new ImageSaveOptions(SaveFormat.Png);
    var savePaperColor = new SKColor((uint)imageOptions.PaperColor.ToArgb());
    imageOptions.PaperColor = Color.Transparent;
    // There a bug which affects the cache of a cloned node. To avoid this we instead clone the entire document including all nodes,
    // find the matching node in the cloned document and render that instead.
    Document doc = (Document)node.Document.Clone(true);
    node = doc.GetChild(NodeType.Any, node.Document.GetChildNodes(NodeType.Any, true).IndexOf(node), true);
    // Create a temporary shape to store the target node in. This shape will be rendered to retrieve
    // the rendered content of the node.
    Shape shape = new Shape(doc, ShapeType.TextBox);
    Section parentSection = (Section)node.GetAncestor(NodeType.Section);
    // Assume that the node cannot be larger than the page in size.
    shape.Width = parentSection.PageSetup.PageWidth;
    shape.Height = parentSection.PageSetup.PageHeight;
    shape.FillColor = Color.Transparent; // We must make the shape and paper color transparent.
                                         // Don't draw a surronding line on the shape.
    shape.Stroked = false;
    Node currentNode = node;
    // If the node contains block level nodes then just add a copy of these nodes to the shape.
    if (currentNode is InlineStory || currentNode is Story)
    {
        CompositeNode composite = (CompositeNode)currentNode;
        foreach (Node childNode in composite.GetChildNodes(NodeType.Any, false))
        {
            shape.AppendChild(childNode.Clone(true));
        }
    }
    else
    {
        // Move up through the DOM until we find node which is suitable to insert into a Shape (a node with a parent can contain paragraph, tables the same as a shape).
        // Each parent node is cloned on the way up so even a descendant node passed to this method can be rendered.
        // Since we are working with the actual nodes of the document we need to clone the target node into the temporary shape.
        while (!(currentNode.ParentNode is InlineStory || currentNode.ParentNode is Story || currentNode.ParentNode is ShapeBase || currentNode.NodeType == NodeType.Paragraph))
        {
            CompositeNode parent = (CompositeNode)currentNode.ParentNode.Clone(false);
            currentNode = currentNode.ParentNode;
            parent.AppendChild(node.Clone(true));
            node = parent; // Store this new node to be inserted into the shape.
        }
        // Add the node to the shape.
        shape.AppendChild(node.Clone(true));
    }
    // We must add the shape to the document tree to have it rendered.
    parentSection.Body.FirstParagraph.AppendChild(shape);
    // Render the shape to stream so we can take advantage of the effects of the ImageSaveOptions class.
    // Retrieve the rendered image and remove the shape from the document.
    using MemoryStream stream = new MemoryStream();
    shape.GetShapeRenderer().Save(stream, imageOptions);
    stream.Seek(0, SeekOrigin.Begin);
    shape.Remove();
    using (SKBitmap renderedImage = SKBitmap.Decode(stream))
    {
        SKRectI cropRectangle = FindBoundingBoxAroundNode(renderedImage);
        
        using var pixmap = new SKPixmap(renderedImage.Info, renderedImage.GetPixels());
        var subset = pixmap.ExtractSubset(cropRectangle);
        using var bitmap = new SKBitmap(subset.Width, subset.Height);
        bitmap.InstallPixels(subset);
        var copyBitmap = bitmap.Copy();

        using (var canvas = new SKCanvas(bitmap))
        {
            canvas.Clear(savePaperColor);
            canvas.DrawBitmap(copyBitmap, 0, 0);
        }

        using var data = SKImage.FromBitmap(bitmap).Encode();
        File.WriteAllBytes(savePath, data.ToArray());
    }
}

///
/// Finds the minimum bounding box around non-transparent pixels in a Bitmap.
///
public static SKRectI FindBoundingBoxAroundNode(SKBitmap originalBitmap)
{
    int minX = originalBitmap.Width;
    int minY = originalBitmap.Height;
    int maxX = 0;
    int maxY = 0;

    for (int x = 0; x < originalBitmap.Width; x++)
    {
        for (int y = 0; y < originalBitmap.Height; y++)
        {
            SKColor pixelColor = originalBitmap.GetPixel(x, y);

            if (pixelColor.Alpha != 0)
            {
                minX = Math.Min(minX, x);
                minY = Math.Min(minY, y);
                maxX = Math.Max(maxX, x);
                maxY = Math.Max(maxY, y);
            }
        }
    }

    int width = maxX - minX + 1;
    int height = maxY - minY + 1;

    return SKRectI.Create(minX, minY, width, height);
}

Offer Letter.docx (32.1 KB)

Hi @vyacheslav.deryushev , When I am trying to upload this kind of document and trying to get image of header and footer.
it throws an exception or sometime it just create background color image which is blue color bar and no content.

any idea or is there any limitation of the aspose?
please find attached png file, I am able to see this output_header0.png file but the empty blue bar and no data in it.

I have one more question is there a way to know if header or footer empty?

@m.darshan.shah This is a limitation. Your header contains two shapes that are not position bound. When we copy these nodes into a shape, we add them one after the other with paragraphs. If you want to use this formatting, you can add text inside the blue form and group the logo with the blue form. Here’s the updated template:

Offer Letter.docx (36.3 KB)

To check of header/footer is empty you can use following code:

NodeCollection nodes = composite.GetChildNodes(NodeType.Any, true);
if (nodes.Count == 1 && nodes[0].Range.Text.Trim().Equals(string.Empty))

@m.darshan.shah Anyway “RenderNode” method shows how you can use Aspose.Words and other libraries to get some functional. It’s not cover all possible cases, but it’s start point to improvements.