using System; using System.Collections.Generic; using System.Drawing.Drawing2D; using System.Drawing; using System.Globalization; using System.Text; using System.Net; using System.IO; namespace FastReport.Utils { /// /// Advanced text renderer is used to perform the following tasks: /// - draw justified text, text with custom line height, text containing html tags; /// - calculate text height, get part of text that does not fit in the display rectangle; /// - get paragraphs, lines, words and char sequence to perform accurate export to such /// formats as PDF, TXT, RTF /// /// Here is how one may operate the renderer items: /// /// foreach (AdvancedTextRenderer.Paragraph paragraph in renderer.Paragraphs) /// { /// foreach (AdvancedTextRenderer.Line line in paragraph.Lines) /// { /// foreach (AdvancedTextRenderer.Word word in line.Words) /// { /// if (renderer.HtmlTags) /// { /// foreach (AdvancedTextRenderer.Run run in word.Runs) /// { /// using (Font f = run.GetFont()) /// using (Brush b = run.GetBrush()) /// { /// g.DrawString(run.Text, f, b, run.Left, run.Top, renderer.Format); /// } /// } /// } /// else /// { /// g.DrawString(word.Text, renderer.Font, renderer.Brush, word.Left, word.Top, renderer.Format); /// } /// } /// } /// } /// /// public class AdvancedTextRenderer { #region Fields private List paragraphs; private string text; private IGraphics graphics; private Font font; private Brush brush; private Pen outlinePen; private RectangleF displayRect; private StringFormat format; private HorzAlign horzAlign; private VertAlign vertAlign; private float lineHeight; private float fontLineHeight; private int angle; private float widthRatio; private bool forceJustify; private bool wysiwyg; private bool htmlTags; private bool pDFMode; private float spaceWidth; private float scale; private InlineImageCache cache; private float fontScale; #endregion #region Properties public List Paragraphs { get { return paragraphs; } } public IGraphics Graphics { get { return graphics; } } public Font Font { get { return font; } } public Brush Brush { get { return brush; } } public Pen OutlinePen { get { return outlinePen; } } public Color BrushColor { get { return brush is SolidBrush ? (brush as SolidBrush).Color : Color.Black; } } public RectangleF DisplayRect { get { return displayRect; } } public StringFormat Format { get { return format; } } public HorzAlign HorzAlign { get { return horzAlign; } } public VertAlign VertAlign { get { return vertAlign; } } public float LineHeight { get { return lineHeight; } } public float FontLineHeight { get { return fontLineHeight; } } public int Angle { get { return angle; } } public float WidthRatio { get { return widthRatio; } } public bool ForceJustify { get { return forceJustify; } } public bool Wysiwyg { get { return wysiwyg; } } public bool HtmlTags { get { return htmlTags; } } public float TabSize { get { // re fix tab offset #2823 sorry linux users, on linux firstTab is firstTab not tabSizes[0] float firstTab = 0; float[] tabSizes = Format.GetTabStops(out firstTab); if (tabSizes.Length > 1) return tabSizes[1]; return 0; } } public float TabOffset { get { // re fix tab offset #2823 sorry linux users, on linux firstTab is firstTab not tabSizes[0] float firstTab = 0; float[] tabSizes = Format.GetTabStops(out firstTab); if (tabSizes.Length > 0) return tabSizes[0]; return 0; } } public bool WordWrap { get { return (Format.FormatFlags & StringFormatFlags.NoWrap) == 0; } } public bool RightToLeft { get { return (Format.FormatFlags & StringFormatFlags.DirectionRightToLeft) != 0; } } public bool PDFMode { get { return pDFMode; } } internal float SpaceWidth { get { return spaceWidth; } } /// /// The scale for font tag /// public float FontScale { get { return fontScale; } set { fontScale = value; } } public float Scale { get { return scale; } set { scale = value; } } public InlineImageCache Cache { get { if (cache == null) cache = new InlineImageCache(); return cache; } } #endregion #region Private Methods const string ab = "abcdefabcdef"; const string a40b = "abcdef abcdef"; internal static float CalculateSpaceSize(IGraphics g, Font f) { float w_ab = g.MeasureString(ab, f).Width; float w_a40b = g.MeasureString(a40b, f).Width; return (w_a40b - w_ab) / 40; } private void SplitToParagraphs(string text) { StyleDescriptor style = new StyleDescriptor(Font.Style, BrushColor, BaseLine.Normal); if (HtmlTags) text = text.Replace("
", "\r\n").Replace("
", "\r\n").Replace("
", "\r\n"); string[] lines = text.Split('\n'); int originalCharIndex = 0; foreach (string line in lines) { string s = line; if (s.Length > 0 && s[s.Length - 1] == '\r') s = s.Remove(s.Length - 1); Paragraph paragraph = new Paragraph(s, this, originalCharIndex); paragraphs.Add(paragraph); if (HtmlTags) style = paragraph.WrapHtmlLines(style); else paragraph.WrapLines(); originalCharIndex += line.Length + 1; } // skip empty paragraphs at the end for (int i = paragraphs.Count - 1; i >= 0; i--) { if (paragraphs[i].IsEmpty && paragraphs.Count != 1) paragraphs.RemoveAt(i); else break; } } private void AdjustParagraphLines() { // calculate text height float height = CalcHeight(); // calculate Y offset float offsetY = DisplayRect.Top; if (VertAlign == VertAlign.Center) offsetY += (DisplayRect.Height - height) / 2; else if (VertAlign == VertAlign.Bottom) offsetY += (DisplayRect.Height - height) - 1; for (int i = 0; i < Paragraphs.Count; i++) { Paragraph paragraph = Paragraphs[i]; paragraph.AlignLines(i == Paragraphs.Count - 1 && ForceJustify); // adjust line tops foreach (Line line in paragraph.Lines) { line.Top = offsetY; line.MakeUnderlines(); offsetY += line.CalcHeight(); } } } #endregion #region Public Methods public void Draw() { // set clipping IGraphicsState state = Graphics.Save(); Graphics.SetClip(DisplayRect, CombineMode.Intersect); // reset alignment StringAlignment saveAlign = Format.Alignment; StringAlignment saveLineAlign = Format.LineAlignment; Format.Alignment = StringAlignment.Near; Format.LineAlignment = StringAlignment.Near; if (Angle != 0) { Graphics.TranslateTransform(DisplayRect.Left + DisplayRect.Width / 2, DisplayRect.Top + DisplayRect.Height / 2); Graphics.RotateTransform(Angle); } Graphics.ScaleTransform(WidthRatio, 1); foreach (Paragraph paragraph in Paragraphs) { paragraph.Draw(); } // restore alignment and clipping Format.Alignment = saveAlign; Format.LineAlignment = saveLineAlign; Graphics.Restore(state); } public float CalcHeight() { int charsFit = 0; StyleDescriptor style = null; return CalcHeight(out charsFit, out style); } public float CalcHeight(out int charsFit, out StyleDescriptor style) { charsFit = 0; style = null; float height = 0; float displayHeight = DisplayRect.Height; if (LineHeight > displayHeight) return 0; foreach (Paragraph paragraph in Paragraphs) { foreach (Line line in paragraph.Lines) { height += line.CalcHeight(); if (charsFit == 0 && height > displayHeight) { charsFit = line.OriginalCharIndex; if (HtmlTags) style = line.Style; } } } if (charsFit == 0) charsFit = text.Length; return height; } public float CalcWidth() { float width = 0; foreach (Paragraph paragraph in Paragraphs) { foreach (Line line in paragraph.Lines) { if (width < line.Width) width = line.Width; } } return width + spaceWidth; } internal float GetTabPosition(float pos) { float tabOffset = TabOffset; float tabSize = TabSize; int tabPosition = (int)((pos - tabOffset) / tabSize); if (pos < tabOffset) return tabOffset; return (tabPosition + 1) * tabSize + tabOffset; } #endregion public AdvancedTextRenderer(string text, IGraphics g, Font font, Brush brush, Pen outlinePen, RectangleF rect, StringFormat format, HorzAlign horzAlign, VertAlign vertAlign, float lineHeight, int angle, float widthRatio, bool forceJustify, bool wysiwyg, bool htmlTags, bool pdfMode, float scale, float fontScale, InlineImageCache cache, bool isPrinting = false) { this.cache = cache; this.scale = scale; this.fontScale = fontScale; paragraphs = new List(); this.text = text; graphics = g; this.font = font; this.brush = brush; this.outlinePen = outlinePen; displayRect = rect; this.format = format; this.horzAlign = horzAlign; this.vertAlign = vertAlign; this.lineHeight = lineHeight; fontLineHeight = font.GetHeight(g.Graphics); if (this.lineHeight == 0) { this.lineHeight = fontLineHeight; if (isPrinting && Config.IsRunningOnMono && DrawUtils.GetMonoRendering(g.Graphics) == MonoRendering.Pango) { // we need this in order to fix inconsistent line spacing when print using Pango rendering this.lineHeight = fontLineHeight * 1.33f; } } this.angle = angle % 360; this.widthRatio = widthRatio; this.forceJustify = forceJustify; this.wysiwyg = wysiwyg; this.htmlTags = htmlTags; pDFMode = pdfMode; spaceWidth = CalculateSpaceSize(g, font);// g.MeasureString(" ", font).Width; StringFormatFlags saveFlags = Format.FormatFlags; StringTrimming saveTrimming = Format.Trimming; // match DrawString behavior: // if height is less than 1.25 of font height, turn off word wrap // commented out due to bug with band.break //if (rect.Height < FFontLineHeight * 1.25f) //FFormat.FormatFlags |= StringFormatFlags.NoWrap; // if word wrap is set, ignore trimming if (WordWrap) Format.Trimming = StringTrimming.Word; // LineLimit flag is essential in linux Format.FormatFlags = Format.FormatFlags | StringFormatFlags.MeasureTrailingSpaces | StringFormatFlags.LineLimit; if (Angle != 0) { // shift displayrect displayRect.X = -DisplayRect.Width / 2; displayRect.Y = -DisplayRect.Height / 2; // rotate displayrect if angle is 90 or 270 if ((Angle >= 90 && Angle < 180) || (Angle >= 270 && Angle < 360)) displayRect = new RectangleF(DisplayRect.Y, DisplayRect.X, DisplayRect.Height, DisplayRect.Width); } displayRect.X /= WidthRatio; displayRect.Width /= WidthRatio; SplitToParagraphs(text); AdjustParagraphLines(); // restore original values displayRect = rect; Format.FormatFlags = saveFlags; Format.Trimming = saveTrimming; } /// /// Paragraph represents single paragraph. It consists of one or several . /// public class Paragraph { #region Fields private List lines; private AdvancedTextRenderer renderer; private string text; private int originalCharIndex; #endregion #region Properties public List Lines { get { return lines; } } public AdvancedTextRenderer Renderer { get { return renderer; } } public bool Last { get { return renderer.Paragraphs[renderer.Paragraphs.Count - 1] == this; } } public bool IsEmpty { get { return String.IsNullOrEmpty(text); } } public string Text { get { return text; } } #endregion #region Private Methods private int MeasureString(string text) { if (text.Length > 0) { // BEGIN: The fix for linux and core app a264aae5-193b-4e5c-955c-0818de3ca01b float left = 0; int tabFit = 0; while (text.Length > 0 && text[0] == '\t') { left = Renderer.GetTabPosition(left); text = text.Substring(1); if (Renderer.DisplayRect.Width < left) return tabFit; tabFit++; } if (tabFit > 0 && Renderer.DisplayRect.Width < left) return tabFit; int charsFit = 0; int linesFit = 0; // END: The fix for linux and core app a264aae5-193b-4e5c-955c-0818de3ca01b Renderer.Graphics.MeasureString(text, Renderer.Font, new SizeF(Renderer.DisplayRect.Width - left, Renderer.FontLineHeight * 1.25f), Renderer.Format, out charsFit, out linesFit); return charsFit + tabFit; } return 0; } #endregion #region Public Methods public void WrapLines() { string text = this.text; int charsFit = 0; if (String.IsNullOrEmpty(text)) { lines.Add(new Line("", this, originalCharIndex)); return; } if (Renderer.WordWrap) { int originalCharIndex = this.originalCharIndex; while (text.Length > 0) { charsFit = MeasureString(text); // avoid infinite loop when width of object less than width of one character if (charsFit == 0) { break; } string textFit = text.Substring(0, charsFit).TrimEnd(' '); lines.Add(new Line(textFit, this, originalCharIndex)); text = text.Substring(charsFit) // Fix for linux system .TrimStart(' '); originalCharIndex += charsFit; } } else { string ellipsis = "\u2026"; StringTrimming trimming = Renderer.Format.Trimming; if (trimming == StringTrimming.EllipsisPath) Renderer.Format.Trimming = StringTrimming.Character; charsFit = MeasureString(text); switch (trimming) { case StringTrimming.Character: case StringTrimming.Word: text = text.Substring(0, charsFit); break; case StringTrimming.EllipsisCharacter: case StringTrimming.EllipsisWord: if (charsFit < text.Length) { text = text.Substring(0, charsFit); if (text.EndsWith(" ")) text = text.Substring(0, text.Length - 1); text += ellipsis; } break; case StringTrimming.EllipsisPath: if (charsFit < text.Length) { while (text.Length > 3) { int mid = text.Length / 2; string newText = text.Substring(0, mid) + ellipsis + text.Substring(mid + 1); if (MeasureString(newText) == newText.Length) { text = newText; break; } else { text = text.Remove(mid, 1); } } } break; } lines.Add(new Line(text, this, originalCharIndex)); } } public StyleDescriptor WrapHtmlLines(StyleDescriptor style) { Line line = new Line("", this, this.originalCharIndex); lines.Add(line); Word word = new Word("", line); line.Words.Add(word); // for img //RunImage img = null; //end img string text = this.text; StringBuilder currentWord = new StringBuilder(100); float width = 0; bool skipSpace = true; int originalCharIndex = this.originalCharIndex; for (int i = 0; i < text.Length; i++) { char lastChar = text[i]; if (lastChar == '&') { if (Converter.FromHtmlEntities(text, ref i, currentWord)) { if (i >= text.Length - 1) { word.Runs.Add(new Run(currentWord.ToString(), style, word)); // check width width += word.Width + Renderer.SpaceWidth; if (width > Renderer.DisplayRect.Width) { // line is too long, make a new line if (line.Words.Count > 1) { // if line has several words, delete the last word from the current line line.Words.RemoveAt(line.Words.Count - 1); // make new line line = new Line("", this, originalCharIndex); // and add word to it line.Words.Add(word); word.SetLine(line); lines.Add(line); } } #if DOTNET_4 currentWord.Clear(); // .NET 2.0 doesn't have Clear() #else currentWord.Length = 0; #endif lastChar = ' '; } else { if (currentWord[currentWord.Length - 1] == '\t') { currentWord.Length--; lastChar = '\t'; } else { continue; } } } } if (lastChar == '<') { // probably html tag StyleDescriptor newStyle = new StyleDescriptor(style.FontStyle, style.Color, style.BaseLine); newStyle.Font = style.Font; newStyle.Size = style.Size; string tag = ""; bool match = false; // , , if (i + 3 <= text.Length) { match = true; tag = text.Substring(i, 3).ToLower(); if (tag == "") newStyle.FontStyle |= FontStyle.Bold; else if (tag == "") newStyle.FontStyle |= FontStyle.Italic; else if (tag == "") newStyle.FontStyle |= FontStyle.Underline; else match = false; if (match) i += 3; } // , , if (!match && i + 4 <= text.Length && text[i + 1] == '/') { match = true; tag = text.Substring(i, 4).ToLower(); if (tag == "") newStyle.FontStyle &= ~FontStyle.Bold; else if (tag == "") newStyle.FontStyle &= ~FontStyle.Italic; else if (tag == "") newStyle.FontStyle &= ~FontStyle.Underline; else match = false; if (match) i += 4; } // , // ") newStyle.BaseLine = BaseLine.Subscript; else if (tag == "") newStyle.BaseLine = BaseLine.Superscript; else if (tag == "', i + 5); if (right <= 0) match = false; else { //found img and parse them string src = null; string alt = " "; //currentWord = ""; int src_ind = text.IndexOf("src=\"", i + 5); if (src_ind < right && src_ind >= 0) { src_ind += 5; int src_end = text.IndexOf("\"", src_ind); if (src_end < right && src_end >= 0) { src = text.Substring(src_ind, src_end - src_ind); } } int alt_ind = text.IndexOf("alt=\"", i + 5); if (alt_ind < right && alt_ind >= 0) { alt_ind += 5; int alt_end = text.IndexOf("\"", alt_ind); if (alt_end < right && alt_end >= 0) { alt = text.Substring(alt_ind, alt_end - alt_ind); } } //begin if (currentWord.Length != 0) { // finish the word word.Runs.Add(new Run(currentWord.ToString(), style, word)); } #if DOTNET_4 currentWord.Clear(); // .NET 2.0 doesn't have Clear() #else currentWord.Length = 0; #endif //end word.Runs.Add(new RunImage(src, alt, style, word)); skipSpace = false; i = right - 4; } } else if (tag == "', i + 5); if (right <= 0) match = false; else { //found font and parse them string color = null; string face = null; string size = null; int color_ind = text.IndexOf("color=\"", i + 5); if (color_ind < right && color_ind >= 0) { color_ind += 7; int color_end = text.IndexOf("\"", color_ind); if (color_end < right && color_end >= 0) { color = text.Substring(color_ind, color_end - color_ind); } } int face_ind = text.IndexOf("face=\"", i + 5); if (face_ind < right && face_ind >= 0) { face_ind += 6; int face_end = text.IndexOf("\"", face_ind); if (face_end < right && face_end >= 0) { face = text.Substring(face_ind, face_end - face_ind); } } int size_ind = text.IndexOf("size=\"", i + 5); if (size_ind < right && size_ind >= 0) { size_ind += 6; int size_end = text.IndexOf("\"", size_ind); if (size_end < right && size_end >= 0) { size = text.Substring(size_ind, size_end - size_ind); } } if (color != null) { if (color.StartsWith("\"") && color.EndsWith("\"")) color = color.Substring(1, color.Length - 2); if (color.StartsWith("#")) { newStyle.Color = Color.FromArgb((int)(0xFF000000 + uint.Parse(color.Substring(1), NumberStyles.HexNumber))); } else { newStyle.Color = Color.FromName(color); } } newStyle.Font = face; if (size != null) { try { size = size.Trim(' '); switch (size[0]) { case '-': size = size.Substring(1); if (style.Size == 0) newStyle.Size = Renderer.Font.Size - (float)Converter.FromString(typeof(float), size) * Renderer.FontScale; else newStyle.Size = style.Size - (float)Converter.FromString(typeof(float), size) * Renderer.FontScale; break; case '+': size = size.Substring(1); if (style.Size == 0) newStyle.Size = Renderer.Font.Size + (float)Converter.FromString(typeof(float), size) * Renderer.FontScale; else newStyle.Size = style.Size + (float)Converter.FromString(typeof(float), size) * Renderer.FontScale; break; default: newStyle.Size = (float)Converter.FromString(typeof(float), size) * Renderer.FontScale; break; } if (newStyle.Size < 0) newStyle.Size = 0; } catch { } } i = right - 4; } } else match = false; if (match) i += 5; } // , if (!match && i + 6 <= text.Length && text[i + 1] == '/') { match = true; tag = text.Substring(i, 6).ToLower(); if (tag == "") newStyle.BaseLine = BaseLine.Normal; else if (tag == "") newStyle.BaseLine = BaseLine.Normal; else match = false; if (match) i += 6; } // if (!match && i + 8 <= text.Length && text.Substring(i, 8).ToLower() == "") { newStyle.FontStyle |= FontStyle.Strikeout; match = true; i += 8; } // if (!match && i + 9 <= text.Length && text.Substring(i, 9).ToLower() == "") { newStyle.FontStyle &= ~FontStyle.Strikeout; match = true; i += 9; } /* // if (!match && i + 7 <= text.Length && text.Substring(i, 7).ToLower() == "") { newStyle.Color = Renderer.BrushColor; newStyle.Size = 0; newStyle.Font = null; match = true; i += 7; } if (match) { if (currentWord.Length != 0) { // finish the word word.Runs.Add(new Run(currentWord.ToString(), style, word)); } #if DOTNET_4 currentWord.Clear(); // .NET 2.0 doesn't have Clear() #else currentWord.Length = 0; #endif style = newStyle; i--; if (i >= text.Length - 1) { // check width width += word.Width + Renderer.SpaceWidth; if (width > Renderer.DisplayRect.Width) { // line is too long, make a new line if (line.Words.Count > 1) { // if line has several words, delete the last word from the current line line.Words.RemoveAt(line.Words.Count - 1); // make new line line = new Line("", this, originalCharIndex); // and add word to it line.Words.Add(word); word.SetLine(line); lines.Add(line); } } } continue; } } if (lastChar == ' ' || lastChar == '\t' || i == text.Length - 1) { // finish the last word bool isLastWord = i == text.Length - 1; if (isLastWord) { currentWord.Append(lastChar); skipSpace = false; } if (lastChar == '\t') skipSpace = false; // space if (skipSpace) { currentWord.Append(lastChar); } else { // finish the word if (currentWord.Length != 0) word.Runs.Add(new Run(currentWord.ToString(), style, word)); // check width width += word.Width + word.SpaceWidth; if (width > Renderer.DisplayRect.Width) { // line is too long, make a new line width = 0; if (line.Words.Count > 1) { // if line has several words, delete the last word from the current line line.Words.RemoveAt(line.Words.Count - 1); // make new line line = new Line("", this, originalCharIndex); // and add word to it line.Words.Add(word); word.SetLine(line); width += word.Width + word.SpaceWidth; } else { line = new Line("", this, i + 1); } lines.Add(line); } // TAB symbol if (lastChar == '\t') { if (currentWord.Length == 0 && line.Words.Count > 0 && line.Words[line.Words.Count - 1].Width == 0) line.Words.RemoveAt(line.Words.Count - 1); word = new Word("\t", line); line.Words.Add(word); // adjust width width = Renderer.GetTabPosition(width); } if (!isLastWord) { word = new Word("", line); line.Words.Add(word); #if DOTNET_4 currentWord.Clear(); // .NET 2.0 doesn't have Clear() #else currentWord.Length = 0; #endif originalCharIndex = this.originalCharIndex + i + 1; skipSpace = true; } } } else { // symbol currentWord.Append(lastChar); skipSpace = false; } } return style; } public void AlignLines(bool forceJustify) { for (int i = 0; i < Lines.Count; i++) { HorzAlign align = Renderer.HorzAlign; if (align == HorzAlign.Justify && i == Lines.Count - 1 && !forceJustify) align = HorzAlign.Left; Lines[i].AlignWords(align); } } public void Draw() { foreach (Line line in Lines) { line.Draw(); } } #endregion public Paragraph(string text, AdvancedTextRenderer renderer, int originalCharIndex) { lines = new List(); this.text = text; this.renderer = renderer; this.originalCharIndex = originalCharIndex; } } /// /// Line represents single text line. It consists of one or several . /// Simple line (that does not contain tabs, html tags, and is not justified) has /// single which contains all the text. /// public class Line { #region Fields private List words; private string text; private bool hasTabs; private Paragraph paragraph; private float top; private float width; private int originalCharIndex; private List underlines; private List strikeouts; #endregion #region Properties public List Words { get { return words; } } public string Text { get { return text; } } public bool HasTabs { get { return hasTabs; } } public float Left { get { return Words.Count > 0 ? Words[0].Left : 0; } } public float Top { get { return top; } set { top = value; } } public float Width { get { return width; } } public int OriginalCharIndex { get { return originalCharIndex; } } public AdvancedTextRenderer Renderer { get { return paragraph.Renderer; } } public StyleDescriptor Style { get { if (Words.Count > 0) if (Words[0].Runs.Count > 0) return Words[0].Runs[0].Style; return null; } } public bool Last { get { return paragraph.Lines[paragraph.Lines.Count - 1] == this; } } public List Underlines { get { return underlines; } } public List Strikeouts { get { return strikeouts; } } #endregion #region Private Methods private void PrepareUnderlines(List list, FontStyle style) { list.Clear(); if (Words.Count == 0) return; if (Renderer.HtmlTags) { float left = 0; float right = 0; bool styleOn = false; foreach (Word word in Words) { foreach (Run run in word.Runs) { using (Font fnt = run.GetFont()) { if ((fnt.Style & style) > 0) { if (!styleOn) { styleOn = true; left = run.Left; } right = run.Left + run.Width; } if ((fnt.Style & style) == 0 && styleOn) { styleOn = false; list.Add(new RectangleF(left, Top, right - left, 1)); } } } } // close the style if (styleOn) list.Add(new RectangleF(left, Top, right - left, 1)); } else if ((Renderer.Font.Style & style) > 0) { float lineWidth = Width; if (Renderer.HorzAlign == HorzAlign.Justify && (!Last || (paragraph.Last && Renderer.ForceJustify))) lineWidth = Renderer.DisplayRect.Width - Renderer.SpaceWidth; list.Add(new RectangleF(Left, Top, lineWidth, 1)); } } #endregion #region Public Methods public void AlignWords(HorzAlign align) { width = 0; // handle each word if (align == HorzAlign.Justify || HasTabs || Renderer.Wysiwyg || Renderer.HtmlTags) { float left = 0; Word word = null; for (int i = 0; i < Words.Count; i++) { word = Words[i]; word.Left = left; if (word.Text == "\t") { left = Renderer.GetTabPosition(left); // remove tab Words.RemoveAt(i); i--; } else left += word.Width + word.SpaceWidth; } if (word != null) width = left - word.SpaceWidth; else width = left - Renderer.SpaceWidth; } else { // join all words into one Words.Clear(); Words.Add(new Word(text, this)); width = Words[0].Width; } float rectWidth = Renderer.DisplayRect.Width; if (align == HorzAlign.Justify) { float delta = (rectWidth - width - Renderer.SpaceWidth) / (Words.Count - 1); float curDelta = delta; for (int i = 1; i < Words.Count; i++) { words[i].Left += curDelta; curDelta += delta; } } else { float delta = 0; if (align == HorzAlign.Center) delta = (rectWidth - width) / 2; else if (align == HorzAlign.Right) delta = rectWidth - width - Renderer.SpaceWidth; for (int i = 0; i < Words.Count; i++) { words[i].Left += delta; } } // adjust X offset foreach (Word word in Words) { if (Renderer.RightToLeft) word.Left = Renderer.DisplayRect.Right - word.Left; else word.Left += Renderer.DisplayRect.Left; word.AdjustRuns(); if (Renderer.RightToLeft && Renderer.PDFMode) word.Left -= word.Width; } } public void MakeUnderlines() { PrepareUnderlines(underlines, FontStyle.Underline); PrepareUnderlines(strikeouts, FontStyle.Strikeout); } public void Draw() { foreach (Word word in Words) { word.Draw(); } if (Underlines.Count > 0 || Strikeouts.Count > 0) { using (Pen pen = new Pen(Renderer.Brush, Renderer.Font.Size * 0.1f)) { float h = Renderer.FontLineHeight; float w = h * 0.1f; // to match .net char X offset // invert offset in case of rtl if (Renderer.RightToLeft) w = -w; // emulate underline & strikeout foreach (RectangleF rect in Underlines) { Renderer.Graphics.DrawLine(pen, rect.Left + w, rect.Top + h - w, rect.Right + w, rect.Top + h - w); } h /= 2; foreach (RectangleF rect in Strikeouts) { Renderer.Graphics.DrawLine(pen, rect.Left + w, rect.Top + h, rect.Right + w, rect.Top + h); } } } } public float CalcHeight() { float height = -1; foreach (Word word in Words) { height = Math.Max(height, word.CalcHeight()); } if (height < 0) height = Renderer.LineHeight; return height; } #endregion public Line(string text, Paragraph paragraph, int originalCharIndex) { this.words = new List(); this.text = text; this.paragraph = paragraph; this.originalCharIndex = originalCharIndex; underlines = new List(); strikeouts = new List(); hasTabs = text.Contains("\t"); // split text by spaces string[] words = text.Split(' '); string textWithSpaces = ""; foreach (string word in words) { if (word == "") textWithSpaces += " "; else { // split text by tabs textWithSpaces += word; string[] tabWords = textWithSpaces.Split('\t'); foreach (string word1 in tabWords) { if (word1 == "") this.words.Add(new Word("\t", this)); else { this.words.Add(new Word(word1, this)); this.words.Add(new Word("\t", this)); } } // remove last tab this.words.RemoveAt(this.words.Count - 1); textWithSpaces = ""; } } } internal float CalcBaseLine() { float baseline = 0; foreach (Word word in Words) { baseline = Math.Max(baseline, word.CalcBaseLine()); } return baseline; } internal float CalcUnderBaseLine() { float underbaseline = 0; foreach (Word word in Words) { underbaseline = Math.Max(underbaseline, word.CalcUnderBaseLine()); } return underbaseline; } } /// /// Word represents single word. It may consist of one or several , in case /// when HtmlTags are enabled in the main class. /// public class Word { #region Fields private List runs; protected string text; private float left; private float width; internal Line line; #endregion #region Properties public string Text { get { return text; } } public float Left { get { return left; } set { left = value; } } public float Width { get { if (width == -1) { if (Renderer.HtmlTags) { width = 0; foreach (Run run in Runs) { width += run.Width; } } else { width = Renderer.Graphics.MeasureString(text, Renderer.Font, 10000, StringFormat.GenericTypographic).Width; } } return width; } } public float Top { get { return line.Top; } } public AdvancedTextRenderer Renderer { get { return line.Renderer; } } public List Runs { get { return runs; } } public float SpaceWidth { get { if (Runs == null || Runs.Count == 0) return Renderer.SpaceWidth; return Runs[Runs.Count - 1].SpaceWidth; } } #endregion #region Public Methods public void AdjustRuns() { float left = Left; foreach (Run run in Runs) { run.Left = left; if (Renderer.RightToLeft) { left -= run.Width; if (Renderer.PDFMode) run.Left -= run.Width; } else left += run.Width; } } public void SetLine(Line line) { this.line = line; } public void Draw() { if (Renderer.HtmlTags) { foreach (Run run in Runs) { run.Draw(); } } else { // don't draw underlines & strikeouts because they are drawn in the Line.Draw method Font font = Renderer.Font; bool disposeFont = false; if ((Renderer.Font.Style & FontStyle.Underline) > 0 || (Renderer.Font.Style & FontStyle.Strikeout) > 0) { font = new Font(Renderer.Font, Renderer.Font.Style & ~FontStyle.Underline & ~FontStyle.Strikeout); disposeFont = true; } if (Renderer.OutlinePen == null) { Renderer.Graphics.DrawString(Text, font, Renderer.Brush, Left, Top, Renderer.Format); } else { GraphicsPath path = new GraphicsPath(); path.AddString(Text, font.FontFamily, Convert.ToInt32(font.Style), Renderer.Graphics.DpiY * font.Size / 72, new PointF(Left - 1, Top - 1), Renderer.Format); Renderer.Graphics.FillAndDrawPath(Renderer.OutlinePen, Renderer.Brush, path); } if (disposeFont) { font.Dispose(); font = null; } } } internal float CalcHeight() { if (Renderer.HtmlTags) { float height = -1; foreach (Run run in Runs) { height = Math.Max(height, run.Height); } if (height < 0) height = Renderer.LineHeight; return height; } else { #if SKIA // we need actual height of a text because it may have font fallback with different metrics if (!string.IsNullOrEmpty(text)) { return DrawUtils.MeasureString(Renderer.Graphics.Graphics, text, Renderer.Font, Renderer.Format).Height; } #endif return Renderer.LineHeight; } } internal float CalcBaseLine() { float baseLine = 0; if (Renderer.HtmlTags) { foreach (Run run in Runs) { baseLine = Math.Max(baseLine, run.CurrentBaseLine); } return baseLine; } else { return 0; } } internal float CalcUnderBaseLine() { float underbaseLine = 0; if (Renderer.HtmlTags) { foreach (Run run in Runs) { underbaseLine = Math.Max(underbaseLine, run.CurrentUnderBaseLine); } return underbaseLine; } else { return 0; } } #endregion public Word(string text, Line line) { this.text = text; runs = new List(); this.line = line; width = -1; } } /// /// Represents character placement. /// public enum BaseLine { Normal, Subscript, Superscript } /// /// Represents a style used in HtmlTags mode. /// public class StyleDescriptor { #region Fields private FontStyle fontStyle; private Color color; private BaseLine baseLine; private string font; private float size; #endregion #region Properties public FontStyle FontStyle { get { return fontStyle; } set { fontStyle = value; } } public string Font { get { return font; } set { font = value; } } public float Size { get { return size; } set { size = value; } } public Color Color { get { return color; } set { color = value; } } public BaseLine BaseLine { get { return baseLine; } set { baseLine = value; } } #endregion #region Public Methods public override string ToString() { string result = ""; if ((FontStyle & FontStyle.Bold) != 0) result += ""; if ((FontStyle & FontStyle.Italic) != 0) result += ""; if ((FontStyle & FontStyle.Underline) != 0) result += ""; if ((FontStyle & FontStyle.Strikeout) != 0) result += ""; if (BaseLine == BaseLine.Subscript) result += ""; if (BaseLine == BaseLine.Superscript) result += ""; result += " /// Represents sequence of characters that have the same . /// public class Run { #region Fields protected string text; private StyleDescriptor style; protected Word word; private float left; protected float width; protected float lineHeight; protected float fontLineHeight; private float baseLine; protected float underBaseLine; protected float spaceWidth; #endregion #region Properties public string Text { get { return text; } } public StyleDescriptor Style { get { return style; } } public AdvancedTextRenderer Renderer { get { return word.Renderer; } } public float Left { get { return left; } set { left = value; } } public float LineHeight { get { if (lineHeight == 0) { if (style.Font == null && style.Size <= 0) lineHeight = Renderer.LineHeight; else lineHeight = GetFont().GetHeight(Renderer.Graphics.Graphics); } return lineHeight; } } virtual public float CurrentBaseLine { get { if (baseLine < 0) { Font ff = GetFont(); float lineSpace = ff.FontFamily.GetLineSpacing(Style.FontStyle); float ascent = ff.FontFamily.GetCellAscent(Style.FontStyle); baseLine = FontLineHeight * ascent / lineSpace; underBaseLine = FontLineHeight - baseLine; } return baseLine; } } virtual public float CurrentUnderBaseLine { get { if (underBaseLine < 0) { Font ff = GetFont(); float lineSpace = ff.FontFamily.GetLineSpacing(Style.FontStyle); float ascent = ff.FontFamily.GetCellAscent(Style.FontStyle); baseLine = FontLineHeight * ascent / lineSpace; underBaseLine = FontLineHeight - baseLine; } return baseLine; } } public float FontLineHeight { get { if (fontLineHeight == 0) { if (style.Font == null && style.Size <= 0) fontLineHeight = Renderer.FontLineHeight; else fontLineHeight = GetFont().GetHeight(Renderer.Graphics.Graphics); } return fontLineHeight; } } public virtual float Top { get { float baseLine = 0; if (Style.BaseLine == BaseLine.Subscript) baseLine += FontLineHeight * 0.45f; else if (Style.BaseLine == BaseLine.Superscript) baseLine -= FontLineHeight * 0.15f; return word.Top + word.line.CalcBaseLine() - CurrentBaseLine + baseLine; } } virtual public float Width { get { return width; } } virtual public float Height { get { return LineHeight; } } public float SpaceWidth { get { if (spaceWidth < 0) { spaceWidth = CalculateSpaceSize(Renderer.Graphics, GetFont());// Renderer.Graphics.MeasureString(" ", GetFont()).Width; } return spaceWidth; } } #endregion #region Private Methods private Font GetFont(bool disableUnderlinesStrikeouts) { float fontSize = Renderer.Font.Size; if (Style.Size != 0) fontSize = Style.Size; if (Style.BaseLine != BaseLine.Normal) fontSize *= 0.6f; FontStyle fontStyle = Style.FontStyle; if (disableUnderlinesStrikeouts) fontStyle = fontStyle & ~FontStyle.Underline & ~FontStyle.Strikeout; if (Style.Font != null) return new Font(Style.Font, fontSize, fontStyle); return new Font(Renderer.Font.FontFamily, fontSize, fontStyle); } #endregion #region Public Methods public Font GetFont() { return GetFont(false); } public Brush GetBrush() { return new SolidBrush(Style.Color); } public virtual void Draw() { using (Font font = GetFont(true)) using (Brush brush = GetBrush()) { Renderer.Graphics.DrawString(text, font, brush, Left, Top, Renderer.Format); } } #endregion public Run(string text, StyleDescriptor style, Word word) { baseLine = float.MinValue; underBaseLine = float.MinValue; this.text = text; this.style = new StyleDescriptor(style.FontStyle, style.Color, style.BaseLine); this.style.Font = style.Font; this.style.Size = style.Size; this.word = word; spaceWidth = -1; using (Font font = GetFont()) { width = Renderer.Graphics.MeasureString(text, font, 10000, StringFormat.GenericTypographic).Width; } } } /// /// Represents inline Image. /// internal class RunImage : Run { public Image Image { get { return image; } } override public float Width { get { if (Image == null) return base.Width; return Image.Width; } } override public float Top { get { float baseLine = 0; if (Style.BaseLine == BaseLine.Subscript) baseLine += FontLineHeight * 0.45f; else if (Style.BaseLine == BaseLine.Superscript) baseLine -= FontLineHeight * 0.15f; return word.Top + word.line.CalcBaseLine() - CurrentBaseLine + baseLine; } } override public float CurrentBaseLine { get { if (Image == null) return base.CurrentBaseLine; return Image.Height; } } override public float Height { get { if (Image == null) return base.Height; return Image.Height + word.line.CalcUnderBaseLine(); } } private Image image; override public void Draw() { if (Image == null) { base.Draw(); return; } Renderer.Graphics.DrawImage(Image, Left, Top);// (FText, font, brush, Left, Top, Renderer.Format); } public static Bitmap ResizeImage(Image image, float scale) { int width = (int)(image.Width * scale); int height = (int)(image.Height * scale); if (width == 0) width = 1; if (height == 0) height = 1; Rectangle destRect = new Rectangle(0, 0, width, height); Bitmap destImage = new Bitmap(width, height); destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); using (Graphics graphics = System.Drawing.Graphics.FromImage(destImage)) { graphics.CompositingMode = CompositingMode.SourceCopy; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; using (System.Drawing.Imaging.ImageAttributes wrapMode = new System.Drawing.Imaging.ImageAttributes()) { wrapMode.SetWrapMode(WrapMode.TileFlipXY); graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode); } } return destImage; } public RunImage(string src, string text, StyleDescriptor style, Word word) : base(text, style, word) { underBaseLine = 0; image = ResizeImage(InlineImageCache.Load(Renderer.Cache, src), Renderer.Scale); } } } /// /// Standard text renderer uses standard DrawString method to draw text. It also supports: /// - text rotation; /// - fonts with non-standard width ratio. /// In case your text is justified, or contains html tags, use the /// class instead. /// internal class StandardTextRenderer { public static void Draw(string text, IGraphics g, Font font, Brush brush, Pen outlinePen, RectangleF rect, StringFormat format, int angle, float widthRatio) { IGraphicsState state = g.Save(); g.SetClip(rect, CombineMode.Intersect); g.TranslateTransform(rect.Left + rect.Width / 2, rect.Top + rect.Height / 2); g.RotateTransform(angle); rect.X = -rect.Width / 2; rect.Y = -rect.Height / 2; if ((angle >= 90 && angle < 180) || (angle >= 270 && angle < 360)) rect = new RectangleF(rect.Y, rect.X, rect.Height, rect.Width); g.ScaleTransform(widthRatio, 1); rect.X /= widthRatio; rect.Width /= widthRatio; if (outlinePen == null) { g.DrawString(text, font, brush, rect, format); } else { GraphicsPath path = new GraphicsPath(); path.AddString(text, font.FontFamily, Convert.ToInt32(font.Style), g.DpiY * font.Size / 72, rect, format); g.FillAndDrawPath(outlinePen, brush, path); } g.Restore(state); } } /// /// Cache for rendering img tags in textobject. /// You can use only HTTP[s] protocol with absolute urls. /// public class InlineImageCache : IDisposable { #region Private Fields private WebClient client; private Dictionary items; private bool serialized; private object locker; #endregion Private Fields #region Public Properties /// /// Is serialized /// public bool Serialized { get { return serialized; } set { serialized = value; } } #endregion Public Properties #region Private Properties /// /// Get or set WebClient for downloading imgs by url /// private WebClient Client { get { if (client == null) { client = new WebClient(); } return client; } set { client = value; } } #endregion Private Properties #region Public Events /// /// Occurs before image load /// public static event EventHandler AfterLoad; /// /// Occurs after image load /// public static event EventHandler BeforeLoad; #endregion Public Events #region Public Methods /// /// Enumerates all values /// /// public IEnumerable AllItems() { List list = new List(); lock (locker) { if (items != null) { foreach (KeyValuePair item in items) { item.Value.Src = item.Key; list.Add(item.Value); } } } return list; } /// /// Return CacheItem by src /// /// Src attribute from img tag /// public CacheItem Get(string src) { CacheItem item = null; if (!Validate(src)) item = new CacheItem(); if (String.IsNullOrEmpty(src)) return item; lock (locker) { if (items == null) { items = new Dictionary(); if (item == null) item = new CacheItem(); items[src] = item; Serialized = false; } if (items.ContainsKey(src)) return items[src]; } return item; } /// /// /// /// /// public Image Load(string src) { CacheItem item = null; if (String.IsNullOrEmpty(src)) item = new CacheItem(); else lock (locker) { if (items == null) items = new Dictionary(); else if (items.ContainsKey(src)) return items[src].Image; item = new CacheItem(); if (Validate(src)) { try { if (src.StartsWith("data:")) { item.Set(src.Substring(src.IndexOf("base64,") + "base64,".Length)); } else item.Set(Client.DownloadData(src)); } catch { item.Set(""); } } items[src] = item; Serialized = false; } item.Src = src; return item.Image; } /// /// Set CacheItem by src /// /// Src attribute from img tag /// CacheItem /// public CacheItem Set(string src, CacheItem item) { if (String.IsNullOrEmpty(src)) return new CacheItem(); lock (locker) { if (items == null) items = new Dictionary(); if (!Validate(src)) item = new CacheItem(); items[src] = item; Serialized = false; } item.Src = src; return item; } /// /// Validate src attribute from image /// /// Src attribute from img tag /// return true if src is valid public bool Validate(string src) { if (String.IsNullOrEmpty(src)) return false; src = src.ToLower(); if (src.StartsWith("http://")) return true; if (src.StartsWith("https://")) return true; if (src.StartsWith("data:") && src.IndexOf("base64,") > 0) return true; return false; } #endregion Public Methods #region Internal Methods static internal Image Load(InlineImageCache cache, string src) { LoadEventArgs args = new LoadEventArgs(cache, src); if (BeforeLoad != null) BeforeLoad(null, args); Image result = null; if (!args.Handled) result = cache.Load(src); args.Handled = false; if (AfterLoad != null) AfterLoad(null, args); if (args.Handled) return cache.Get(src).Image; return result; } #endregion Internal Methods #region Public Constructors /// public InlineImageCache() { locker = new object(); client = null; } /// /// /// ~InlineImageCache() { Dispose(false); } #endregion Public Constructors #region Public Classes /// /// Item of image cache Dictionary /// public class CacheItem : IDisposable { #region Private Fields private string base64; private bool error; private Image image; //private int FId; private string src; private byte[] stream; #endregion Private Fields #region Public Properties /// /// Get Base64 string /// public string Base64 { get { try {//For strange img tag if (base64 != null) return base64; if (stream != null) { base64 = Convert.ToBase64String(stream); return base64; } if (image != null) { using (MemoryStream ms = new MemoryStream()) { image.Save(ms, System.Drawing.Imaging.ImageFormat.Png); ms.Flush(); stream = ms.ToArray(); } base64 = Convert.ToBase64String(stream); return base64; } } catch { } GetErrorImage(); return ""; } } /// /// Return true if has some error with Image /// public bool Error { get { return error; } } /// /// Get Image /// public Image Image { get { try {//for strange img tag if (image != null) return image; if (stream != null) { MemoryStream ms = new MemoryStream(stream); image = Bitmap.FromStream(ms); return image; } if (base64 != null) { this.stream = Convert.FromBase64String(base64); MemoryStream ms = new MemoryStream(stream); image = Bitmap.FromStream(ms); return image; } } catch { } return GetErrorImage(); } } /// /// Get byte array /// public byte[] Stream { get { if (stream != null) return stream; if (base64 != null) { stream = Convert.FromBase64String(base64); return stream; } if (image != null) { using (MemoryStream ms = new MemoryStream()) { image.Save(ms, System.Drawing.Imaging.ImageFormat.Png); ms.Flush(); stream = ms.ToArray(); } return stream; } return new byte[0]; } } #endregion Public Properties #region Internal Properties internal string Src { get { return src; } set { src = value; } } #endregion Internal Properties #region Public Methods /// /// Return error image and set true to error property /// /// public Image GetErrorImage() { error = true; base64 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAFdJREFUOE9jbGlq+c9AIqipq2GEawEZQAo4dvgYqoXD0QAGhv9ATyKCBY1PXBjANKEbBjSWOANA9mPRDBImzgCKXECVMMCTsojzwtAzAOQvUjCJmRe/cgDt6ZAkZx23LwAAAABJRU5ErkJggg=="; src = "data:image/png;base64," + base64; stream = Convert.FromBase64String(base64); using (MemoryStream ms = new MemoryStream(stream)) image = Bitmap.FromStream(ms); return image; } /// /// Set value for cache item /// /// Image encoded base64 string public void Set(string base64) { this.base64 = base64; image = null; stream = null; } /// /// Set value for cache item /// /// Image public void Set(Image img) { base64 = null; image = img; stream = null; } /// /// Set value for cache item /// /// Image public void Set(byte[] arr) { base64 = null; image = null; stream = arr; } #region IDisposable Support private bool disposedValue = false; // To detect redundant calls /// /// /// /// protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { if (image != null) { image.Dispose(); image = null; } // TODO: dispose managed state (managed objects). } // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. // TODO: set large fields to null. disposedValue = true; } } // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. // ~CacheItem() { // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. // Dispose(false); // } // This code added to correctly implement the disposable pattern. public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); // TODO: uncomment the following line if the finalizer is overridden above. // GC.SuppressFinalize(this); } #endregion #endregion Public Methods } /// /// WebClientEventArgs /// public class LoadEventArgs : EventArgs { #region Private Fields private InlineImageCache cache; private bool handled; private string source; #endregion Private Fields #region Public Properties /// /// Gets a cache /// public InlineImageCache Cache { get { return cache; } } /// /// Gets or sets a value indicating whether the event was handled. /// public bool Handled { get { return handled; } set { handled = value; } } /// /// Gets or sets a url from src attribue of img tag /// public string Source { get { return source; } set { source = value; } } #endregion Public Properties #region Internal Constructors internal LoadEventArgs(InlineImageCache c, string src) { cache = c; source = src; handled = false; } #endregion Internal Constructors } #region IDisposable Support private bool disposedValue = false; // To detect redundant calls /// /// /// /// protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects). } if (this.items != null) { Dictionary items = this.items; this.items = null; foreach (CacheItem item in items.Values) item.Dispose(); } // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. // TODO: set large fields to null. disposedValue = true; } } // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. // ~InlineImageCache() { // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. // Dispose(false); // } // This code added to correctly implement the disposable pattern. public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); // TODO: uncomment the following line if the finalizer is overridden above. // GC.SuppressFinalize(this); } #endregion #endregion Public Classes } }