Slice 2 of the nest report plan: pagination, overflow and dense-label
coverage on top of the Slice 1 one-plate library.
- NestPdfWriter: general multi-plate pagination. Summary/plate tables
continue across pages with repeated (HeadingFormat) header rows; notes
flow as an ordinary paragraph instead of a bounded cell; page headers
are wrapped and sized into the top margin; drawing areas are located
per page via DocumentRenderer.GetRenderInfoFromPage.
- ReportText: lossless pre-wrapping (MigraDoc clips an over-tall row and
lets an unbroken token overflow a narrow cell without warning), capped
table-cell line counts, plate-range compression ("1-2, 4"), and
A/B/.../AA map-grid row names.
- NestReportDiagram: label placement centers each part ID on its
PolyLabel pole (matching PlateView's LayoutPart), computed on a
placement-independent quantized copy so identical parts share one
label position. Parts whose ID cannot fit legibly at overview scale
get a lettered/numbered map grid and a zoomed, framed detail page per
crowded cell; the writer fails with plate/part/ID when even that
cannot place a label, or a plate would need more than 24 detail views.
- Tests: NestPdfLayoutTests (multi-plate totals/ranges/same-named
references, table continuation with no lost rows, long name/notes
wrapping without column overflow, mm units in all four quadrants,
dense-label detail views with hole avoidance, save/reload of the
tabbed/lead-in fixture with stale tab flags, invalid later-plate data,
late write failures against an existing destination, source
unchanged on success/failure), ReportPdf test helper (page/word/
content-stream extraction).
- docs/nest-reports.md: replace the Slice 1 one-plate limits section
with the general pagination/dense-label contract and the process-wide
PDFsharp/MigraDoc render lock.
Verification: Reporting filter 70/70; full OpenNest.Tests 2524
passed/21 skipped/0 failed; Engine 351/351; IO 77/77;
EnableWindowsTargeting=true full-solution build 0 errors; scoped
dotnet format --verify-no-changes exit 0. Preview PDFs rendered and
visually inspected (evidence: /home/aj/extracted/2026-09-29/opennest-report-slice2/).
Windows runtime, the desktop adapter and packaged-app font deployment
remain unverified (Slice 3).
85 lines
3.3 KiB
C#
85 lines
3.3 KiB
C#
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using PdfSharp.Pdf;
|
|
|
|
namespace OpenNest.Tests.Reporting;
|
|
|
|
/// <summary>PDF inspection for report tests: PDFsharp for structure, Poppler for text.</summary>
|
|
public static class ReportPdf
|
|
{
|
|
public static (int, int) EffectiveSize(PdfPage page)
|
|
{
|
|
var box = page.MediaBox;
|
|
var width = (int)System.Math.Round(box.Width);
|
|
var height = (int)System.Math.Round(box.Height);
|
|
return page.Rotate % 180 == 0 ? (width, height) : (height, width);
|
|
}
|
|
|
|
public static IEnumerable<string> ContentStreams(PdfPage page)
|
|
{
|
|
foreach (var item in page.Contents.Elements)
|
|
{
|
|
var dictionary = (item as PdfSharp.Pdf.Advanced.PdfReference)?.Value as PdfDictionary
|
|
?? item as PdfDictionary;
|
|
if (dictionary?.Stream != null)
|
|
yield return Encoding.Latin1.GetString(dictionary.Stream.UnfilteredValue);
|
|
}
|
|
}
|
|
|
|
/// <summary>Layout text of one page; skipped where poppler-utils is not installed.</summary>
|
|
public static string Text(string path, int page) =>
|
|
Regex.Replace(Run("pdftotext", "-layout", "-f", Page(page), "-l", Page(page), path, "-"), "[ \t]+\n", "\n");
|
|
|
|
/// <summary>Every page's layout text, in order.</summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>Words with page-space boxes (points, Y down).</summary>
|
|
public static List<(double Left, double Top, double Right, double Bottom, string Text)> Words(string path, int page)
|
|
{
|
|
var xml = Run("pdftotext", "-bbox", "-f", Page(page), "-l", Page(page), path, "-");
|
|
return Regex.Matches(xml, "<word xMin=\"([\\d.]+)\" yMin=\"([\\d.]+)\" xMax=\"([\\d.]+)\" yMax=\"([\\d.]+)\">([^<]*)</word>")
|
|
.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)))
|
|
.ToList();
|
|
}
|
|
|
|
private static string Page(int page) => page.ToString(CultureInfo.InvariantCulture);
|
|
|
|
private static double Number(string value) => double.Parse(value, CultureInfo.InvariantCulture);
|
|
|
|
private static string Run(string tool, params string[] arguments)
|
|
{
|
|
var info = new ProcessStartInfo(tool)
|
|
{
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
};
|
|
foreach (var argument in arguments)
|
|
info.ArgumentList.Add(argument);
|
|
Process process;
|
|
try
|
|
{
|
|
process = Process.Start(info)!;
|
|
}
|
|
catch (System.ComponentModel.Win32Exception)
|
|
{
|
|
Skip.If(true, $"{tool} (poppler-utils) is not installed.");
|
|
throw;
|
|
}
|
|
using (process)
|
|
{
|
|
var text = process.StandardOutput.ReadToEnd();
|
|
process.WaitForExit();
|
|
Assert.Equal(0, process.ExitCode);
|
|
return text;
|
|
}
|
|
}
|
|
}
|