TX Text Control .NET Server

Pages

Topic

How many pages does a merged report have, and on which page does a table start? A document is flowing text, but once TX Text Control has formatted it, it consists of pages with numbers, sizes and positions. With ServerTextControl, you count the pages, find the page that contains a word, a field or a table, read the text of a single page, render pages as SVG images for thumbnails, combine page bitmaps in a multipage TIFF file and remove empty pages.

Page size, margins and orientation are set per section, as Sections describes, and page numbers in headers and footers are added in Headers and Footers. The examples below are console programs that load Documents/sample.docx.

How Pages Work

ServerTextControl.Pages is the number of pages of the document, and GetPages returns a PageCollection with a Page object for each page. A document has at least one page. A section can span several pages, and several sections can share one page, because a section doesn't have to begin at the top of a page.

A page has these properties, all read only:

PropertyContains
NumberThe number of the page, starting with 1
Section, NumberInSectionThe number of the section and the number of the page within it, both starting with 1. With several sections on a page, the first one, which determines the page size and orientation
Start, LengthThe one-based position of the first character and the number of characters, including the page break character at the end
Bounds, TextBoundsThe rectangle of the page and the rectangle in which its text can be placed, in twips, relative to the top of the document
Header, FooterThe header and the footer shown on the page, or null

What ends up on a page depends on the formatting of the document: the page settings of its sections, paragraph settings such as PageBreakBefore and KeepWithNext, which Paragraphs describes, and the fonts available on the server, which Creating Documents covers.

Count and Describe the Pages

The following program prints the number of pages and sections and describes each page:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/sample.docx", StreamType.WordprocessingML);

    Console.WriteLine($"{tx.Pages} pages in {tx.Sections.Count} sections");

    foreach (Page page in tx.GetPages())
    {
        string header = page.Header == null ? "none" : page.Header.Type.ToString();
        string footer = page.Footer == null ? "none" : page.Footer.Type.ToString();

        Console.WriteLine($"Page {page.Number}: section {page.Section}, page {page.NumberInSection} of the section");
        Console.WriteLine($"  Characters {page.Start} to {page.Start + page.Length - 1}");
        Console.WriteLine($"  Page {page.Bounds}, text area {page.TextBounds}");
        Console.WriteLine($"  Header: {header}, footer: {footer}");
    }
}

HeaderFooter.Type tells which header or footer a page shows: FirstPageHeader and FirstPageFooter on the first page, EvenHeader and EvenFooter on even pages and Header and Footer on the other pages.

Find the Page of Text, Fields and Tables

PageCollection.GetItem with a zero-based text position returns the page that contains the position, or null. The following program finds the page of a word, of every application field and of every table:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/sample.docx", StreamType.WordprocessingML);

    PageCollection pages = tx.GetPages();

    // Find returns a zero-based position
    int position = tx.Find("Total");

    if (position != -1)
    {
        Console.WriteLine($"\"Total\" is on page {pages.GetItem(position).Number}");
    }

    // Start is one-based
    foreach (ApplicationField field in tx.ApplicationFields)
    {
        Page page = pages.GetItem(field.Start - 1);
        Console.WriteLine($"{field.TypeName} \"{field.Text}\": page {page?.Number}");
    }

    // A table can continue on the following pages
    foreach (Table table in tx.Tables)
    {
        int firstPage = int.MaxValue;
        int lastPage = 0;

        foreach (TableCell cell in table.Cells)
        {
            int number = pages.GetItem(cell.Start - 1).Number;
            firstPage = Math.Min(firstPage, number);
            lastPage = Math.Max(lastPage, number);
        }

        Console.WriteLine($"Table {table.ID}: pages {firstPage} to {lastPage}");
    }
}

The code works as follows:

  • Find returns a zero-based position, which GetItem takes as it is.
  • TextField.Start and TableCell.Start are one-based, so 1 is subtracted. Application fields are listed only if the document was loaded with LoadSettings.ApplicationFieldFormat, as Fields shows.
  • The pages of the cells give the first and the last page of a table.

For a single position, Line.Page returns the page number too: tx.Lines.GetItem(position).Page.

Read the Text of Each Page

Page.Select selects the text of a page, without the page breaks around it and without its header and footer. The following program prints the text page by page:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/sample.docx", StreamType.WordprocessingML);

    foreach (Page page in tx.GetPages())
    {
        page.Select();

        Console.WriteLine($"--- Page {page.Number} ---");
        Console.WriteLine(tx.Selection.Text);
    }
}

After Select, the selection can also be saved in a format with Selection.Save, as Saving and Converting Documents describes.

Render Pages as SVG Images

Page.GetImage returns a page as a string in SVG format, with the same rendering as the document in a PDF. The following program saves each page as an SVG file:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/sample.docx", StreamType.WordprocessingML);

    Directory.CreateDirectory("pages");
    PageCollection pages = tx.GetPages();

    for (int i = 1; i <= pages.Count; i++)
    {
        // Text as glyph outlines, embedded images with at most 96 dpi
        string svg = pages[i].GetImage(Page.PageContent.All | Page.PageContent.GlyphOutlines, 96);
        File.WriteAllText($"pages/page-{i}.svg", svg);
    }

    Console.WriteLine($"{pages.Count} pages saved");
}

