vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF

This commit is contained in:
2026-08-27 06:58:22 +02:00
commit 532485a830
577 changed files with 149058 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
using PdfSharpCore.Drawing.Layout.enums;
namespace PdfSharpCore.Drawing.Layout
{
/// <summary>
/// Represents a single word.
/// </summary>
internal class Block
{
/// <summary>
/// Initializes a new instance of the <see cref="Block"/> class.
/// </summary>
/// <param name="text">The text of the block.</param>
/// <param name="type">The type of the block.</param>
/// <param name="width">The width of the text.</param>
public Block(string text, BlockType type, double width)
{
Text = text;
Type = type;
Width = width;
}
/// <summary>
/// Initializes a new instance of the <see cref="Block"/> class.
/// </summary>
/// <param name="type">The type.</param>
public Block(BlockType type)
{
Type = type;
}
/// <summary>
/// The text represented by this block.
/// </summary>
public string Text { get; set; }
/// <summary>
/// The type of the block.
/// </summary>
public BlockType Type { get; }
/// <summary>
/// The width of the text.
/// </summary>
public double Width { get; set; }
/// <summary>
/// The location relative to the upper left corner of the layout rectangle.
/// </summary>
public XPoint Location { get; set; }
/// <summary>
/// The alignment of this line.
/// </summary>
public XParagraphAlignment Alignment { get; set; }
/// <summary>
/// A flag indicating that this is the last block that fits in the layout rectangle.
/// </summary>
public bool Stop { get; set; }
/// <summary>
/// Contains information about spacing of the font
/// </summary>
public FormatterEnvironment Environment { get; set; }
/// <summary>
/// Indent of the text when a new line is started
/// </summary>
public double LineIndent { get; set; }
/// <summary>
/// Skips block for alignment justify calculation, when its the first block in line
/// </summary>
public bool SkipParagraphAlignment { get; set; }
/// <summary>
/// Links this block with the next block
/// </summary>
public bool NextBlockBelongsToMe { get; set; }
}
}
@@ -0,0 +1,13 @@
namespace PdfSharpCore.Drawing.Layout
{
internal class FormatterEnvironment
{
public XFont Font { get; set; }
public XBrush Brush { get; set; }
public double LineSpace { get; set; }
public double CyAscent { get; set; }
public double CyDescent { get; set; }
public double SpaceWidth { get; set; }
}
}
+16
View File
@@ -0,0 +1,16 @@
namespace PdfSharpCore.Drawing.Layout
{
public class TextSegment
{
public XFont Font { get; set; }
public XBrush Brush { get; set; }
public string Text { get; set; }
public double LineIndent { get; set; }
public bool SkipParagraphAlignment { get; set; }
public double LineSpace { get; set; }
public double CyAscent { get; set; }
public double CyDescent { get; set; }
public double SpaceWidth { get; set; }
}
}
@@ -0,0 +1,466 @@
#region PDFsharp - A .NET library for processing PDF
//
// Authors:
// Stefan Lange
//
// Copyright (c) 2005-2016 empira Software GmbH, Cologne Area (Germany)
//
// http://www.PdfSharp.com
// http://sourceforge.net/projects/pdfsharp
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using PdfSharpCore.Drawing.Layout.enums;
using PdfSharpCore.Pdf.IO;
namespace PdfSharpCore.Drawing.Layout
{
/// <summary>
/// Represents a very simple text formatter.
/// If this class does not satisfy your needs on formatting paragraphs I recommend to take a look
/// at MigraDoc Foundation. Alternatively you should copy this class in your own source code and modify it.
/// </summary>
public class XTextFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="XTextFormatter"/> class.
/// </summary>
public XTextFormatter(XGraphics gfx)
{
if (gfx == null)
throw new ArgumentNullException("gfx");
_gfx = gfx;
}
readonly XGraphics _gfx;
/// <summary>
/// Gets or sets the text.
/// </summary>
/// <value>The text.</value>
public string Text
{
get { return _text; }
set { _text = value; }
}
string _text;
/// <summary>
/// Gets or sets the font.
/// </summary>
public XFont Font
{
get { return _font; }
set
{
if (value == null)
throw new ArgumentNullException("Font");
_font = value;
_lineSpace = _font.GetHeight(); // old: _font.GetHeight(_gfx);
_cyAscent = _lineSpace * _font.CellAscent / _font.CellSpace;
_cyDescent = _lineSpace * _font.CellDescent / _font.CellSpace;
// HACK in XTextFormatter
_spaceWidth = _gfx.MeasureString("x x", value).Width;
_spaceWidth -= _gfx.MeasureString("xx", value).Width;
}
}
XFont _font;
double _lineSpace;
double _cyAscent;
double _cyDescent;
double _spaceWidth;
double _lineHeight;
// KillerPDF patch: removed unused _textLayout field (upstream dead code, CS0169)
/// <summary>
/// Gets or sets the bounding box of the layout.
/// </summary>
public XRect LayoutRectangle
{
get { return _layoutRectangle; }
set { _layoutRectangle = value; }
}
XRect _layoutRectangle;
/// <summary>
/// When true, ignore the height of text areas when rendering multiline strings
/// </summary>
public bool AllowVerticalOverflow { get; set; } = false;
/// <summary>
/// Gets or sets the horizontal alignment of the text.
/// </summary>
public XParagraphAlignment Alignment { get; set; } = XParagraphAlignment.Left;
/// <summary>
/// Gets or sets the vertical alignment of the text.
/// </summary>
public XVerticalAlignment VerticalAlignment { get; set; } = XVerticalAlignment.Top;
/// <summary>
/// Set vertical and horizontal alignment
/// </summary>
/// <param name="alignments"></param>
public void SetAlignment(TextFormatAlignment alignments)
{
Alignment = alignments.Horizontal;
VerticalAlignment = alignments.Vertical;
}
/// <summary>
/// Draws the text.
/// </summary>
/// <param name="text">The text to be drawn.</param>
/// <param name="font">The font.</param>
/// <param name="brush">The text brush.</param>
/// <param name="layoutRectangle">The layout rectangle.</param>
/// <param name="lineHeight">The line height.</param>
public void DrawString(string text, XFont font, XBrush brush, XRect layoutRectangle, XUnit? lineHeight = null)
{
DrawString(text, font, brush, layoutRectangle, new TextFormatAlignment()
{
Horizontal = XParagraphAlignment.Justify, Vertical = XVerticalAlignment.Top
}, lineHeight);
}
/// <summary>
/// Get the layout rectangle required.
/// </summary>
/// <param name="text">The text to be drawn.</param>
/// <param name="font">The font.</param>
/// <param name="brush">The text brush.</param>
/// <param name="layoutRectangle">The layout rectangle.</param>
/// <param name="lineHeight">The height of each line</param>
public XRect GetLayout(string text, XFont font, XBrush brush, XRect layoutRectangle,
XUnit? lineHeight = null)
{
if (text == null)
throw new ArgumentNullException("text");
if (font == null)
throw new ArgumentNullException("font");
if (brush == null)
throw new ArgumentNullException("brush");
Text = text;
Font = font;
LayoutRectangle = layoutRectangle;
_lineHeight = lineHeight?.Point ?? _lineSpace;
if (text.Length == 0)
return new XRect(layoutRectangle.Location.X, layoutRectangle.Location.Y, 0, 0);
CreateBlocks();
CreateLayout();
return _layoutRectangle;
}
/// <summary>
/// Draws the text.
/// </summary>
/// <param name="text">The text to be drawn.</param>
/// <param name="font">The font.</param>
/// <param name="brush">The text brush.</param>
/// <param name="layoutRectangle">The layout rectangle.</param>
/// <param name="alignments">The alignments.</c></param>
/// <param name="lineHeight">The height of each line.</param>
public void DrawString(string text, XFont font, XBrush brush, XRect layoutRectangle, TextFormatAlignment alignments,
XUnit? lineHeight = null)
{
if (alignments == null)
throw new ArgumentNullException(nameof(alignments));
if (text.Length == 0)
return;
GetLayout(text, font, brush, layoutRectangle, lineHeight);
SetAlignment(alignments);
double dx = layoutRectangle.Location.X;
double dy = layoutRectangle.Location.Y;
var lines = GetLines(_blocks).ToArray();
if (VerticalAlignment == XVerticalAlignment.Middle)
{
dy += (layoutRectangle.Height - _layoutRectangle.Height) / 2;
}
else if (VerticalAlignment == XVerticalAlignment.Bottom)
{
dy = layoutRectangle.Location.Y + layoutRectangle.Height - _layoutRectangle.Height + _lineHeight -
_cyDescent;
}
int count = _blocks.Count;
foreach (var line in lines)
{
var lineBlocks = line as Block[] ?? line.ToArray();
if (Alignment == XParagraphAlignment.Justify)
{
// LineBreak blocks carry no text (the Block(BlockType) ctor never sets Text) and
// CreateLayout never assigns them a Location, so they keep XPoint(0,0) and
// GetLines' GroupBy lumps them in with the first line. Drawing one dereferenced
// null and threw. They have nothing to render - skip them.
var drawable = lineBlocks.Where(b => b.Type != BlockType.LineBreak && b.Text != null).ToArray();
if (drawable.Length == 0)
continue;
var locationX = dx;
// A single-block line has no gaps to distribute; dividing by zero here yielded
// Infinity and pushed every following block off the page.
var gapSize = drawable.Length > 1
? (layoutRectangle.Width - drawable.Select(l => l.Width).Sum()) / (drawable.Length - 1)
: 0;
foreach (var block in drawable)
{
_gfx.DrawString(block.Text.Trim(), font, brush, locationX, dy + drawable.First().Location.Y, XStringFormats.TopLeft);
locationX += block.Width + gapSize;
}
}
else
{
var lineText = string.Join(" ", lineBlocks.Select(l => l.Text));
var locationX = dx;
if (Alignment == XParagraphAlignment.Center)
locationX = dx + layoutRectangle.Width / 2;
if (Alignment == XParagraphAlignment.Right)
locationX += layoutRectangle.Width;
_gfx.DrawString(lineText, font, brush, locationX, dy + lineBlocks.First().Location.Y, GetXStringFormat());
}
}
}
private static IEnumerable<IEnumerable<Block>> GetLines(List<Block> blocks)
{
return blocks.GroupBy(b => b.Location.Y);
}
void CreateBlocks()
{
_blocks.Clear();
int length = _text.Length;
bool inNonWhiteSpace = false;
int startIndex = 0, blockLength = 0;
for (int idx = 0; idx < length; idx++)
{
char ch = _text[idx];
// Treat CR and CRLF as LF
if (ch == Chars.CR)
{
if (idx < length - 1 && _text[idx + 1] == Chars.LF)
idx++;
ch = Chars.LF;
}
if (ch == Chars.LF)
{
if (blockLength != 0)
{
string token = _text.Substring(startIndex, blockLength);
_blocks.Add(new Block(token, BlockType.Text,
_gfx.MeasureString(token, _font).Width));
}
startIndex = idx + 1;
blockLength = 0;
_blocks.Add(new Block(BlockType.LineBreak));
}
else if (char.IsWhiteSpace(ch))
{
if (inNonWhiteSpace)
{
string token = _text.Substring(startIndex, blockLength);
_blocks.Add(new Block(token, BlockType.Text,
_gfx.MeasureString(token, _font).Width));
startIndex = idx + 1;
blockLength = 0;
}
else
{
blockLength++;
}
}
else
{
inNonWhiteSpace = true;
blockLength++;
}
}
if (blockLength != 0)
{
string token = _text.Substring(startIndex, blockLength);
_blocks.Add(new Block(token, BlockType.Text,
_gfx.MeasureString(token, _font).Width));
}
}
void CreateLayout()
{
double rectWidth = _layoutRectangle.Width;
double rectHeight = _layoutRectangle.Height - _cyAscent - _cyDescent;
int firstIndex = 0;
double x = 0, y = 0;
int count = _blocks.Count;
for (int idx = 0; idx < count; idx++)
{
Block block = _blocks[idx];
if (block.Type == BlockType.LineBreak)
{
if (Alignment == XParagraphAlignment.Justify)
_blocks[firstIndex].Alignment = XParagraphAlignment.Left;
HorizontalAlignLine(firstIndex, idx - 1, rectWidth);
firstIndex = idx + 1;
x = 0;
y += _lineHeight;
if (!AllowVerticalOverflow && y > rectHeight)
{
block.Stop = true;
break;
}
}
else
{
double width = block.Width;
if ((x + width <= rectWidth || x == 0) && block.Type != BlockType.LineBreak)
{
block.Location = new XPoint(x, y);
x += width + _spaceWidth;
}
else
{
HorizontalAlignLine(firstIndex, idx - 1, rectWidth);
// Begin implicit line break
firstIndex = idx;
y += _lineHeight;
if (!AllowVerticalOverflow && y > rectHeight)
{
block.Stop = true;
break;
}
block.Location = new XPoint(0, y);
x = width + _spaceWidth;
}
}
}
if (firstIndex < count && Alignment != XParagraphAlignment.Justify)
HorizontalAlignLine(firstIndex, count - 1, rectWidth);
var minY = _blocks.Min(b => b.Location.Y);
var maxY = _blocks.Max(b => b.Location.Y + _lineHeight);
var minX = _blocks.Min(b => b.Location.X);
var maxX = _blocks.Max(b => b.Location.X + b.Width);
_layoutRectangle = new XRect
{
X = minX,
Y = minY,
Height = maxY - minY,
Width = maxX - minX
};
}
/// <summary>
/// Align center, right, or justify.
/// </summary>
void HorizontalAlignLine(int firstIndex, int lastIndex, double layoutWidth)
{
XParagraphAlignment blockAlignment = _blocks[firstIndex].Alignment;
if (Alignment == XParagraphAlignment.Left || blockAlignment == XParagraphAlignment.Left)
return;
int count = lastIndex - firstIndex + 1;
if (count == 0)
return;
double totalWidth = -_spaceWidth;
for (int idx = firstIndex; idx <= lastIndex; idx++)
totalWidth += _blocks[idx].Width + _spaceWidth;
double dx = Math.Max(layoutWidth - totalWidth, 0);
//Debug.Assert(dx >= 0);
if (Alignment != XParagraphAlignment.Justify)
{
if (Alignment == XParagraphAlignment.Center)
dx /= 2;
for (int idx = firstIndex; idx <= lastIndex; idx++)
{
Block block = _blocks[idx];
block.Location += new XSize(dx, 0);
}
}
else if (count > 1) // case: justify
{
dx /= count - 1;
for (int idx = firstIndex + 1, i = 1; idx <= lastIndex; idx++, i++)
{
Block block = _blocks[idx];
block.Location += new XSize(dx * i, 0);
}
}
}
readonly List<Block> _blocks = new List<Block>();
// TODO:
// - more XStringFormat variations
// - left and right indent
// - first line indent
// - margins and paddings
// - background color
// - text background color
// - border style
// - hyphens, soft hyphens, hyphenation
// - kerning
// - change font, size, text color etc.
// - underline and strike-out variation
// - super- and sub-script
// - ...
private XStringFormat GetXStringFormat()
{
switch (Alignment)
{
case XParagraphAlignment.Center:
return XStringFormats.TopCenter;
case XParagraphAlignment.Right:
return XStringFormats.TopRight;
case XParagraphAlignment.Default:
case XParagraphAlignment.Justify:
case XParagraphAlignment.Left:
default:
return XStringFormats.TopLeft;
}
}
}
public class TextFormatAlignment
{
public XParagraphAlignment Horizontal { get; set; } = XParagraphAlignment.Left;
public XVerticalAlignment Vertical { get; set; } = XVerticalAlignment.Top;
}
}
@@ -0,0 +1,641 @@
using PdfSharpCore.Drawing.Layout.enums;
using PdfSharpCore.Pdf.IO;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PdfSharpCore.Drawing.Layout
{
public class XTextSegmentFormatter
{
private readonly XGraphics _gfx;
/// <summary>
/// Initializes a new instance of the <see cref="XTextSegmentFormatter"/> class.
/// </summary>
public XTextSegmentFormatter(XGraphics gfx)
{
if (gfx == null)
{
throw new ArgumentNullException("gfx");
}
_gfx = gfx;
}
/// <summary>
/// Gets or sets the alignment of the text.
/// </summary>
public XParagraphAlignment Alignment { get; set; }
/// <summary>
/// Draws the text.
/// </summary>
/// <param name="text">The text to be drawn.</param>
/// <param name="font">The font.</param>
/// <param name="brush">The text brush.</param>
/// <param name="layoutRectangle">The layout rectangle.</param>
public void DrawString(string text, XFont font, XBrush brush, XRect layoutRectangle)
{
var textSegments = new List<TextSegment>
{
new TextSegment { Font = font, Brush = brush, Text = text }
};
DrawString(textSegments, layoutRectangle, XStringFormats.TopLeft);
}
/// <summary>
/// Draws the text.
/// </summary>
/// <param name="text">The text to be drawn.</param>
/// <param name="font">The font.</param>
/// <param name="brush">The text brush.</param>
/// <param name="layoutRectangle">The layout rectangle.</param>
/// <param name="format">The format. Must be <c>XStringFormat.TopLeft</c></param>
public void DrawString(string text, XFont font, XBrush brush, XRect layoutRectangle, XStringFormat format)
{
var textSegments = new List<TextSegment>
{
new TextSegment { Font = font, Brush = brush, Text = text }
};
DrawString(textSegments, layoutRectangle, format);
}
/// <summary>
/// Draws the text.
/// </summary>
/// <param name="textSegments">The texts to be drawn with font and color information</param>
/// <param name="layoutRectangle">The layout rectangle.</param>
public void DrawString(IEnumerable<TextSegment> textSegments, XRect layoutRectangle)
{
DrawString(textSegments, layoutRectangle, XStringFormats.TopLeft);
}
/// <summary>
/// Draws the text.
/// </summary>
/// <param name="textSegments">The texts to be drawn with font and color information.</param>
/// <param name="layoutRectangle">The layout rectangle.</param>
/// <param name="format">The format. Must be <c>XStringFormat.TopLeft</c></param>
public void DrawString(IEnumerable<TextSegment> textSegments, XRect layoutRectangle, XStringFormat format)
{
ProcessTextSegments(
textSegments,
layoutRectangle,
format,
(block, dx, dy) => _gfx.DrawString(block.Text, block.Environment.Font, block.Environment.Brush, dx + block.Location.X, dy + block.Location.Y),
false
);
}
/// <summary>
/// Calculates the size of the given text
/// </summary>
/// <param name="text">The text to be drawn.</param>
/// <param name="font">The font.</param>
/// <param name="brush">The text brush.</param>
/// <param name="width">Max text width</param>
/// <returns></returns>
public XSize CalculateTextSize(string text, XFont font, XBrush brush, double width)
{
return CalculateTextSize(text, font, brush, width, XStringFormats.TopLeft);
}
/// <summary>
/// Calculates the size of the given text
/// </summary>
/// <param name="text">The text to be drawn.</param>
/// <param name="font">The font.</param>
/// <param name="brush">The text brush.</param>
/// <param name="width">Max text width</param>
/// <param name="format">The format. Must be <c>XStringFormat.TopLeft</param>
/// <returns></returns>
public XSize CalculateTextSize(string text, XFont font, XBrush brush, double width, XStringFormat format)
{
var textSegments = new List<TextSegment>
{
new TextSegment { Font = font, Brush = brush, Text = text }
};
return CalculateTextSize(textSegments, width, format);
}
/// <summary>
/// Calculates the size of the given text
/// </summary>
/// <param name="textSegments">The texts to be drawn with font and color information.</param>
/// <param name="width">Max text width</param>
/// <returns></returns>
public XSize CalculateTextSize(IEnumerable<TextSegment> textSegments, double width)
{
return CalculateTextSize(textSegments, width, XStringFormats.TopLeft);
}
/// <summary>
/// Calculates the size of the given text
/// </summary>
/// <param name="textSegments">The texts to be drawn with font and color information.</param>
/// <param name="width">Max text width</param>
/// <param name="format">The format. Must be <c>XStringFormat.TopLeft</param>
/// <returns></returns>
public XSize CalculateTextSize(IEnumerable<TextSegment> textSegments, double width, XStringFormat format)
{
var layoutRectangle = new XRect(0, 0, width, 100000000);
var blocks = new List<Block>();
ProcessTextSegments(textSegments, layoutRectangle, format, (block, dx, dy) => blocks.Add(block), true);
var height = blocks.Any()
? blocks.Max(b => b.Location.Y)
: 0;
var maxLineHeight = 0.0;
for (int i = blocks.Count - 1; i >= 0; i--)
{
if (blocks[i].Type == BlockType.LineBreak)
{
break;
}
maxLineHeight = Math.Max(maxLineHeight, blocks[i].Environment.LineSpace);
}
var calculatedWith = blocks.Any()
? blocks.Max(b => b.Location.X + b.Width)
: width;
if (width < calculatedWith)
{
calculatedWith = width;
}
return new XSize(calculatedWith, height + maxLineHeight);
}
private void ProcessTextSegments(IEnumerable<TextSegment> textSegments, XRect layoutRectangle, XStringFormat format, Action<Block, double, double> applyBlock, bool applyBlockIfLineBreak)
{
if (textSegments.All(ts => string.IsNullOrEmpty(ts.Text)))
{
return;
}
if (textSegments.Any(ts => ts.Font == default))
{
throw new ArgumentNullException("font");
}
if (textSegments.Any(ts => ts.Brush == default))
{
throw new ArgumentNullException("brush");
}
if (format.Alignment != XStringAlignment.Near || format.LineAlignment != XLineAlignment.Near)
{
throw new ArgumentException("Only TopLeft alignment is currently implemented.");
}
foreach (var segment in textSegments)
{
SetFontSpacings(segment);
}
var blocks = CreateBlocks(textSegments);
var blockUnits = new List<List<Block>>();
var currentBlockUnit = new List<Block>();
foreach (var block in blocks)
{
currentBlockUnit.Add(block);
if (block.Stop || block.Type == BlockType.LineBreak)
{
blockUnits.Add(currentBlockUnit);
currentBlockUnit = new List<Block>();
}
}
if (!blocks.Last().Stop && blocks.Last().Type != BlockType.LineBreak)
{
blockUnits.Add(currentBlockUnit);
}
CreateLayout(blockUnits, layoutRectangle);
for (int index = 0; index < blockUnits.Count; index++)
{
var blockUnit = blockUnits[index];
var maxCyAscend = blockUnit.Max(b => b.Environment.CyAscent);
var dx = layoutRectangle.Location.X;
var dy = layoutRectangle.Location.Y + maxCyAscend;
// Check all blocks of the current line in order to move all blocks of the next lines down,
// when the first block of the current line has not the max cy ascent of the whole line
if (!blockUnit.All(b => b.Environment.CyAscent == maxCyAscend))
{
for (var indexSiblings = index + 1; indexSiblings < blockUnits.Count; indexSiblings++)
{
blockUnits[indexSiblings].ForEach(b => b.Location += new XSize(0, maxCyAscend - blockUnit.First().Environment.CyAscent));
}
}
foreach (var block in blockUnit)
{
if (block.Stop)
{
break;
}
if (block.Type == BlockType.LineBreak && !applyBlockIfLineBreak)
{
continue;
}
applyBlock(block, dx, dy);
}
}
}
private List<Block> CreateBlocks(IEnumerable<TextSegment> textSegments)
{
var blocks = new List<Block>();
foreach (var textSegment in textSegments)
{
if (string.IsNullOrEmpty(textSegment.Text) && !(textSegment.Text ?? "").Contains(Chars.LF))
{
continue;
}
// Check whether the current block belongs to the last block
if (blocks.Any() && !textSegment.Text.StartsWith(" "))
{
blocks.Last().NextBlockBelongsToMe = true;
}
var length = textSegment.Text.Length;
var inNonWhiteSpace = false;
var startIndex = 0;
var blockLength = 0;
for (var idx = 0; idx < length; idx++)
{
char ch = textSegment.Text[idx];
// Treat CR and CRLF as LF
if (ch == Chars.CR)
{
if (idx < length - 1 && textSegment.Text[idx + 1] == Chars.LF)
{
idx++;
}
ch = Chars.LF;
}
if (ch == Chars.LF)
{
if (blockLength != 0)
{
var token = textSegment.Text.Substring(startIndex, blockLength);
var block = new Block(token, BlockType.Text, _gfx.MeasureString(token, textSegment.Font).Width);
SetFormatterEnvironment(block, textSegment);
block.LineIndent = textSegment.LineIndent;
block.SkipParagraphAlignment = textSegment.SkipParagraphAlignment;
blocks.Add(block);
}
startIndex = idx + 1;
blockLength = 0;
var lineBreakBlock = new Block(BlockType.LineBreak);
SetFormatterEnvironment(lineBreakBlock, textSegment);
blocks.Add(lineBreakBlock);
}
else if (char.IsWhiteSpace(ch))
{
if (inNonWhiteSpace)
{
var token = textSegment.Text.Substring(startIndex, blockLength).Trim();
var block = new Block(token, BlockType.Text, _gfx.MeasureString(token, textSegment.Font).Width);
SetFormatterEnvironment(block, textSegment);
block.LineIndent = textSegment.LineIndent;
block.SkipParagraphAlignment = textSegment.SkipParagraphAlignment;
blocks.Add(block);
startIndex = idx + 1;
blockLength = 0;
}
else
{
blockLength++;
}
}
else
{
inNonWhiteSpace = true;
blockLength++;
}
}
if (blockLength != 0)
{
var token = textSegment.Text.Substring(startIndex, blockLength);
var block = new Block(token, BlockType.Text, _gfx.MeasureString(token, textSegment.Font).Width);
block.LineIndent = textSegment.LineIndent;
block.SkipParagraphAlignment = textSegment.SkipParagraphAlignment;
SetFormatterEnvironment(block, textSegment);
blocks.Add(block);
}
}
return blocks;
}
private void CreateLayout(List<List<Block>> blockUnits, XRect layoutRectangle)
{
var rectWidth = layoutRectangle.Width;
var rectHeight = layoutRectangle.Height - blockUnits.First().First().Environment.CyAscent - blockUnits.Last().Last().Environment.CyDescent;
var x = 0.0;
var y = 0.0;
foreach (var blockUnit in blockUnits)
{
var count = blockUnit.Count;
var firstIndex = 0;
var currentMaxLineSpace = 0.0;
var currentMaxCyDescent = 0.0;
var currentLineBlocks = new List<Block>();
var startLineSpace = blockUnit[0].Environment.LineSpace;
var startCyDescent = blockUnit[0].Environment.CyDescent;
for (int idx = 0; idx < count; idx++)
{
var block = blockUnit[idx];
if (block.Type == BlockType.LineBreak)
{
if (Alignment == XParagraphAlignment.Justify)
{
blockUnit[firstIndex].Alignment = XParagraphAlignment.Left;
}
AlignLine(blockUnit, firstIndex, idx - 1, rectWidth);
firstIndex = idx + 1;
x = 0;
startLineSpace = (idx + 1) < count
? blockUnit[idx + 1].Environment.LineSpace
: block.Environment.LineSpace;
startCyDescent = (idx + 1) < count
? blockUnit[idx + 1].Environment.CyDescent
: block.Environment.CyDescent;
currentMaxLineSpace = startLineSpace;
currentMaxCyDescent = startCyDescent;
y += currentMaxLineSpace;
currentLineBlocks.Clear();
if (y > rectHeight)
{
block.Stop = true;
break;
}
// necessary to correctly calculate closing line breaks
block.Location = new XPoint(0, y);
}
else
{
double width = block.Width;
if (x == 0.0)
{
x += block.LineIndent;
}
if (x + width <= rectWidth || x == 0.0)
{
// if the font style is set to "underline", we don't want a underlined space character
width = RemovedLeadingSpace(block, width);
block.Location = new XPoint(x, y);
x += width;
if (!block.NextBlockBelongsToMe)
{
// The current and the next block are treated as one unit, so there is no space between them
x += block.Environment.SpaceWidth;
}
currentLineBlocks.Add(block);
currentMaxLineSpace = Math.Max(block.Environment.LineSpace, currentMaxLineSpace);
currentMaxCyDescent = Math.Max(block.Environment.CyDescent, currentMaxCyDescent);
}
else
{
// if the previous blocks are linked to the current block, all linked blocks have to be moved to the next line
while (idx > 0 && blockUnit[idx - 1].NextBlockBelongsToMe)
{
idx--;
currentLineBlocks.RemoveAt(currentLineBlocks.Count - 1);
block = blockUnit[idx];
width = block.Width;
}
AlignLine(blockUnit, firstIndex, idx - 1, rectWidth);
firstIndex = idx;
if (currentMaxLineSpace != startLineSpace)
{
y += -startLineSpace + currentMaxLineSpace;
currentLineBlocks.ForEach(b => b.Location = new XPoint(b.Location.X, y));
}
startLineSpace = block.Environment.LineSpace;
startCyDescent = block.Environment.CyDescent;
if (startLineSpace < currentMaxLineSpace)
{
var cyDescentDiff = currentMaxCyDescent - startCyDescent;
y += cyDescentDiff;
}
currentMaxLineSpace = startLineSpace;
currentMaxCyDescent = startCyDescent;
y += currentMaxLineSpace;
currentLineBlocks.Clear();
if (y > rectHeight)
{
block.Stop = true;
break;
}
// A new line must not start with a space character
width = RemovedLeadingSpace(block, width);
block.Location = new XPoint(block.LineIndent, y);
x = block.LineIndent + width;
if (!block.NextBlockBelongsToMe)
{
// The current and the next block are treated as one unit, so there is no space between them
x += block.Environment.SpaceWidth;
}
currentLineBlocks.Add(block);
}
}
}
if (firstIndex < count && Alignment != XParagraphAlignment.Justify)
{
AlignLine(blockUnit, firstIndex, count - 1, rectWidth);
}
}
}
private double RemovedLeadingSpace(Block block, double width)
{
while (block.Text.StartsWith(" "))
{
block.Text = block.Text.Substring(1);
block.Width -= block.Environment.SpaceWidth;
width -= block.Environment.SpaceWidth;
}
return width;
}
/// <summary>
/// Align center, right, or justify.
/// </summary>
private void AlignLine(IList<Block> blockUnit, int firstIndex, int lastIndex, double layoutWidth)
{
var firstBlock = blockUnit[firstIndex];
var blockAlignment = firstBlock.Alignment;
if (Alignment == XParagraphAlignment.Left || blockAlignment == XParagraphAlignment.Left)
{
return;
}
var count = lastIndex - firstIndex + 1;
if (count == 0)
{
return;
}
var totalWidth = firstBlock.LineIndent;
if (Alignment == XParagraphAlignment.Justify)
{
// Skip not movable leading blocks
for (int idx = firstIndex; idx <= lastIndex; idx++)
{
if (!blockUnit[idx].SkipParagraphAlignment && !blockUnit[idx].NextBlockBelongsToMe)
{
firstIndex = idx;
break;
}
else
{
count--;
layoutWidth -= blockUnit[idx].Width + (blockUnit[idx].NextBlockBelongsToMe ? 0 : blockUnit[idx].Environment.SpaceWidth);
}
}
}
// Remove not movable blocks from space calculation
for (int idx = firstIndex; idx <= lastIndex; idx++)
{
totalWidth += blockUnit[idx].Width + (blockUnit[idx].NextBlockBelongsToMe ? 0 : blockUnit[idx].Environment.SpaceWidth);
if (idx == lastIndex)
{
totalWidth -= (blockUnit[idx].NextBlockBelongsToMe ? 0 : blockUnit[idx].Environment.SpaceWidth);
}
if (blockUnit[idx].NextBlockBelongsToMe)
{
count--;
}
}
var dx = Math.Max(layoutWidth - totalWidth, 0);
if (Alignment != XParagraphAlignment.Justify)
{
// right or center
if (Alignment == XParagraphAlignment.Center)
{
dx /= 2;
}
for (int idx = firstIndex; idx <= lastIndex; idx++)
{
var block = blockUnit[idx];
block.Location += new XSize(dx, 0);
}
}
else if (count > 1) // case: justify
{
dx /= count - 1;
var spaceCounter = 1;
for (int idx = firstIndex + 1; idx <= lastIndex; idx++)
{
var block = blockUnit[idx];
block.Location += new XSize(dx * spaceCounter, 0);
if (!block.NextBlockBelongsToMe)
{
spaceCounter++;
}
}
}
}
private void SetFormatterEnvironment(Block block, TextSegment textSegment)
{
block.Alignment = Alignment;
block.Environment = new FormatterEnvironment
{
Font = textSegment.Font,
Brush = textSegment.Brush,
LineSpace = textSegment.LineSpace,
CyAscent = textSegment.CyAscent,
CyDescent = textSegment.CyDescent,
SpaceWidth = textSegment.SpaceWidth
};
}
private void SetFontSpacings(TextSegment segment)
{
if (segment.Font == null)
{
throw new ArgumentNullException("Font");
}
segment.LineSpace = segment.Font.GetHeight();
segment.CyAscent = segment.LineSpace * segment.Font.CellAscent / segment.Font.CellSpace;
segment.CyDescent = segment.LineSpace * segment.Font.CellDescent / segment.Font.CellSpace;
// HACK in XTextSegmentFormatter
segment.SpaceWidth = _gfx.MeasureString("x x", segment.Font).Width;
segment.SpaceWidth -= _gfx.MeasureString("xx", segment.Font).Width;
}
// TODO:
// - more XStringFormat variations
// - calculate bounding box
// - left and right indent
// - first line indent
// - margins and paddings
// - background color
// - text background color
// - border style
// - hyphens, soft hyphens, hyphenation
// - kerning
// - line spacing
// - underline and strike-out variation
// - super- and sub-script
// - ...
}
}
@@ -0,0 +1,7 @@
namespace PdfSharpCore.Drawing.Layout.enums
{
internal enum BlockType
{
Text, Space, Hyphen, LineBreak,
}
}
@@ -0,0 +1,62 @@
#region PDFsharp - A .NET library for processing PDF
//
// Authors:
// Stefan Lange
//
// Copyright (c) 2005-2016 empira Software GmbH, Cologne Area (Germany)
//
// http://www.PdfSharp.com
// http://sourceforge.net/projects/pdfsharp
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#endregion
namespace PdfSharpCore.Drawing.Layout
{
/// <summary>
/// Specifies the alignment of a paragraph.
/// </summary>
public enum XParagraphAlignment
{
/// <summary>
/// Default alignment, typically left alignment.
/// </summary>
Default,
/// <summary>
/// The paragraph is rendered left aligned.
/// </summary>
Left,
/// <summary>
/// The paragraph is rendered centered.
/// </summary>
Center,
/// <summary>
/// The paragraph is rendered right aligned.
/// </summary>
Right,
/// <summary>
/// The paragraph is rendered justified.
/// </summary>
Justify,
}
}
@@ -0,0 +1,9 @@
namespace PdfSharpCore.Drawing.Layout.enums
{
public enum XVerticalAlignment
{
Top,
Middle,
Bottom,
}
}