2018-11-25 04:51:27 +08:00
|
|
|
|
namespace UglyToad.PdfPig.Content
|
|
|
|
|
{
|
2020-01-16 23:43:01 +08:00
|
|
|
|
using Core;
|
2018-11-25 04:51:27 +08:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2020-01-23 04:42:27 +08:00
|
|
|
|
using System.Linq;
|
2019-08-25 22:06:37 +08:00
|
|
|
|
using System.Text;
|
2020-01-23 04:42:27 +08:00
|
|
|
|
using UglyToad.PdfPig.Geometry;
|
2018-11-25 04:51:27 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// A word.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class Word
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The text of the word.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string Text { get; }
|
|
|
|
|
|
2019-05-13 02:34:00 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// The text direction of the word.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public TextDirection TextDirection { get; }
|
|
|
|
|
|
2018-11-25 04:51:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// The rectangle completely containing the word.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public PdfRectangle BoundingBox { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The name of the font for the word.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string FontName { get; }
|
|
|
|
|
|
2019-05-13 02:34:00 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// The letters contained in the word.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public IReadOnlyList<Letter> Letters { get; }
|
|
|
|
|
|
2018-11-25 04:51:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Create a new <see cref="Word"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="letters">The letters contained in the word.</param>
|
|
|
|
|
public Word(IReadOnlyList<Letter> letters)
|
|
|
|
|
{
|
|
|
|
|
if (letters == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(letters));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (letters.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException("Empty letters provided.", nameof(letters));
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-13 02:34:00 +08:00
|
|
|
|
Letters = letters;
|
|
|
|
|
|
2020-01-16 22:43:06 +08:00
|
|
|
|
var tempTextDirection = letters[0].TextDirection;
|
2020-01-21 21:07:07 +08:00
|
|
|
|
if (tempTextDirection != TextDirection.Other)
|
2020-01-16 22:43:06 +08:00
|
|
|
|
{
|
2020-01-21 21:07:07 +08:00
|
|
|
|
foreach (var letter in letters)
|
|
|
|
|
{
|
|
|
|
|
if (letter.TextDirection != tempTextDirection)
|
|
|
|
|
{
|
|
|
|
|
tempTextDirection = TextDirection.Other;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-16 22:43:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 23:43:01 +08:00
|
|
|
|
Tuple<string, PdfRectangle> data;
|
2020-01-16 22:43:06 +08:00
|
|
|
|
|
|
|
|
|
switch (tempTextDirection)
|
|
|
|
|
{
|
|
|
|
|
case TextDirection.Horizontal:
|
|
|
|
|
data = GetBoundingBoxH(letters);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case TextDirection.Rotate180:
|
|
|
|
|
data = GetBoundingBox180(letters);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case TextDirection.Rotate90:
|
|
|
|
|
data = GetBoundingBox90(letters);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case TextDirection.Rotate270:
|
|
|
|
|
data = GetBoundingBox270(letters);
|
|
|
|
|
break;
|
|
|
|
|
|
2020-01-17 19:33:59 +08:00
|
|
|
|
case TextDirection.Other:
|
2020-01-16 22:43:06 +08:00
|
|
|
|
default:
|
2020-01-17 19:33:59 +08:00
|
|
|
|
data = GetBoundingBoxOther(letters);
|
2020-01-16 22:43:06 +08:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text = data.Item1;
|
|
|
|
|
BoundingBox = data.Item2;
|
|
|
|
|
|
|
|
|
|
FontName = letters[0].FontName;
|
|
|
|
|
TextDirection = tempTextDirection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Bounding box
|
2020-01-16 23:43:01 +08:00
|
|
|
|
private Tuple<string, PdfRectangle> GetBoundingBoxH(IReadOnlyList<Letter> letters)
|
2020-01-16 22:43:06 +08:00
|
|
|
|
{
|
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
var minX = double.MaxValue;
|
|
|
|
|
var maxX = double.MinValue;
|
|
|
|
|
var minY = double.MaxValue;
|
|
|
|
|
var maxY = double.MinValue;
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < letters.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var letter = letters[i];
|
|
|
|
|
builder.Append(letter.Value);
|
|
|
|
|
|
|
|
|
|
if (letter.StartBaseLine.X < minX)
|
|
|
|
|
{
|
|
|
|
|
minX = letter.StartBaseLine.X;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (letter.StartBaseLine.Y < minY)
|
|
|
|
|
{
|
|
|
|
|
minY = letter.StartBaseLine.Y;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-19 18:56:59 +08:00
|
|
|
|
var right = letter.StartBaseLine.X + Math.Max(letter.Width, letter.GlyphRectangle.Width);
|
2020-01-16 22:43:06 +08:00
|
|
|
|
if (right > maxX)
|
|
|
|
|
{
|
|
|
|
|
maxX = right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (letter.GlyphRectangle.Top > maxY)
|
|
|
|
|
{
|
|
|
|
|
maxY = letter.GlyphRectangle.Top;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 23:43:01 +08:00
|
|
|
|
return new Tuple<string, PdfRectangle>(builder.ToString(), new PdfRectangle(minX, minY, maxX, maxY));
|
2020-01-16 22:43:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 23:43:01 +08:00
|
|
|
|
private Tuple<string, PdfRectangle> GetBoundingBox180(IReadOnlyList<Letter> letters)
|
2020-01-16 22:43:06 +08:00
|
|
|
|
{
|
2019-08-25 22:06:37 +08:00
|
|
|
|
var builder = new StringBuilder();
|
2018-11-25 04:51:27 +08:00
|
|
|
|
|
2019-12-22 02:09:49 +08:00
|
|
|
|
var minX = double.MaxValue;
|
2020-01-23 05:23:12 +08:00
|
|
|
|
var maxX = double.MinValue;
|
2020-01-16 22:43:06 +08:00
|
|
|
|
var maxY = double.MinValue;
|
2019-12-22 02:09:49 +08:00
|
|
|
|
var minY = double.MaxValue;
|
2020-01-16 22:43:06 +08:00
|
|
|
|
|
|
|
|
|
for (var i = 0; i < letters.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var letter = letters[i];
|
|
|
|
|
builder.Append(letter.Value);
|
|
|
|
|
|
|
|
|
|
if (letter.StartBaseLine.X > maxX)
|
|
|
|
|
{
|
|
|
|
|
maxX = letter.StartBaseLine.X;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (letter.StartBaseLine.Y > maxY)
|
|
|
|
|
{
|
|
|
|
|
maxY = letter.StartBaseLine.Y;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-19 19:00:20 +08:00
|
|
|
|
var right = letter.StartBaseLine.X + Math.Min(letter.Width, letter.GlyphRectangle.Width);
|
2020-01-16 22:43:06 +08:00
|
|
|
|
if (right < minX)
|
|
|
|
|
{
|
|
|
|
|
minX = right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (letter.GlyphRectangle.Top < minY)
|
|
|
|
|
{
|
|
|
|
|
minY = letter.GlyphRectangle.Top;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 23:43:01 +08:00
|
|
|
|
return new Tuple<string, PdfRectangle>(builder.ToString(), new PdfRectangle(maxX, maxY, minX, minY));
|
2020-01-16 22:43:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 23:43:01 +08:00
|
|
|
|
private Tuple<string, PdfRectangle> GetBoundingBox90(IReadOnlyList<Letter> letters)
|
2020-01-16 22:43:06 +08:00
|
|
|
|
{
|
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
var minX = double.MaxValue;
|
2019-12-22 02:09:49 +08:00
|
|
|
|
var maxX = double.MinValue;
|
2020-01-16 22:43:06 +08:00
|
|
|
|
var minY = double.MaxValue;
|
2019-12-22 02:09:49 +08:00
|
|
|
|
var maxY = double.MinValue;
|
2019-08-25 22:06:37 +08:00
|
|
|
|
|
|
|
|
|
for (var i = 0; i < letters.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var letter = letters[i];
|
|
|
|
|
builder.Append(letter.Value);
|
|
|
|
|
|
2020-01-16 22:43:06 +08:00
|
|
|
|
if (letter.StartBaseLine.X < minX)
|
2019-08-25 22:06:37 +08:00
|
|
|
|
{
|
2020-01-16 22:43:06 +08:00
|
|
|
|
minX = letter.StartBaseLine.X;
|
2019-08-25 22:06:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 22:43:06 +08:00
|
|
|
|
if (letter.EndBaseLine.Y < minY)
|
2019-08-25 22:06:37 +08:00
|
|
|
|
{
|
2020-01-16 22:43:06 +08:00
|
|
|
|
minY = letter.EndBaseLine.Y;
|
2019-08-25 22:06:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-21 21:07:07 +08:00
|
|
|
|
var right = letter.StartBaseLine.X - letter.GlyphRectangle.Height;
|
2019-08-25 22:06:37 +08:00
|
|
|
|
if (right > maxX)
|
|
|
|
|
{
|
|
|
|
|
maxX = right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (letter.GlyphRectangle.Top > maxY)
|
|
|
|
|
{
|
|
|
|
|
maxY = letter.GlyphRectangle.Top;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 23:43:01 +08:00
|
|
|
|
return new Tuple<string, PdfRectangle>(builder.ToString(), new PdfRectangle(new PdfPoint(maxX, maxY),
|
2020-01-16 22:43:06 +08:00
|
|
|
|
new PdfPoint(maxX, minY),
|
|
|
|
|
new PdfPoint(minX, maxY),
|
|
|
|
|
new PdfPoint(minX, minY)));
|
|
|
|
|
}
|
2019-05-13 02:34:00 +08:00
|
|
|
|
|
2020-01-16 23:43:01 +08:00
|
|
|
|
private Tuple<string, PdfRectangle> GetBoundingBox270(IReadOnlyList<Letter> letters)
|
2020-01-16 22:43:06 +08:00
|
|
|
|
{
|
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
var minX = double.MaxValue;
|
2020-01-23 05:23:12 +08:00
|
|
|
|
var maxX = double.MinValue;
|
2020-01-16 22:43:06 +08:00
|
|
|
|
var minY = double.MaxValue;
|
|
|
|
|
var maxY = double.MinValue;
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < letters.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var letter = letters[i];
|
|
|
|
|
builder.Append(letter.Value);
|
|
|
|
|
|
|
|
|
|
if (letter.StartBaseLine.X > maxX)
|
|
|
|
|
{
|
|
|
|
|
maxX = letter.StartBaseLine.X;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (letter.StartBaseLine.Y < minY)
|
|
|
|
|
{
|
|
|
|
|
minY = letter.StartBaseLine.Y;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-17 19:03:09 +08:00
|
|
|
|
var right = letter.StartBaseLine.X - letter.GlyphRectangle.Height;
|
2020-01-16 22:43:06 +08:00
|
|
|
|
if (right < minX)
|
|
|
|
|
{
|
|
|
|
|
minX = right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (letter.GlyphRectangle.Bottom > maxY)
|
|
|
|
|
{
|
|
|
|
|
maxY = letter.GlyphRectangle.Bottom;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-16 23:43:01 +08:00
|
|
|
|
return new Tuple<string, PdfRectangle>(builder.ToString(), new PdfRectangle(new PdfPoint(minX, minY),
|
2020-01-16 22:43:06 +08:00
|
|
|
|
new PdfPoint(minX, maxY),
|
|
|
|
|
new PdfPoint(maxX, minY),
|
|
|
|
|
new PdfPoint(maxX, maxY)));
|
2018-11-25 04:51:27 +08:00
|
|
|
|
}
|
2020-01-16 23:43:01 +08:00
|
|
|
|
|
2020-01-17 19:33:59 +08:00
|
|
|
|
private Tuple<string, PdfRectangle> GetBoundingBoxOther(IReadOnlyList<Letter> letters)
|
2020-01-16 23:43:01 +08:00
|
|
|
|
{
|
2020-01-23 19:11:38 +08:00
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
|
for (var i = 0; i < letters.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
builder.Append(letters[i].Value);
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-23 04:42:27 +08:00
|
|
|
|
var points = letters.SelectMany(r => new[]
|
|
|
|
|
{
|
|
|
|
|
r.StartBaseLine,
|
|
|
|
|
r.EndBaseLine,
|
|
|
|
|
r.GlyphRectangle.TopLeft,
|
|
|
|
|
r.GlyphRectangle.TopRight
|
|
|
|
|
}).Distinct();
|
|
|
|
|
var convexHull = GeometryExtensions.GrahamScan(points).ToList();
|
2020-01-23 19:11:38 +08:00
|
|
|
|
var mbr = GeometryExtensions.ParametricPerpendicularProjection(convexHull);
|
|
|
|
|
var mbrPoints = new[] { mbr.TopLeft, mbr.TopRight, mbr.BottomLeft, mbr.BottomRight };
|
2020-01-16 23:43:01 +08:00
|
|
|
|
|
2020-01-23 19:11:38 +08:00
|
|
|
|
// Find the orientation of the minimum bounding box, using the baseline angle.
|
|
|
|
|
// This method needs improvment as the baseline angle and the bbox angle can
|
|
|
|
|
// belong to different quadrants of the unit circle.
|
2020-01-21 21:20:01 +08:00
|
|
|
|
var firstLetter = letters[0];
|
|
|
|
|
var lastLetter = letters[letters.Count - 1];
|
2020-01-21 21:07:07 +08:00
|
|
|
|
var rotation = Math.Atan2(
|
2020-01-21 21:20:01 +08:00
|
|
|
|
lastLetter.EndBaseLine.Y - firstLetter.StartBaseLine.Y,
|
|
|
|
|
lastLetter.EndBaseLine.X - firstLetter.StartBaseLine.X);
|
2020-01-16 23:43:01 +08:00
|
|
|
|
|
2020-01-23 19:11:38 +08:00
|
|
|
|
if (rotation >= -Math.PI && rotation <= -1.570796) // (-180 to -90deg)
|
|
|
|
|
{
|
|
|
|
|
var br = mbrPoints.OrderBy(p => p.X).ThenByDescending(p => p.Y).First();
|
|
|
|
|
var bl = mbrPoints.OrderByDescending(p => p.Y).ThenByDescending(p => p.X).First();
|
|
|
|
|
var tl = mbrPoints.OrderByDescending(p => p.Y).ThenBy(p => p.X).First();
|
|
|
|
|
var tr = mbrPoints.OrderBy(p => p.X).ThenBy(p => p.Y).First();
|
|
|
|
|
return new Tuple<string, PdfRectangle>(builder.ToString(), new PdfRectangle(tl, tr, bl, br));
|
|
|
|
|
}
|
|
|
|
|
else if (rotation > -1.570796 && rotation <= 0.0) // (-90deg to 0)
|
2020-01-21 21:07:07 +08:00
|
|
|
|
{
|
2020-01-23 19:11:38 +08:00
|
|
|
|
var bl = mbrPoints.OrderBy(p => p.X).ThenBy(p => p.Y).First();
|
|
|
|
|
var tl = mbrPoints.OrderByDescending(p => p.Y).ThenBy(p => p.X).First();
|
|
|
|
|
var tr = mbrPoints.OrderByDescending(p => p.X).ThenByDescending(p => p.Y).First();
|
|
|
|
|
var br = mbrPoints.OrderBy(p => p.Y).ThenByDescending(p => p.X).First();
|
|
|
|
|
return new Tuple<string, PdfRectangle>(builder.ToString(), new PdfRectangle(tl, tr, bl, br));
|
2020-01-21 21:07:07 +08:00
|
|
|
|
}
|
2020-01-23 19:11:38 +08:00
|
|
|
|
else if (rotation > 0.0 && rotation <= 1.570796) // (0 to 90deg)
|
2020-01-21 21:07:07 +08:00
|
|
|
|
{
|
2020-01-23 19:11:38 +08:00
|
|
|
|
var tl = mbrPoints.OrderBy(p => p.X).ThenByDescending(p => p.Y).First();
|
|
|
|
|
var bl = mbrPoints.OrderBy(p => p.Y).ThenBy(p => p.X).First();
|
|
|
|
|
var br = mbrPoints.OrderByDescending(p => p.X).ThenBy(p => p.Y).First();
|
|
|
|
|
var tr = mbrPoints.OrderByDescending(p => p.Y).ThenByDescending(p => p.X).First();
|
|
|
|
|
return new Tuple<string, PdfRectangle>(builder.ToString(), new PdfRectangle(tl, tr, bl, br));
|
2020-01-21 21:07:07 +08:00
|
|
|
|
}
|
2020-01-23 19:11:38 +08:00
|
|
|
|
else if (rotation > 1.570796 && rotation <= Math.PI) // (90 to 180deg)
|
2020-01-21 21:07:07 +08:00
|
|
|
|
{
|
2020-01-23 19:11:38 +08:00
|
|
|
|
var tr = mbrPoints.OrderBy(p => p.X).ThenBy(p => p.Y).First();
|
|
|
|
|
var bl = mbrPoints.OrderByDescending(p => p.X).ThenByDescending(p => p.Y).First();
|
|
|
|
|
var br = mbrPoints.OrderByDescending(p => p.Y).ThenBy(p => p.X).First();
|
|
|
|
|
var tl = mbrPoints.OrderBy(p => p.X).ThenByDescending(p => p.Y).First();
|
|
|
|
|
return new Tuple<string, PdfRectangle>(builder.ToString(), new PdfRectangle(tl, tr, bl, br));
|
2020-01-21 21:07:07 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2020-01-23 19:11:38 +08:00
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(rotation), "Word orientation not handled.");
|
2020-01-21 21:07:07 +08:00
|
|
|
|
}
|
2020-01-16 23:43:01 +08:00
|
|
|
|
}
|
2020-01-16 22:43:06 +08:00
|
|
|
|
#endregion
|
2018-11-25 04:51:27 +08:00
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
|
|
|
|
return Text;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|