The code works as follows:

  • The first parameter combines values of PageContent, written as Page.PageContent in code: Background, HeadersAndFooters and MainText select the parts of the page, and All combines these three. ScreenElements adds the selection, control characters and helper lines.
  • GlyphOutlines writes the characters as outlines, so the image looks the same where the font isn't installed. Without it, the SVG contains text with font information, which remains selectable.
  • The second parameter is the maximum resolution of embedded images in dots per inch. Images are embedded as PNG.
  • Coordinates in the SVG are in twips: a US letter page is 12240 wide and 15840 high.

Two further overloads of GetImage return a System.Drawing.Imaging.Bitmap or a Metafile. They need System.Drawing.Common, which the Classic package TXTextControl.TextControl.ASP.SDK depends on and the Core package doesn't; Render Pages as Bitmaps and Multipage TIFF Files shows them, and Document Comparison uses the bitmaps.

Return a Page Image From ASP.NET Core

A controller action can send a page as an image that a browser shows in an img element, for example as a thumbnail:

using System.Text;
using Microsoft.AspNetCore.Mvc;
using TXTextControl;

public class ThumbnailController : Controller
{
    [HttpGet]
    public IActionResult GetPageImage(int number = 1)
    {
        using (ServerTextControl tx = new ServerTextControl())
        {
            tx.Create();
            tx.Load("App_Data/sample.docx", StreamType.WordprocessingML);

            if (number < 1 || number > tx.Pages)
            {
                return NotFound();
            }

            string svg = tx.GetPages()[number].GetImage(Page.PageContent.All | Page.PageContent.GlyphOutlines, 96);
            return File(Encoding.UTF8.GetBytes(svg), "image/svg+xml");
        }
    }
}

The blog article How to Generate SVG Images from Document Pages in ASP.NET Core C# embeds the images in a page as data URLs.

Render Pages as Bitmaps and Multipage TIFF Files

A TIFF file can hold several images as frames, one for each page of a document. Page.GetImage(int zoomFactor, Page.PageContent contents) returns a page as a System.Drawing.Imaging.Bitmap in screen resolution, and the TIFF encoder of System.Drawing combines the bitmaps into one file. The bitmap overload needs System.Drawing.Common, so the program uses the Classic package TXTextControl.TextControl.ASP.SDK, which depends on it and runs on Windows only. On Linux, render the pages as SVG images as shown above.

using System.Drawing;
using System.Drawing.Imaging;
using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/sample.docx", StreamType.WordprocessingML);

    // One bitmap per page, at a zoom factor of 100 percent
    List<Bitmap> bitmaps = new List<Bitmap>();

    foreach (Page page in tx.GetPages())
    {
        bitmaps.Add(page.GetImage(100, Page.PageContent.All));
    }

    SaveMultipageTiff(bitmaps, "pages.tif");
    Console.WriteLine($"{bitmaps.Count} pages saved in pages.tif");

    foreach (Bitmap bitmap in bitmaps)
    {
        bitmap.Dispose();
    }
}

static void SaveMultipageTiff(List<Bitmap> bitmaps, string fileName)
{
    ImageCodecInfo tiffCodec = ImageCodecInfo.GetImageEncoders().First(codec => codec.MimeType == "image/tiff");

    using (EncoderParameters parameters = new EncoderParameters(2))
    {
        parameters.Param[0] = new EncoderParameter(Encoder.Compression, (long)EncoderValue.CompressionLZW);

        // The first page creates the file
        parameters.Param[1] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.MultiFrame);
        bitmaps[0].Save(fileName, tiffCodec, parameters);

        // Every further page is added as a frame
        parameters.Param[1] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.FrameDimensionPage);

        for (int i = 1; i < bitmaps.Count; i++)
        {
            bitmaps[0].SaveAdd(bitmaps[i], parameters);
        }

        // Close the file
        parameters.Param[1] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.Flush);
        bitmaps[0].SaveAdd(parameters);
    }
}

The code works as follows:

  • GetImage with a zoom factor and a PageContent value returns a bitmap of the page. A larger zoom factor gives a larger image.
  • The first bitmap is saved with the encoder parameter MultiFrame, which creates a TIFF file that can hold more frames. SaveAdd with FrameDimensionPage appends each further page to it, and SaveAdd with Flush closes the file.
  • CompressionLZW sets the compression of the frames.

Remove Empty Pages

A page that contains only its page break or section break character has a length of 1. Such empty pages remain, for example, when a forced page break follows content that already filled a page. The following program removes them, from the last page to the first:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/sample.docx", StreamType.WordprocessingML);

    int pagesBefore = tx.Pages;

    // From the last page to the first, so the pages still to check keep their numbers
    for (int number = tx.Pages; number >= 1; number--)
    {
        Page page = tx.GetPages()[number];

        if (page.Length == 1)
        {
            // Delete the break character of the empty page
            tx.Select(page.Start - 1, 1);
            tx.Selection.Text = "";
        }
    }

    Console.WriteLine($"Pages before: {pagesBefore}, after: {tx.Pages}");
    tx.Save("without-empty-pages.docx", StreamType.WordprocessingML);
}

To avoid empty pages in templates, start headings on a new page with the paragraph setting PageBreakBefore instead of a page break character, as the blog article Document Templates Tip: Say No to Forced Page Breaks explains.

Next Steps