// ===========================================================================
// ©2013-2026 WebSupergoo. All rights reserved.
//
// This source code is for use exclusively with the ABCpdf product with
// which it is distributed, under the terms of the license for that
// product. Details can be found at
//
// http://www.websupergoo.com/
//
// This copyright notice must not be deleted and must be reproduced alongside
// any sections of code extracted from this module.
// ===========================================================================
using System;
using System.Xml;
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Globalization;
using WebSupergoo.ABCpdf14;
using WebSupergoo.ABCpdf14.Atoms;
namespace TaggedPDF {
/// <summary>
/// Parses xml files and converts them to tagged pdf
/// </summary>
public class XmlConverter {
protected enum TextFormat {
/// <summary>
/// No format
/// </summary>
None,
/// <summary>
/// HTML styled. Allows a single structure elements for
/// text with many styles. The XML should use < and >
/// </summary>
Html,
/// <summary>
/// Pre-formatted. Respects new line characters
/// </summary>
Pre
}
protected sealed class BackgroundElement {
/// <summary>
/// Rectangle
/// </summary>
public string Rect;
/// <summary>
/// Background color
/// </summary>
public string BackgroundColor;
/// <summary>
/// Background image
/// </summary>
public string BackgroundImage;
/// <summary>
/// Constructor
/// </summary>
public BackgroundElement() {
}
}
#region Members
/// <summary>
/// Base directory for resolving relative URIs
/// </summary>
private string mBaseDirectory;
/// <summary>
/// Parent document
/// </summary>
protected Doc mDoc;
/// <summary>
/// Xml reader for parsing document
/// </summary>
protected XmlTextReader mReader;
/// <summary>
/// The last page with the background set
/// </summary>
protected int mBackgroundSetPage;
/// <summary>
/// The last background set
/// </summary>
protected BackgroundElement mBackground;
/// <summary>
/// Stack of background items (e.g. body tags)
/// </summary>
protected Stack<BackgroundElement> mBackgroundStack = new Stack<BackgroundElement>();
/// <summary>
/// Stack of text formats
/// </summary>
protected Stack<TextFormat> mTextFormatStack = new Stack<TextFormat>();
/// <summary>
/// Stack of font sizes
/// </summary>
protected Stack<double> mFontSizeStack = new Stack<double>();
/// <summary>
/// Stack of font faces
/// </summary>
protected Stack<string> mFontFaceStack = new Stack<string>();
/// <summary>
/// Stack of font colors
/// </summary>
protected Stack<string> mFontColorStack = new Stack<string>();
/// <summary>
/// Stack of paragraph alignments
/// </summary>
protected Stack<double> mAlignStack = new Stack<double>();
/// <summary>
/// Stack of text styles
/// </summary>
protected Stack<XTextStyle> mTextStyleStack = new Stack<XTextStyle>();
/// <summary>
/// Height of the current line
/// </summary>
protected double mNewLineAdvance;
#endregion
#region TopLevel
protected XmlConverter() {
Init();
}
protected Doc Load(string fileName) {
mBaseDirectory = Path.GetDirectoryName(fileName) + Path.DirectorySeparatorChar;
mDoc = new Doc();
Init();
try {
mReader = new XmlTextReader(fileName);
mReader.WhitespaceHandling = WhitespaceHandling.None;
while (mReader.Read()) {
switch (mReader.NodeType) {
case XmlNodeType.Element:
string tagName = mReader.Name;
ProcessTag();
break;
case XmlNodeType.Text:
string text = mReader.Value;
ProcessText(text);
break;
case XmlNodeType.EndElement:
ProcessCloseTag(mReader);
break;
}
}
}
finally {
if (mReader != null)
mReader.Close();
}
return mDoc;
}
#endregion
#region Handlers
/// <summary>
/// Handle new paragraph tag P
/// </summary>
protected void HandleP() {
mDoc.Pos.X = mDoc.Rect.Left;
XTextStyle theTextStyle = new XTextStyle();
theTextStyle.String = mTextStyleStack.Peek().String;
string attr;
if (GetAttribute("fontsize", out attr))
mFontSizeStack.Push(double.Parse(attr,
NumberFormatInfo.InvariantInfo));
else
mFontSizeStack.Push(mFontSizeStack.Peek());
ExtractParagraphAttributes(theTextStyle);
mTextStyleStack.Push(theTextStyle);
}
/// <summary>
/// Handle end of the paragraph
/// </summary>
protected void HandleCloseP() {
mTextFormatStack.Pop();
mAlignStack.Pop();
mFontSizeStack.Pop();
XTextStyle textStyle = mTextStyleStack.Pop();
mDoc.Pos.Y -= textStyle.ParaSpacing + mNewLineAdvance;
mNewLineAdvance = 0;
}
/// <summary>
/// Handle headers tag (H1, H2 ... H6)
/// </summary>
protected void HandleH() {
mDoc.Pos.X = mDoc.Rect.Left;
XTextStyle theTextStyle = new XTextStyle();
theTextStyle.String = mTextStyleStack.Peek().String;
switch (mReader.Name.ToLower()) {
case "h1":
mFontSizeStack.Push(28);
break;
case "h2":
mFontSizeStack.Push(24);
break;
case "h3":
mFontSizeStack.Push(20);
break;
case "h4":
mFontSizeStack.Push(16);
break;
case "h5":
mFontSizeStack.Push(10);
break;
case "h6":
mFontSizeStack.Push(6);
break;
}
ExtractParagraphAttributes(theTextStyle);
mTextStyleStack.Push(theTextStyle);
}
/// <summary>
/// Handle Font tag
/// </summary>
protected void HandleFont() {
string attr;
if (GetAttribute("face", out attr))
mFontFaceStack.Push(attr);
else
mFontFaceStack.Push(mFontFaceStack.Peek());
if (GetAttribute("size", out attr))
mFontSizeStack.Push(double.Parse(attr,
NumberFormatInfo.InvariantInfo));
else
mFontSizeStack.Push(mFontSizeStack.Peek());
if (GetAttribute("color", out attr))
mFontColorStack.Push(attr);
else
mFontColorStack.Push(mFontColorStack.Peek());
}
/// <summary>
/// Handle end of the font tag
/// </summary>
protected void HandleCloseFont() {
mFontFaceStack.Pop();
mFontSizeStack.Pop();
mFontColorStack.Pop();
}
/// <summary>
/// Handle image tag
/// </summary>
protected void HandleImage() {
string alt, src;
GetAttribute("alt", out alt);
if (GetAttribute("src", out src))
ProcessImage(src, alt);
}
/// <summary>
/// Handle B tag
/// </summary>
protected void HandleB() {
XTextStyle theStyle = new XTextStyle();
theStyle.String = mTextStyleStack.Peek().String;
theStyle.Bold = true;
mTextStyleStack.Push(theStyle);
}
/// <summary>
/// Handle end of the B tag
/// </summary>
protected void HandleCloseB() {
mTextStyleStack.Pop();
}
/// <summary>
/// Handle I tag
/// </summary>
protected void HandleI() {
XTextStyle theStyle = new XTextStyle();
theStyle.String = mTextStyleStack.Peek().String;
theStyle.Italic = true;
mTextStyleStack.Push(theStyle);
}
/// <summary>
/// Handle end of the I tag
/// </summary>
protected void HandleCloseI() {
mTextStyleStack.Pop();
}
/// <summary>
/// Handle U tag
/// </summary>
protected void HandleU() {
XTextStyle theStyle = new XTextStyle();
theStyle.String = mTextStyleStack.Peek().String;
theStyle.Underline = true;
mTextStyleStack.Push(theStyle);
}
/// <summary>
/// Handle end of the U tag
/// </summary>
protected void HandleCloseU() {
mTextStyleStack.Pop();
}
/// <summary>
/// Handle Strike tag
/// </summary>
protected void HandleStrike() {
XTextStyle theStyle = new XTextStyle();
theStyle.String = mTextStyleStack.Peek().String;
theStyle.Strike = true;
mTextStyleStack.Push(theStyle);
}
/// <summary>
/// Handle end of the Strike tag
/// </summary>
protected void HandleCloseStrike() {
mTextStyleStack.Pop();
}
/// <summary>
/// Handle BR tag
/// </summary>
protected void HandleBR() {
mDoc.Pos.X = mDoc.Rect.Left;
mDoc.Pos.Y -= mNewLineAdvance;
mNewLineAdvance = 0;
}
/// <summary>
/// Handle BODY tag
/// </summary>
protected void HandleBody() {
string bgcolor, background;
GetAttribute("bgcolor", out bgcolor);
GetAttribute("background", out background);
string attr;
if(GetAttribute("leftmargin", out attr))
mDoc.Rect.Left += double.Parse(attr, NumberFormatInfo.InvariantInfo);
if (GetAttribute("rightmargin", out attr))
mDoc.Rect.Right -= double.Parse(attr, NumberFormatInfo.InvariantInfo);
if (GetAttribute("topmargin", out attr))
mDoc.Rect.Top -= double.Parse(attr, NumberFormatInfo.InvariantInfo);
if (GetAttribute("bottommargin", out attr))
mDoc.Rect.Bottom += double.Parse(attr, NumberFormatInfo.InvariantInfo);
BackgroundElement bodyItem = new BackgroundElement();
if (mBackgroundStack.Count <= 0) {
bodyItem.BackgroundColor = bgcolor;
bodyItem.BackgroundImage = background;
} else {
bodyItem.BackgroundColor = bgcolor??
mBackgroundStack.Peek().BackgroundColor;
bodyItem.BackgroundImage = background??
mBackgroundStack.Peek().BackgroundImage;
}
mBackgroundStack.Push(bodyItem);
}
/// <summary>
/// Handle end of BODY tag
/// </summary>
protected void HandleCloseBody() {
mBackgroundStack.Pop();
}
#endregion
#region Utilities
/// <summary>
/// Get the value of the attribute
/// </summary>
/// <param name="name">The name of the attribute</param>
/// <param name="value">The vale of the attribute</param>
/// <returns>Whether the result is null</returns>
private bool GetAttribute(string name, out string value) {
value = mReader.GetAttribute(name);
return value != null;
}
/// <summary>
/// Extract all common atrribute of p, h1, ... h6 tags
/// </summary>
/// <param name="textStyle">Textstyle descibed by the tag attributes</param>
private void ExtractParagraphAttributes(XTextStyle textStyle) {
string attr;
if (GetAttribute("textformat", out attr)) {
switch(attr) {
case "none":
mTextFormatStack.Push(TextFormat.None);
break;
case "html":
mTextFormatStack.Push(TextFormat.Html);
break;
case "pre":
mTextFormatStack.Push(TextFormat.Pre);
break;
default:
mTextFormatStack.Push(mTextFormatStack.Peek());
break;
}
}
else
mTextFormatStack.Push(mTextFormatStack.Peek());
if (GetAttribute("align", out attr)) {
switch (attr) {
case "left":
mAlignStack.Push(0);
break;
case "right":
mAlignStack.Push(1);
break;
case "center":
mAlignStack.Push(0.5);
break;
default:
mAlignStack.Push(mAlignStack.Peek());
break;
}
}
else
mAlignStack.Push(mAlignStack.Peek());
if (GetAttribute("charspacing", out attr))
textStyle.CharSpacing = double.Parse(attr,
NumberFormatInfo.InvariantInfo);
if (GetAttribute("wordspacing", out attr))
textStyle.WordSpacing = double.Parse(attr,
NumberFormatInfo.InvariantInfo);
if (GetAttribute("justification", out attr))
textStyle.Justification = double.Parse(attr,
NumberFormatInfo.InvariantInfo);
if (GetAttribute("bold", out attr))
textStyle.Bold = bool.Parse(attr);
if (GetAttribute("italic", out attr))
textStyle.Italic = bool.Parse(attr);
if (GetAttribute("underline", out attr))
textStyle.Underline = bool.Parse(attr);
if (GetAttribute("strike", out attr))
textStyle.Strike = bool.Parse(attr);
if (GetAttribute("strike2", out attr))
textStyle.Strike2 = bool.Parse(attr);
if (GetAttribute("outline", out attr))
textStyle.Outline = double.Parse(attr,
NumberFormatInfo.InvariantInfo);
if (GetAttribute("linespacing", out attr))
textStyle.LineSpacing = double.Parse(attr,
NumberFormatInfo.InvariantInfo);
if (GetAttribute("paraspacing", out attr)) {
textStyle.ParaSpacing = double.Parse(attr,
NumberFormatInfo.InvariantInfo);
mDoc.Pos.Y -= textStyle.ParaSpacing;
}
if (GetAttribute("leftmargin", out attr))
textStyle.LeftMargin = double.Parse(attr,
NumberFormatInfo.InvariantInfo);
if (GetAttribute("indent", out attr))
textStyle.Indent = double.Parse(attr,
NumberFormatInfo.InvariantInfo);
}
/// <summary>
/// Process arbitrary tag
/// </summary>
protected void ProcessTag() {
switch (mReader.Name.ToLower()) {
case "p":
HandleP();
mDoc.Tag.Open(mReader.Name);
break;
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
mDoc.Tag.Open(mReader.Name);
HandleH();
break;
case "font":
HandleFont();
break;
case "img":
HandleImage();
break;
case "b":
HandleB();
break;
case "br":
HandleBR();
break;
case "i":
HandleI();
break;
case "u":
HandleU();
break;
case "strike":
HandleStrike();
break;
case "body":
HandleBody();
break;
}
}
/// <summary>
/// Process closing of arbitrary tag
/// </summary>
/// <param name="mReader">Xml reader to read data from</param>
private void ProcessCloseTag(XmlReader mReader) {
switch (mReader.Name.ToLower()) {
case "p":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
HandleCloseP();
mDoc.Tag.Close(mReader.Name);
break;
case "font":
HandleCloseFont();
break;
case "b":
HandleCloseB();
break;
case "i":
HandleCloseI();
break;
case "u":
HandleCloseU();
break;
case "strike":
HandleCloseStrike();
break;
case "body":
HandleCloseBody();
break;
}
}
/// <summary>
/// Add block of text to the output content
/// </summary>
/// <param name="text">Text data</param>
protected void ProcessText(string text) {
SetBackground();
mDoc.TextStyle.String = mTextStyleStack.Peek().String;
mDoc.TextStyle.Size = mFontSizeStack.Peek();
mDoc.Font = mDoc.AddFont(mFontFaceStack.Peek());
mDoc.Color.String = mFontColorStack.Peek();
mDoc.TextStyle.HPos = mAlignStack.Peek();
if (mDoc.Pos.X == mDoc.Rect.Right)
mDoc.Pos.Y -= mNewLineAdvance;
int initialY = (int)mDoc.Pos.Y;
int id;
switch(mTextFormatStack.Peek()) {
case TextFormat.Pre:
id = mDoc.AddText(text);
if(id == 0) {
NewPage();
id = mDoc.AddText(text);
}
break;
case TextFormat.Html:
id = mDoc.AddTextStyled(text);
if(id == 0) {
NewPage();
id = mDoc.AddTextStyled(text);
}
break;
default:
text = HttpUtility.HtmlEncode(text);
id = mDoc.AddTextStyled(text);
if(id == 0) {
NewPage();
id = mDoc.AddTextStyled(text);
}
break;
}
while (mDoc.Chainable(id)) {
NewPage();
id = mDoc.AddTextStyled("", id);
}
if (mDoc.Pos.Y > initialY)
mNewLineAdvance = mDoc.TextStyle.Size;
else
mNewLineAdvance = Math.Max(mDoc.TextStyle.Size, mNewLineAdvance);
}
/// <summary>
/// Create a new page for the same tagged item.
/// </summary>
private void NewPage() {
mDoc.Page = mDoc.AddPage();
SetBackground();
}
/// <summary>
/// Add image object to the output content
/// </summary>
/// <param name="fileName">Image file name</param>
protected void ProcessImage(string fileName, string alt) {
XImage image = new XImage();
if (!Path.IsPathRooted(fileName))
fileName = mBaseDirectory + fileName;
if (!File.Exists(fileName))
return;
image.SetFile(fileName);
HandleBR();
SetBackground();
if (mDoc.Pos.Y - image.Height < mDoc.Rect.Bottom)
NewPage();
string curRect = mDoc.Rect.String;
double curY = mDoc.Pos.Y;
mDoc.TextStyle.HPos = mAlignStack.Peek();
if (image.Width < mDoc.Rect.Width)
mDoc.Rect.Left = mDoc.Rect.Left + (mDoc.Rect.Width - image.Width) * mDoc.TextStyle.HPos;
else
mDoc.Rect.Left = 0;
mDoc.Rect.Right = mDoc.Rect.Left + image.Width;
mDoc.Rect.Top = curY;
mDoc.Rect.Bottom = mDoc.Rect.Top - image.Height;
var figure = mDoc.Tag.MakeTag("Image");
if (!string.IsNullOrEmpty(alt)) {
figure.Attributes = new DictAtom();
figure.Attributes["Alt"] = new StringAtom(alt);
}
mDoc.Tag.Open(figure);
int id = mDoc.AddImage(fileName);
mDoc.Tag.Close(figure.Type);
mDoc.Rect.String = curRect;
mDoc.Pos.Y = curY;
mDoc.Pos.X += image.Width;
mNewLineAdvance = image.Height;
HandleBR();
image.Dispose();
}
/// <summary>
/// Add background to the document
/// </summary>
/// <param name="item">background item</param>
protected void SetBackground() {
if (mBackgroundStack.Count <= 0)
return;
BackgroundElement item = mBackgroundStack.Peek();
// Whether either color or image is set for the current page
// If both are set, return
bool partialSet = false;
if (mBackgroundSetPage == mDoc.Page) {
if (item == mBackground)
return;
if (mBackground != null) {
if(mBackground.BackgroundColor != null
&& mBackground.BackgroundImage != null)
return;
partialSet = true;
}
}
mBackgroundSetPage = mDoc.Page;
mBackground = item;
string rect = item.Rect?? mDoc.MediaBox.String;
List<int> contentIds = null;
if (item.BackgroundColor != null
&& (!partialSet || mBackground.BackgroundColor == null))
{
if (contentIds == null)
contentIds = new List<int>();
string curPos = mDoc.Pos.String;
string curRect = mDoc.Rect.String;
string curColor = mDoc.Color.String;
mDoc.Rect.String = rect;
mDoc.Color.String = item.BackgroundColor;
contentIds.Add(mDoc.FillRect());
mDoc.Rect.String = curRect;
mDoc.Color.String = curColor;
mDoc.Pos.String = curPos;
}
if (item.BackgroundImage != null
&& (!partialSet || mBackground.BackgroundImage == null))
{
string fileName = item.BackgroundImage;
if (!Path.IsPathRooted(fileName))
fileName = mBaseDirectory + fileName;
if (!File.Exists(fileName))
item.BackgroundImage = null;
else {
if (contentIds == null)
contentIds = new List<int>();
XImage image = new XImage();
image.SetFile(fileName);
string curPos = mDoc.Pos.String;
string curRect = mDoc.Rect.String;
mDoc.Rect.String = rect;
contentIds.Add(mDoc.AddImage(image));
mDoc.Rect.String = curRect;
mDoc.Pos.String = curPos;
}
}
}
/// <summary>
/// Set default values for text styles and other formatting attributes
/// </summary>
protected void Init() {
mBackgroundSetPage = 0;
mBackground = null;
mBackgroundStack.Clear();
mTextFormatStack.Clear();
mTextFormatStack.Push(TextFormat.None);
mFontSizeStack.Clear();
mFontSizeStack.Push(10);
mFontFaceStack.Clear();
mFontFaceStack.Push("Times-Roman");
mFontColorStack.Clear();
mFontColorStack.Push("black");
mAlignStack.Clear();
mAlignStack.Push(0);
mNewLineAdvance = 0;
mTextStyleStack.Clear();
mTextStyleStack.Push(new XTextStyle());
}
#endregion
#region Public methods
/// <summary>
/// Convert xml file to the tagged pdf
/// </summary>
/// <param name="fileName">Xml file name</param>
public static Doc Create(string fileName) {
var conv = new XmlConverter();
return conv.Load(fileName);
}
#endregion
}
}