diff --git a/OpenNest.Tests/Reporting/ReportPdf.cs b/OpenNest.Tests/Reporting/ReportPdf.cs index b33b600..ed6c425 100644 --- a/OpenNest.Tests/Reporting/ReportPdf.cs +++ b/OpenNest.Tests/Reporting/ReportPdf.cs @@ -28,22 +28,36 @@ public static class ReportPdf } } - /// Layout text of one page; skipped where poppler-utils is not installed. + /// Visual-row text of one page; bbox coordinates avoid platform-specific -layout order. public static string Text(string path, int page) => - Regex.Replace(Run("pdftotext", "-layout", "-f", Page(page), "-l", Page(page), path, "-"), "[ \t]+\n", "\n"); + LayoutPage(Run("pdftotext", "-bbox", "-f", Page(page), "-l", Page(page), path, "-")); - /// Every page's layout text, in order. + /// Every page's visual-row text, in order. public static string[] Pages(string path) { - var pages = Run("pdftotext", "-layout", path, "-").Split('\f'); - // pdftotext terminates the last page with a form feed. - return pages.Take(pages.Length - 1).Select(page => Regex.Replace(page, "[ \t]+\n", "\n")).ToArray(); + var html = Run("pdftotext", "-bbox", path, "-"); + return Regex.Matches(html, @"]*>.*?", RegexOptions.Singleline) + .Select(match => LayoutPage(match.Value)).ToArray(); + } + + private static string LayoutPage(string html) + { + var rows = new List<(double Top, List<(double Left, string Text)> Words)>(); + foreach (var word in ParseWords(html).OrderBy(word => word.Top).ThenBy(word => word.Left)) + { + if (rows.Count == 0 || System.Math.Abs(word.Top - rows[^1].Top) > 1.5) + rows.Add((word.Top, [])); + rows[^1].Words.Add((word.Left, word.Text)); + } + return string.Join("\n", rows.Select(row => string.Join(" ", row.Words.OrderBy(word => word.Left).Select(word => word.Text)))) + "\n"; } /// Words with page-space boxes (points, Y down). - public static List<(double Left, double Top, double Right, double Bottom, string Text)> Words(string path, int page) + public static List<(double Left, double Top, double Right, double Bottom, string Text)> Words(string path, int page) => + ParseWords(Run("pdftotext", "-bbox", "-f", Page(page), "-l", Page(page), path, "-")); + + private static List<(double Left, double Top, double Right, double Bottom, string Text)> ParseWords(string xml) { - var xml = Run("pdftotext", "-bbox", "-f", Page(page), "-l", Page(page), path, "-"); return Regex.Matches(xml, "([^<]*)") .Select(match => (Number(match.Groups[1].Value), Number(match.Groups[2].Value), Number(match.Groups[3].Value), Number(match.Groups[4].Value), System.Net.WebUtility.HtmlDecode(match.Groups[5].Value))) diff --git a/docs/nest-reports.md b/docs/nest-reports.md index 5364af6..92113d7 100644 --- a/docs/nest-reports.md +++ b/docs/nest-reports.md @@ -131,8 +131,9 @@ dotnet test OpenNest.Tests/OpenNest.Tests.csproj --filter FullyQualifiedName~Rep ``` The writer tests read page sizes and content streams with PDFsharp. Text -assertions extract with Poppler's `pdftotext -layout` and are skipped, not -passed, when poppler-utils is absent. For manual inspection also use `pdfinfo`, +assertions use Poppler's `pdftotext -bbox` coordinates to rebuild visual rows; +`-layout` can return columns in a different order on Windows. They are skipped, +not passed, when poppler-utils is absent. For manual inspection also use `pdfinfo`, `pdffonts` and `pdftoppm -png` (Linux verification baseline: poppler-utils 24.02.0). Confirm page sizes, quantity rows, page X of Y, embedded fonts, native vector curves, unfilled holes and visible tab gaps with no connecting stroke.