diff --git a/src/UglyToad.PdfPig.Core/PdfSubpath.cs b/src/UglyToad.PdfPig.Core/PdfSubpath.cs index 39414090..84669f4b 100644 --- a/src/UglyToad.PdfPig.Core/PdfSubpath.cs +++ b/src/UglyToad.PdfPig.Core/PdfSubpath.cs @@ -311,7 +311,7 @@ /// /// Converts from the path command to an SVG string representing the path operation. /// - void WriteSvg(StringBuilder builder); + void WriteSvg(StringBuilder builder, double height); } /// @@ -326,7 +326,7 @@ } /// - public void WriteSvg(StringBuilder builder) + public void WriteSvg(StringBuilder builder, double height) { builder.Append("Z "); } @@ -373,9 +373,9 @@ } /// - public void WriteSvg(StringBuilder builder) + public void WriteSvg(StringBuilder builder, double height) { - builder.Append("M ").Append(Location.X).Append(' ').Append(Location.Y).Append(' '); + builder.Append($"M {Location.X} {height - Location.Y} "); } /// @@ -439,9 +439,9 @@ } /// - public void WriteSvg(StringBuilder builder) + public void WriteSvg(StringBuilder builder, double height) { - builder.AppendFormat("L {0} {1} ", To.X, To.Y); + builder.Append($"L {To.X} {height - To.Y} "); } /// @@ -543,10 +543,9 @@ } /// - public void WriteSvg(StringBuilder builder) + public void WriteSvg(StringBuilder builder, double height) { - builder.AppendFormat("C {0} {1}, {2} {3}, {4} {5} ", FirstControlPoint.X, FirstControlPoint.Y, SecondControlPoint.X, SecondControlPoint.Y, - EndPoint.X, EndPoint.Y); + builder.Append($"C {FirstControlPoint.X} { height - FirstControlPoint.Y}, { SecondControlPoint.X} {height - SecondControlPoint.Y}, {EndPoint.X} {height - EndPoint.Y} "); } private bool TrySolveQuadratic(bool isX, double currentMin, double currentMax, out (double min, double max) solutions) diff --git a/src/UglyToad.PdfPig.DocumentLayoutAnalysis/Export/SvgTextExporter.cs b/src/UglyToad.PdfPig.DocumentLayoutAnalysis/Export/SvgTextExporter.cs new file mode 100644 index 00000000..f7f6bd56 --- /dev/null +++ b/src/UglyToad.PdfPig.DocumentLayoutAnalysis/Export/SvgTextExporter.cs @@ -0,0 +1,209 @@ +namespace UglyToad.PdfPig.DocumentLayoutAnalysis.Export +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using UglyToad.PdfPig.Content; + using UglyToad.PdfPig.Graphics; + using UglyToad.PdfPig.Graphics.Colors; + using UglyToad.PdfPig.Graphics.Core; + + /// + /// + /// + public class SvgTextExporter : ITextExporter + { + static readonly int rounding = 4; + + /// + /// + /// + /// + /// + public string Get(Page page) + { + var builder = new StringBuilder($""); + + var paths = page.ExperimentalAccess.Paths; + foreach (var path in paths) + { + if (path.IsClipping) + { + //var svg = PathToSvg(path, page.Height); + //svg = svg.Replace("stroke='black'", "stroke='yellow'"); + //builder.Append(svg); + } + else + { + builder.Append(PathToSvg(path, page.Height)); + } + } + + foreach (var letter in page.Letters) + { + builder.Append(LetterToSvg(letter, page.Height)); + } + + builder.Append(""); + return builder.ToString(); + } + + static readonly Dictionary _fonts = new Dictionary() + { + { "ArialMT", "Arial Rounded MT Bold" } + }; + + private static string LetterToSvg(Letter l, double height) + { + string fontFamily = GetFontFamily(l.FontName, out string style, out string weight); + string rotation = ""; + if (l.GlyphRectangle.Rotation != 0) + { + rotation = $" transform='rotate({Math.Round(-l.GlyphRectangle.Rotation, rounding)} {Math.Round(l.GlyphRectangle.BottomLeft.X, rounding)},{Math.Round(height - l.GlyphRectangle.TopLeft.Y, rounding)})'"; + } + + string fontSize = l.FontSize != 1 ? $"font-size='{l.FontSize.ToString("0")}'" : $"style='font-size:{Math.Round(l.GlyphRectangle.Height, 2)}px'"; + + return $"{l.Value}"; + } + + private static string GetFontFamily(string fontName, out string style, out string weight) + { + style = "normal"; // normal | italic | oblique + weight = "normal"; // normal | bold | bolder | lighter + + // remove subset prefix + if (fontName.Contains('+')) + { + if (fontName.Length > 7 && fontName[6] == '+') + { + var split = fontName.Split('+'); + if (split[0].All(c => char.IsUpper(c))) + { + fontName = split[1]; + } + } + } + + if (fontName.Contains('-')) + { + var infos = fontName.Split('-'); + fontName = infos[0]; + + for (int i = 1; i < infos.Length; i++) + { + string infoLower = infos[i].ToLowerInvariant(); + if (infoLower.Contains("light")) + { + weight = "lighter"; + } + else if (infoLower.Contains("bolder")) + { + weight = "bolder"; + } + else if (infoLower.Contains("bold")) + { + weight = "bold"; + } + + if (infoLower.Contains("italic")) + { + style = "italic"; + } + else if (infoLower.Contains("oblique")) + { + style = "oblique"; + } + } + } + + if (_fonts.ContainsKey(fontName)) fontName = _fonts[fontName]; + return fontName; + } + + private static string ColorToSvg(IColor color) + { + if (color == null) return ""; + var (r, g, b) = color.ToRGBValues(); + return $"rgb({Math.Ceiling(r * 255)},{Math.Ceiling(g * 255)},{Math.Ceiling(b * 255)})"; + } + + private static string PathToSvg(PdfPath p, double height) + { + var builder = new StringBuilder(); + foreach (var subpath in p) + { + foreach (var command in subpath.Commands) + { + command.WriteSvg(builder, height); + } + } + + if (builder.Length == 0) + { + return string.Empty; + } + + if (builder[builder.Length - 1] == ' ') + { + builder.Remove(builder.Length - 1, 1); + } + + var glyph = builder.ToString(); + + string dashArray = ""; + string capStyle = ""; + string jointStyle = ""; + string strokeColor = " stroke='none'"; + string strokeWidth = ""; + + if (p.IsStroked) + { + strokeColor = $" stroke='{ColorToSvg(p.StrokeColor)}'"; + strokeWidth = $" stroke-width='{p.LineWidth}'"; + + if (p.LineDashPattern.HasValue && p.LineDashPattern.Value.Array.Count > 0) + { + dashArray = $" stroke-dasharray='{string.Join(" ", p.LineDashPattern.Value.Array)}'"; + } + + if (p.LineCapStyle != LineCapStyle.Butt) + { + if (p.LineCapStyle == LineCapStyle.Round) + { + capStyle = " stroke-linecap='round'"; + } + else + { + capStyle = " stroke-linecap='square'"; + } + } + + if (p.LineJoinStyle != LineJoinStyle.Miter) + { + if (p.LineJoinStyle == LineJoinStyle.Round) + { + jointStyle = " stroke-linejoin='round'"; + } + else + { + jointStyle = " stroke-linejoin='bevel'"; + } + } + } + + string fillColor = " fill='none'"; + string fillRule = ""; + + if (p.IsFilled) + { + fillColor = $" fill='{ColorToSvg(p.FillColor)}'"; + //if (p.FillingRule == FillingRule.EvenOdd) fillRule = " fill-rule='evenodd'"; + } + + var path = $""; + return path; + } + } +} diff --git a/src/UglyToad.PdfPig.Tests/Fonts/CharacterPathTests.cs b/src/UglyToad.PdfPig.Tests/Fonts/CharacterPathTests.cs index 59d9505d..71efdc6d 100644 --- a/src/UglyToad.PdfPig.Tests/Fonts/CharacterPathTests.cs +++ b/src/UglyToad.PdfPig.Tests/Fonts/CharacterPathTests.cs @@ -53,9 +53,9 @@ new PdfPoint(140, 160)); var builder = new StringBuilder(); - curve.WriteSvg(builder); + curve.WriteSvg(builder, 0); - Assert.Equal("C 75 30, 215 115, 140 160 ", builder.ToString()); + Assert.Equal("C 75 -30, 215 -115, 140 -160 ", builder.ToString()); } } } diff --git a/src/UglyToad.PdfPig.Tests/Fonts/Type1/Type1FontParserTests.cs b/src/UglyToad.PdfPig.Tests/Fonts/Type1/Type1FontParserTests.cs index b1693255..dee960b1 100644 --- a/src/UglyToad.PdfPig.Tests/Fonts/Type1/Type1FontParserTests.cs +++ b/src/UglyToad.PdfPig.Tests/Fonts/Type1/Type1FontParserTests.cs @@ -63,7 +63,7 @@ foreach (var charString in result.CharStrings.CharStrings) { Assert.True(result.CharStrings.TryGenerate(charString.Key, out var path)); - builder.AppendLine(path.ToFullSvg()); + builder.AppendLine(path.ToFullSvg(double.NaN)); // TODO } builder.Append(""); diff --git a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs index e1147111..5e629f36 100644 --- a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs @@ -888,12 +888,12 @@ return new[] {x1, x2, x3}; } - internal static string ToSvg(this PdfSubpath p) + internal static string ToSvg(this PdfSubpath p, double height) { var builder = new StringBuilder(); foreach (var pathCommand in p.Commands) { - pathCommand.WriteSvg(builder); + pathCommand.WriteSvg(builder, height); } if (builder.Length == 0) @@ -909,15 +909,15 @@ return builder.ToString(); } - internal static string ToFullSvg(this PdfSubpath p) + internal static string ToFullSvg(this PdfSubpath p, double height) { - string BboxToRect(PdfRectangle box, string stroke) + static string BboxToRect(PdfRectangle box, string stroke) { var overallBbox = $""; return overallBbox; } - var glyph = p.ToSvg(); + var glyph = p.ToSvg(height); var bbox = p.GetBoundingRectangle(); var bboxes = new List();