diff --git a/OpenNest.Reporting/AtomicReportFile.cs b/OpenNest.Reporting/AtomicReportFile.cs new file mode 100644 index 0000000..d10b8d6 --- /dev/null +++ b/OpenNest.Reporting/AtomicReportFile.cs @@ -0,0 +1,27 @@ +namespace OpenNest.Reporting; + +internal static class AtomicReportFile +{ + internal static void Write(string destination, Action render) + { + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ArgumentNullException.ThrowIfNull(render); + var target = Path.GetFullPath(destination); + var temporary = Path.Combine(Path.GetDirectoryName(target)!, + "." + Path.GetFileName(target) + "." + Guid.NewGuid().ToString("N") + ".tmp"); + try + { + using (var stream = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + { + render(stream); + stream.Flush(flushToDisk: true); + } + // The sibling stays on the same filesystem. Never truncate/delete the target first. + File.Move(temporary, target, overwrite: true); + } + finally + { + File.Delete(temporary); + } + } +} diff --git a/OpenNest.Reporting/Fonts/DejaVuSans-Bold.ttf b/OpenNest.Reporting/Fonts/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..426812b Binary files /dev/null and b/OpenNest.Reporting/Fonts/DejaVuSans-Bold.ttf differ diff --git a/OpenNest.Reporting/Fonts/DejaVuSans.ttf b/OpenNest.Reporting/Fonts/DejaVuSans.ttf new file mode 100644 index 0000000..fb0bd94 Binary files /dev/null and b/OpenNest.Reporting/Fonts/DejaVuSans.ttf differ diff --git a/OpenNest.Reporting/Fonts/LICENSE.txt b/OpenNest.Reporting/Fonts/LICENSE.txt new file mode 100644 index 0000000..af85989 --- /dev/null +++ b/OpenNest.Reporting/Fonts/LICENSE.txt @@ -0,0 +1,48 @@ +DejaVu Sans 2.37 (regular and bold) +Source: https://dejavu-fonts.github.io/ +Distributed unmodified from Ubuntu fonts-dejavu-core 2.37-8. + +Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. + Bitstream Vera is a trademark of Bitstream, Inc. + DejaVu changes are in public domain. +License: bitstream-vera + Permission is hereby granted, free of charge, to any person obtaining a copy + of the fonts accompanying this license ("Fonts") and associated + documentation files (the "Font Software"), to reproduce and distribute the + Font Software, including without limitation the rights to use, copy, merge, + publish, distribute, and/or sell copies of the Font Software, and to permit + persons to whom the Font Software is furnished to do so, subject to the + following conditions: + + The above copyright and trademark notices and this permission notice shall + be included in all copies of one or more of the Font Software typefaces. + + The Font Software may be modified, altered, or added to, and in particular + the designs of glyphs or characters in the Fonts may be modified and + additional glyphs or characters may be added to the Fonts, only if the fonts + are renamed to names not containing either the words "Bitstream" or the word + "Vera". + + This License becomes null and void to the extent applicable to Fonts or Font + Software that has been modified and is distributed under the "Bitstream + Vera" names. + + The Font Software may be sold as part of a larger software package but no + copy of one or more of the Font Software typefaces may be sold by itself. + + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, + TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME + FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING + ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF + THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE + FONT SOFTWARE. + + Except as contained in this notice, the names of Gnome, the Gnome + Foundation, and Bitstream Inc., shall not be used in advertising or + otherwise to promote the sale, use or other dealings in this Font Software + without prior written authorization from the Gnome Foundation or Bitstream + Inc., respectively. For further information, contact: fonts at gnome dot + org. diff --git a/OpenNest.Reporting/NestPdfWriter.cs b/OpenNest.Reporting/NestPdfWriter.cs new file mode 100644 index 0000000..1de7df5 --- /dev/null +++ b/OpenNest.Reporting/NestPdfWriter.cs @@ -0,0 +1,440 @@ +using System.Globalization; +using MigraDoc.DocumentObjectModel; +using MigraDoc.DocumentObjectModel.Tables; +using MigraDoc.Rendering; +using PdfSharp.Drawing; +using PdfSharp.Pdf; + +namespace OpenNest.Reporting; + +/// +/// Renders a detached to PDF: MigraDoc owns text, tables and +/// pagination; PDFsharp draws vector thumbnails and the sheet diagram into reserved layout areas. +/// +public static class NestPdfWriter +{ + // Slice 1 layout limits. Anything beyond them fails before the destination is touched. + private const double Margin = 36; + private const double BottomMargin = 50; + private const double ThumbnailWidth = 54; + private const double ThumbnailHeight = 36; + private const double DiagramHeight = 320; + private const double DiagramInset = 5; + private const double LabelFontSize = 7; + private const double LabelPadding = 1; + + private static readonly XColor Fill = XColor.FromArgb(222, 222, 222); + private static readonly CultureInfo Invariant = CultureInfo.InvariantCulture; + + public static void Write(NestReportSnapshot snapshot, string destination) => + Write(snapshot, destination, null); + + /// Test seam for injecting late write failures. + internal static void Write(NestReportSnapshot snapshot, string destination, Func? wrapOutput) + { + ArgumentNullException.ThrowIfNull(snapshot); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ValidateText(snapshot); + if (snapshot.Plates.Length > 1) + throw new NotSupportedException($"A report with {snapshot.Plates.Length} plate layouts is not supported by this report slice; it supports at most one layout."); + + ReportFonts.Initialize(); + var document = new Document(); + document.Info.Title = snapshot.Name; + var normal = document.Styles[StyleNames.Normal] + ?? throw new InvalidOperationException("MigraDoc normal style is unavailable."); + normal.Font.Name = ReportFonts.Family; + normal.Font.Size = 9; + var diagrams = new List(); + AddSummary(document, snapshot); + foreach (var plate in snapshot.Plates) + diagrams.Add(AddPlate(document, snapshot, plate)); + + var renderer = new PdfDocumentRenderer { Document = document }; + renderer.RenderDocument(); + using var pdf = renderer.PdfDocument; + // PDFsharp 6.2.4 disallows reading the page count after Save. + var pageCount = pdf.PageCount; + var areas = LocateDiagrams(renderer, pageCount, diagrams); + for (var index = 0; index < areas.Count; index++) + { + var (page, area) = areas[index]; + if (page != index + 2) + throw new NotSupportedException("A summary longer than one page is not supported by this report slice."); + using var gfx = XGraphics.FromPdfPage(pdf.Pages[page - 1]); + DrawPlate(gfx, snapshot, snapshot.Plates[index], area); + } + if (pageCount != 1 + snapshot.Plates.Length) + throw new NotSupportedException(snapshot.Plates.Length == 0 + ? "A summary longer than one page is not supported by this report slice." + : "A plate page longer than one page is not supported by this report slice."); + + pdf.Info.Title = snapshot.Name; + pdf.Info.Creator = "OpenNest"; + pdf.Info.CreationDate = snapshot.GeneratedAt.UtcDateTime; + AtomicReportFile.Write(destination, stream => pdf.Save(wrapOutput?.Invoke(stream) ?? stream, false)); + } + + private static void ValidateText(NestReportSnapshot snapshot) + { + ReportFonts.ValidateText(snapshot.Name, "Nest name"); + ReportFonts.ValidateText(snapshot.Customer, "Customer"); + ReportFonts.ValidateText(snapshot.Notes, "Notes"); + ReportFonts.ValidateText(snapshot.Material, "Material"); + ReportFonts.ValidateText(snapshot.Grade, "Material grade"); + foreach (var drawing in snapshot.Drawings) + ReportFonts.ValidateText(drawing.Name, $"Drawing {drawing.Id} name"); + } + + private static void AddSummary(Document document, NestReportSnapshot snapshot) + { + var section = AddSection(document, Orientation.Portrait); + var title = section.AddParagraph("Nest Report"); + title.Format.Font.Size = 14; + title.Format.Font.Bold = true; + title.Format.SpaceAfter = Unit.FromPoint(6); + + var info = AddTable(section, 130, 410); + info.Borders.Visible = false; + AddInfoRow(info, "Nest:", snapshot.Name); + if (snapshot.Customer.Length > 0) + AddInfoRow(info, "Customer:", snapshot.Customer); + if (snapshot.Notes.Length > 0) + AddInfoRow(info, "Notes:", snapshot.Notes); + AddInfoRow(info, "Material:", Material(snapshot)); + AddInfoRow(info, "Thickness:", Thickness(snapshot)); + AddInfoRow(info, "Units:", snapshot.Units == "mm" ? "millimeters (mm)" : "inches (in)"); + AddInfoRow(info, "Generated:", snapshot.GeneratedAt.ToString("yyyy-MM-dd HH:mm zzz", Invariant)); + AddInfoRow(info, "Distinct layouts:", snapshot.Plates.Length.ToString(Invariant)); + AddInfoRow(info, "Total physical sheets:", snapshot.TotalSheets.ToString(Invariant)); + + AddHeading(section, "Plates"); + if (snapshot.Plates.Length == 0) + section.AddParagraph("No plates in this job."); + else + { + var plates = AddTable(section, 50, 150, 70, 80, 80); + AddHeader(plates, "Plate", "Stock size", "Copies", "Parts/sheet", "Utilization"); + foreach (var plate in snapshot.Plates) + AddRow(plates, plate.Number.ToString(Invariant), SheetSize(snapshot, plate), + plate.Copies.ToString(Invariant), plate.Parts.Length.ToString(Invariant), Percent(plate.Utilization)); + } + + AddHeading(section, "Parts"); + if (snapshot.Drawings.Length == 0) + { + section.AddParagraph("No parts in this job."); + return; + } + var parts = AddTable(section, 36, 62, 170, 50, 50, 52, 40, 80); + AddHeader(parts, "ID", "Part", "Drawing", "Required", "Nested", "Shortage", "Extra", "Plates"); + foreach (var drawing in snapshot.Drawings) + { + var row = AddRow(parts, drawing.Id, null, drawing.Name, drawing.Required.ToString(Invariant), + drawing.Nested.ToString(Invariant), drawing.Shortage.ToString(Invariant), + drawing.Extra.ToString(Invariant), drawing.Plates.Length == 0 ? "-" : string.Join(", ", drawing.Plates)); + var image = row.Cells[1].AddParagraph().AddImage(Thumbnail(drawing.Geometry)); + image.Width = Unit.FromPoint(ThumbnailWidth); + image.Height = Unit.FromPoint(ThumbnailHeight); + } + } + + private static Table AddPlate(Document document, NestReportSnapshot snapshot, ReportPlate plate) + { + var section = AddSection(document, Orientation.Landscape); + var title = section.AddParagraph($"Plate {plate.Number} of {snapshot.Plates.Length}"); + title.Format.Font.Size = 13; + title.Format.Font.Bold = true; + + var info = AddTable(section, 80, 280, 90, 270); + info.Borders.Visible = false; + AddInfoRow(info, "Nest:", snapshot.Name, "Sheet size:", SheetSize(snapshot, plate)); + AddInfoRow(info, "Material:", Material(snapshot), "Copies:", plate.Copies.ToString(Invariant)); + AddInfoRow(info, "Thickness:", Thickness(snapshot), "Part spacing:", + $"{Number(plate.PartSpacing)} {snapshot.Units}"); + AddInfoRow(info, "Parts/sheet:", plate.Parts.Length.ToString(Invariant), "Utilization:", + Percent(plate.Utilization) + " (net part area / full sheet)"); + + // An empty fixed-height row reserves the vector diagram area in MigraDoc's flow. + var diagram = AddTable(section, 720); + diagram.Borders.Visible = false; + diagram.TopPadding = diagram.BottomPadding = Unit.Zero; + var reserved = diagram.AddRow(); + reserved.HeightRule = RowHeightRule.Exactly; + reserved.Height = Unit.FromPoint(DiagramHeight); + diagram.Format.SpaceBefore = Unit.FromPoint(4); + + var legend = section.AddParagraph("Filled outlines are closed parts; open material paths such as tab gaps are shown without fill. Dashed lines are scrap cutoffs."); + legend.Format.Font.Size = 7.5; + legend.Format.SpaceBefore = Unit.FromPoint(4); + var approval = section.AddParagraph("This report is not a geometry or CNC approval."); + approval.Format.Font.Size = 7.5; + approval.Format.SpaceAfter = Unit.FromPoint(4); + + var names = snapshot.Drawings.ToDictionary(drawing => drawing.Id, drawing => drawing.Name, StringComparer.Ordinal); + var perSheet = plate.Parts.GroupBy(part => part.ReportId, StringComparer.Ordinal) + .ToDictionary(group => group.Key, group => (long)group.Count(), StringComparer.Ordinal); + var parts = AddTable(section, 50, 330, 100, 140); + AddHeader(parts, "ID", "Drawing", "Qty/sheet", $"Total ({plate.Copies} copies)"); + foreach (var drawing in snapshot.Drawings.Where(drawing => perSheet.ContainsKey(drawing.Id))) + { + var count = perSheet[drawing.Id]; + AddRow(parts, drawing.Id, names[drawing.Id], count.ToString(Invariant), + checked(count * plate.Copies).ToString(Invariant)); + } + return diagram; + } + + private static List<(int Page, XRect Area)> LocateDiagrams(PdfDocumentRenderer renderer, int pageCount, List
diagrams) + { + var found = new (int Page, XRect Area)?[diagrams.Count]; + for (var page = 1; page <= pageCount; page++) + { + var renderInfos = renderer.DocumentRenderer?.GetRenderInfoFromPage(page) + ?? throw new InvalidOperationException("MigraDoc render information is unavailable."); + foreach (var info in renderInfos) + { + var index = diagrams.FindIndex(table => ReferenceEquals(table, info.DocumentObject)); + if (index < 0) + continue; + if (found[index] != null) + throw new NotSupportedException($"A diagram for plate {index + 1} split across pages is not supported by this report slice."); + var area = info.LayoutInfo.ContentArea; + found[index] = (page, new XRect(area.X.Point, area.Y.Point, area.Width.Point, area.Height.Point)); + } + } + return found.Select((item, index) => item + ?? throw new NotSupportedException($"Plate {index + 1}: a missing diagram area is not supported by this report slice.")).ToList(); + } + + private static void DrawPlate(XGraphics gfx, NestReportSnapshot snapshot, ReportPlate plate, XRect area) + { + var inner = new XRect(area.X + DiagramInset, area.Y + DiagramInset, + area.Width - 2 * DiagramInset, DiagramHeight - 2 * DiagramInset); + var fit = Fit.Create(plate.Bounds, inner); + var font = new XFont(ReportFonts.Family, LabelFontSize, XFontStyleEx.Bold); + + // Validate every label before drawing: dense-label callouts are a later slice. + var labels = new List<(string Id, XRect Box)>(); + for (var index = 0; index < plate.Parts.Length; index++) + { + var part = plate.Parts[index]; + var bounds = fit.Rect(part.Geometry.Bounds); + var size = gfx.MeasureString(part.ReportId, font); + var width = size.Width + 2 * LabelPadding; + var height = size.Height + 2 * LabelPadding; + if (width > bounds.Width || height > bounds.Height) + throw new NotSupportedException($"Plate {plate.Number}, part {index + 1} ({part.ReportId}): a label that does not fit inside its part at {LabelFontSize} pt is not supported by this report slice."); + var box = new XRect(bounds.X + (bounds.Width - width) / 2, bounds.Y + (bounds.Height - height) / 2, width, height); + foreach (var other in labels) + { + if (box.IntersectsWith(other.Box)) + throw new NotSupportedException($"Plate {plate.Number}: overlapping labels {other.Id} and {part.ReportId} are not supported by this report slice."); + } + labels.Add((part.ReportId, box)); + } + + gfx.DrawRectangle(new XPen(XColors.Black, 1), fit.Rect(plate.Bounds)); + var outline = new XPen(XColors.Black, 0.6); + foreach (var part in plate.Parts) + DrawGeometry(gfx, part.Geometry, fit, outline); + var cutoff = new XPen(XColors.Black, 0.75) { DashStyle = XDashStyle.Dash }; + foreach (var geometry in plate.Cutoffs) + StrokeContours(gfx, geometry, fit, cutoff); + foreach (var (id, box) in labels) + { + gfx.DrawRectangle(XBrushes.White, box); + gfx.DrawString(id, font, XBrushes.Black, box, XStringFormats.Center); + } + } + + private static string Thumbnail(ReportGeometry geometry) + { + using var document = new PdfDocument(); + var page = document.AddPage(); + page.Width = XUnit.FromPoint(ThumbnailWidth); + page.Height = XUnit.FromPoint(ThumbnailHeight); + using (var gfx = XGraphics.FromPdfPage(page)) + { + var fit = Fit.Create(geometry.Bounds, new XRect(2, 2, ThumbnailWidth - 4, ThumbnailHeight - 4)); + DrawGeometry(gfx, geometry, fit, new XPen(XColors.Black, 0.5)); + } + using var stream = new MemoryStream(); + document.Save(stream, false); + // MigraDoc embeds a PDF image source as a vector form XObject, not a raster image. + return "base64:" + Convert.ToBase64String(stream.ToArray()); + } + + private static void DrawGeometry(XGraphics gfx, ReportGeometry geometry, Fit fit, XPen pen) + { + if (geometry.StrokeOnly) + { + StrokeContours(gfx, geometry, fit, pen); + return; + } + // Every contour is closed here, so CloseFigure separates the subpaths. + var path = new XGraphicsPath(); + foreach (var contour in geometry.Contours) + { + AddContour(path, contour, fit); + path.CloseFigure(); + } + path.FillMode = XFillMode.Alternate; // Even-odd: holes stay unfilled. + gfx.DrawPath(pen, new XSolidBrush(Fill), path); + } + + /// + /// PDFsharp's StartFigure does not emit a move-to after an open figure, which would draw a + /// false segment across a tab gap to the next contour. Stroke each contour as its own path. + /// + private static void StrokeContours(XGraphics gfx, ReportGeometry geometry, Fit fit, XPen pen) + { + foreach (var contour in geometry.Contours) + { + var path = new XGraphicsPath(); + AddContour(path, contour, fit); + if (contour.Closed) + path.CloseFigure(); + gfx.DrawPath(pen, path); + } + } + + private static void AddContour(XGraphicsPath path, ReportContour contour, Fit fit) + { + foreach (var segment in contour.Segments) + { + if (segment.Center == null) + { + path.AddLine(fit.Point(segment.Start), fit.Point(segment.End)); + continue; + } + var center = fit.Point(segment.Center); + var radius = segment.Radius * fit.Scale; + var box = new XRect(center.X - radius, center.Y - radius, 2 * radius, 2 * radius); + // Page Y points down, so model angles and sweeps change sign. + path.AddArc(box, -Degrees(segment.StartAngle), -Degrees(segment.SweepAngle)); + } + } + + private static double Degrees(double radians) => radians * 180 / System.Math.PI; + + private static Section AddSection(Document document, Orientation orientation) + { + var section = document.AddSection(); + var setup = section.PageSetup; + setup.PageFormat = PageFormat.Letter; + setup.Orientation = orientation; + setup.TopMargin = setup.LeftMargin = setup.RightMargin = Unit.FromPoint(Margin); + setup.BottomMargin = Unit.FromPoint(BottomMargin); + setup.FooterDistance = Unit.FromPoint(20); + var footer = section.Footers.Primary.AddParagraph("Page "); + footer.AddPageField(); + footer.AddText(" of "); + footer.AddNumPagesField(); + footer.AddText(" | Diagram fitted to page; not a dimensioned cutting drawing."); + footer.Format.Font.Size = 7.5; + return section; + } + + private static void AddHeading(Section section, string text) + { + var heading = section.AddParagraph(text); + heading.Format.Font.Bold = true; + heading.Format.Font.Size = 11; + heading.Format.SpaceBefore = Unit.FromPoint(10); + heading.Format.SpaceAfter = Unit.FromPoint(4); + } + + private static Table AddTable(Section section, params double[] widths) + { + var table = section.AddTable(); + table.Borders.Width = 0.5; + table.Borders.Color = Colors.Gray; + table.LeftPadding = table.RightPadding = Unit.FromPoint(3); + table.TopPadding = table.BottomPadding = Unit.FromPoint(1.5); + foreach (var width in widths) + table.AddColumn(Unit.FromPoint(width)); + return table; + } + + private static void AddHeader(Table table, params string[] cells) + { + var row = AddRow(table, cells); + row.HeadingFormat = true; + row.Format.Font.Bold = true; + row.Shading.Color = Colors.LightGray; + } + + private static void AddInfoRow(Table table, params string[] cells) + { + var row = AddRow(table, cells); + for (var index = 0; index < cells.Length; index += 2) + row.Cells[index].Format.Font.Bold = true; + } + + /// Adds text cells; a null cell is left empty for non-text content. + private static Row AddRow(Table table, params string?[] cells) + { + var row = table.AddRow(); + row.VerticalAlignment = VerticalAlignment.Center; + for (var index = 0; index < cells.Length; index++) + { + if (cells[index] == null) + continue; + var paragraph = row.Cells[index].AddParagraph(); + var lines = cells[index]!.Replace("\r\n", "\n").Replace('\r', '\n').Replace('\t', ' ').Split('\n'); + for (var line = 0; line < lines.Length; line++) + { + if (line > 0) + paragraph.AddLineBreak(); + paragraph.AddText(lines[line]); + } + } + return row; + } + + private static string Material(NestReportSnapshot snapshot) + { + var material = $"{snapshot.Material} {snapshot.Grade}".Trim(); + return material.Length == 0 ? "Not specified" : material; + } + + // Zero thickness means "unset" in a new nest; never present it as a genuine value. + private static string Thickness(NestReportSnapshot snapshot) => + snapshot.Thickness > 0 ? $"{Number(snapshot.Thickness)} {snapshot.Units}" : "Not specified"; + + private static string SheetSize(NestReportSnapshot snapshot, ReportPlate plate) => + $"{Number(plate.Bounds.Length)} x {Number(plate.Bounds.Width)} {snapshot.Units}"; + + private static string Number(double value) => value.ToString("0.####", Invariant); + + private static string Percent(double value) => (value * 100).ToString("0.0", Invariant) + "%"; + + /// One aspect-preserving model-to-page transform, centered, with the Y axis flipped. + private readonly record struct Fit(ReportBounds Bounds, double Scale, double X, double Y) + { + public static Fit Create(ReportBounds bounds, XRect area) + { + var scales = new List(2); + if (bounds.Length > 0) + scales.Add(area.Width / bounds.Length); + if (bounds.Width > 0) + scales.Add(area.Height / bounds.Width); + var scale = scales.Count == 0 ? 1 : scales.Min(); + if (!double.IsFinite(scale) || scale <= 0) + throw new InvalidOperationException("Report geometry cannot be fitted to the page."); + return new Fit(bounds, scale, + area.X + (area.Width - bounds.Length * scale) / 2, + area.Y + (area.Height - bounds.Width * scale) / 2); + } + + public XPoint Point(ReportPoint point) => + new(X + (point.X - Bounds.Left) * Scale, Y + (Bounds.Top - point.Y) * Scale); + + public XRect Rect(ReportBounds box) + { + var low = Point(new ReportPoint(box.Left, box.Top)); + return new XRect(low.X, low.Y, box.Length * Scale, box.Width * Scale); + } + } +} diff --git a/OpenNest.Reporting/NestReportBuilder.cs b/OpenNest.Reporting/NestReportBuilder.cs new file mode 100644 index 0000000..423206f --- /dev/null +++ b/OpenNest.Reporting/NestReportBuilder.cs @@ -0,0 +1,131 @@ +using System.Collections.Immutable; +using System.Globalization; +using OpenNest.Geometry; + +namespace OpenNest.Reporting; + +public static class NestReportBuilder +{ + /// + /// Capture synchronously while the caller keeps the nest stable. No quantity refresh, + /// domain cloning, or event-wired temporary plates are used. + /// + public static NestReportSnapshot Capture(Nest nest, DateTimeOffset generatedAt) + { + ArgumentNullException.ThrowIfNull(nest); + var ordered = new List(); + var byReference = new Dictionary(ReferenceEqualityComparer.Instance); + var plates = ImmutableArray.CreateBuilder(); + + DrawingCount Register(Drawing drawing) + { + if (byReference.TryGetValue(drawing, out var existing)) + return existing; + var item = new DrawingCount(drawing, "R" + (ordered.Count + 1).ToString("D3", CultureInfo.InvariantCulture)); + byReference.Add(drawing, item); + ordered.Add(item); + return item; + } + + for (var index = 0; index < nest.Plates.Count; index++) + { + var number = index + 1; + var plate = nest.Plates[index]; + if (plate == null) + throw new InvalidOperationException($"Plate {number}: missing plate."); + if (!PositiveFinite(plate.Size.Length) || !PositiveFinite(plate.Size.Width) + || !PositiveFinite(plate.Area())) + throw new InvalidOperationException($"Plate {number}: sheet dimensions must be finite and positive."); + if (plate.Quantity <= 0) + throw new InvalidOperationException($"Plate {number}: copies must be a positive integer."); + if (!double.IsFinite(plate.PartSpacing) || plate.PartSpacing < 0) + throw new InvalidOperationException($"Plate {number}: part spacing must be finite and nonnegative."); + + var box = plate.BoundingBox(false); + var bounds = new ReportBounds(box.Left, box.Bottom, box.Right, box.Top); + if (!double.IsFinite(bounds.Left) || !double.IsFinite(bounds.Bottom) + || !double.IsFinite(bounds.Right) || !double.IsFinite(bounds.Top)) + throw new InvalidOperationException($"Plate {number}: sheet bounds are not finite."); + var parts = ImmutableArray.CreateBuilder(); + var cutoffs = ImmutableArray.CreateBuilder(); + var perSheet = new Dictionary(); + var area = 0.0; + for (var partIndex = 0; partIndex < plate.Parts.Count; partIndex++) + { + var part = plate.Parts[partIndex]; + var context = $"Plate {number}, part {partIndex + 1} ('{part?.BaseDrawing?.Name}')"; + if (part?.BaseDrawing == null) + throw new InvalidOperationException($"{context}: missing part or drawing reference."); + var geometry = NestReportGeometry.Capture(part.Program, part.Location, context); + if (part.BaseDrawing.IsCutOff) + { + cutoffs.Add(geometry); + continue; + } + + var item = Register(part.BaseDrawing); + parts.Add(new ReportPlacement(item.Id, geometry)); + perSheet.TryGetValue(item, out var count); + perSheet[item] = checked(count + 1); + var partArea = part.BaseDrawing.Area; + if (!double.IsFinite(partArea) || partArea < 0) + throw new InvalidOperationException($"{context}: net part area is invalid."); + area += partArea; + } + + foreach (var (item, count) in perSheet) + { + item.Nested = checked(item.Nested + checked(count * plate.Quantity)); + item.Plates.Add(number); + } + var utilization = area / plate.Area(); + if (!double.IsFinite(utilization)) + throw new InvalidOperationException($"Plate {number}: utilization is not finite."); + plates.Add(new ReportPlate(number, bounds, plate.Quantity, plate.PartSpacing, + utilization, parts.ToImmutable(), cutoffs.ToImmutable())); + } + + // OrderBy is stable, so the original enumeration breaks equal name/path ties. + foreach (var drawing in nest.Drawings + .Where(drawing => !drawing.IsCutOff) + .OrderBy(drawing => drawing.Name ?? "", StringComparer.Ordinal) + .ThenBy(drawing => drawing.Source?.Path ?? "", StringComparer.Ordinal)) + Register(drawing); + + var drawings = ImmutableArray.CreateBuilder(); + foreach (var item in ordered) + { + var drawing = item.Drawing; + var context = $"Drawing {item.Id} ('{drawing.Name}')"; + var required = (long)drawing.Quantity.Required; + if (required < 0) + throw new InvalidOperationException($"{context}: required quantity cannot be negative."); + var geometry = NestReportGeometry.Capture(drawing.Program, new Vector(), context); + drawings.Add(new ReportDrawing(item.Id, drawing.Name ?? "", required, item.Nested, + System.Math.Max(checked(required - item.Nested), 0), + System.Math.Max(checked(item.Nested - required), 0), item.Plates.ToImmutableArray(), geometry)); + } + + var units = nest.Units switch + { + Units.Inches => "in", + Units.Millimeters => "mm", + _ => throw new InvalidOperationException("Nest: unsupported units."), + }; + if (!double.IsFinite(nest.Thickness) || nest.Thickness < 0) + throw new InvalidOperationException("Nest: thickness must be finite and nonnegative."); + return new NestReportSnapshot(nest.Name ?? "", nest.Customer ?? "", nest.Notes ?? "", + nest.Material?.Name ?? "", nest.Material?.Grade ?? "", nest.Thickness, units, generatedAt, + drawings.ToImmutable(), plates.ToImmutable()); + } + + private static bool PositiveFinite(double value) => double.IsFinite(value) && value > 0; + + private sealed class DrawingCount(Drawing drawing, string id) + { + public Drawing Drawing { get; } = drawing; + public string Id { get; } = id; + public long Nested { get; set; } + public List Plates { get; } = []; + } +} diff --git a/OpenNest.Reporting/NestReportGeometry.cs b/OpenNest.Reporting/NestReportGeometry.cs new file mode 100644 index 0000000..cd6dfc6 --- /dev/null +++ b/OpenNest.Reporting/NestReportGeometry.cs @@ -0,0 +1,174 @@ +using System.Collections.Immutable; +using OpenNest.CNC; +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.Math; + +namespace OpenNest.Reporting; + +internal static class NestReportGeometry +{ + internal static ReportGeometry Capture(Program? program, Vector location, string context) + { + ValidatePoint(location, context); + var current = new Vector(); + // ConvertProgram recurses without cycle/null guards and silently skips unknown codes. + // Validate first; never clone through SubProgramCall's rotating setters. + ValidateProgram(program, ref current, new HashSet(ReferenceEqualityComparer.Instance), context); + var entities = ConvertProgram.ToGeometry(program!); + var contours = ImmutableArray.CreateBuilder(); + var segments = ImmutableArray.CreateBuilder(); + var left = double.PositiveInfinity; + var bottom = double.PositiveInfinity; + var right = double.NegativeInfinity; + var top = double.NegativeInfinity; + + void Flush() + { + if (segments.Count == 0) + return; + var closed = SamePoint(segments[0].Start, segments[^1].End); + contours.Add(new ReportContour(segments.ToImmutable(), closed)); + segments.Clear(); + } + + foreach (var entity in entities) + { + if (!SpecialLayers.IsMaterial(entity.Layer) + || entity.Layer == SpecialLayers.Leadin || entity.Layer == SpecialLayers.Leadout) + { + Flush(); + continue; + } + + ReportSegment segment; + switch (entity) + { + case Line line: + if (line.StartPoint.X == line.EndPoint.X && line.StartPoint.Y == line.EndPoint.Y) + continue; + segment = new ReportSegment(Point(line.StartPoint, location, context), + Point(line.EndPoint, location, context), null, 0, 0, 0); + break; + case Arc arc: + segment = new ReportSegment(Point(arc.StartPoint(), location, context), + Point(arc.EndPoint(), location, context), Point(arc.Center, location, context), + arc.Radius, arc.StartAngle, arc.SweepAngle() * (arc.IsReversed ? -1 : 1)); + break; + case Circle circle: + Flush(); + var start = Point(new Vector(circle.Center.X + circle.Radius, circle.Center.Y), location, context); + segment = new ReportSegment(start, start, Point(circle.Center, location, context), + circle.Radius, 0, circle.Rotation == RotationType.CW ? -2 * System.Math.PI : 2 * System.Math.PI); + break; + default: + throw Error(context, $"unsupported geometry '{entity.GetType().Name}'."); + } + + if (segment.Center != null && (!double.IsFinite(segment.Radius) || segment.Radius <= 0 + || !double.IsFinite(segment.StartAngle) || !double.IsFinite(segment.SweepAngle) + || segment.SweepAngle == 0 || System.Math.Abs(segment.SweepAngle) > 2 * System.Math.PI)) + throw Error(context, "invalid arc/circle data."); + if (segments.Count > 0 && !SamePoint(segments[^1].End, segment.Start)) + Flush(); + segments.Add(segment); + if (SamePoint(segments[0].Start, segment.End)) + Flush(); + + var box = entity.BoundingBox; + var low = Point(new Vector(box.Left, box.Bottom), location, context); + var high = Point(new Vector(box.Right, box.Top), location, context); + left = System.Math.Min(left, low.X); + bottom = System.Math.Min(bottom, low.Y); + right = System.Math.Max(right, high.X); + top = System.Math.Max(top, high.Y); + } + Flush(); + if (contours.Count == 0) + throw Error(context, "missing drawable material geometry."); + if (!double.IsFinite(right - left) || !double.IsFinite(top - bottom)) + throw Error(context, "geometry bounds are not finite."); + return new ReportGeometry(contours.ToImmutable(), new ReportBounds(left, bottom, right, top)); + } + + private static void ValidateProgram(Program? program, ref Vector current, + HashSet active, string context) + { + if (program?.Codes == null) + throw Error(context, "missing program or subprogram reference."); + if (!active.Add(program)) + throw Error(context, "cyclic subprogram reference."); + if (active.Count > 256) + throw Error(context, "unsupported subprogram nesting depth (maximum 256)."); + if (!double.IsFinite(program.Rotation) || !Enum.IsDefined(program.Mode)) + throw Error(context, "invalid program transform or coordinate mode."); + var origin = current; + for (var index = 0; index < program.Codes.Count; index++) + { + var code = program.Codes[index]; + var codeContext = $"{context}, code {index + 1}"; + switch (code) + { + case SubProgramCall call when code.Type == CodeType.SubProgramCall: + ValidatePoint(call.Offset, codeContext); + if (!double.IsFinite(call.Rotation)) + throw Error(codeContext, "invalid subprogram rotation."); + current = origin + call.Offset; + ValidatePoint(current, codeContext); + ValidateProgram(call.Program, ref current, active, $"{codeContext}, subprogram {call.Id}"); + break; + case Motion motion when code is LinearMove && code.Type == CodeType.LinearMove + || code is RapidMove && code.Type == CodeType.RapidMove + || code is ArcMove && code.Type == CodeType.ArcMove: + ValidatePoint(motion.EndPoint, codeContext); + var end = program.Mode == Mode.Incremental ? current + motion.EndPoint : motion.EndPoint; + ValidatePoint(end, codeContext); + if (motion is LinearMove linear && !Enum.IsDefined(linear.Layer)) + throw Error(codeContext, "unsupported material layer."); + if (motion is ArcMove arc) + { + ValidatePoint(arc.CenterPoint, codeContext); + if (!Enum.IsDefined(arc.Rotation) || !Enum.IsDefined(arc.Layer)) + throw Error(codeContext, "invalid arc rotation or layer."); + var center = program.Mode == Mode.Incremental ? current + arc.CenterPoint : arc.CenterPoint; + ValidatePoint(center, codeContext); + var startRadius = current.DistanceTo(center); + var endRadius = end.DistanceTo(center); + if (!double.IsFinite(startRadius) || !double.IsFinite(endRadius) + || startRadius <= 0 || endRadius <= 0 + || System.Math.Abs(startRadius - endRadius) > Tolerance.Epsilon) + throw Error(codeContext, "invalid arc/circle radius or inconsistent endpoints."); + } + current = end; + break; + case Comment when code.Type == CodeType.Comment: + case Feedrate when code.Type == CodeType.SetFeedrate: + case Kerf when code.Type == CodeType.SetKerf: + break; + default: + throw Error(codeContext, code == null ? "missing code." : $"unsupported code '{code.GetType().Name}'."); + } + } + active.Remove(program); + } + + private static ReportPoint Point(Vector point, Vector location, string context) + { + var translated = point + location; + ValidatePoint(translated, context); + return new ReportPoint(translated.X, translated.Y); + } + + private static void ValidatePoint(Vector point, string context) + { + if (!double.IsFinite(point.X) || !double.IsFinite(point.Y)) + throw Error(context, "nonfinite geometry coordinate."); + } + + // Only absorb floating-point trig roundoff, never ShapeBuilder's welding tolerance. + // A deliberate tab/rapid interruption is never joined by contour construction. + private static bool SamePoint(ReportPoint first, ReportPoint second) => + System.Math.Abs(first.X - second.X) <= 1e-12 && System.Math.Abs(first.Y - second.Y) <= 1e-12; + + private static InvalidOperationException Error(string context, string message) => new($"{context}: {message}"); +} diff --git a/OpenNest.Reporting/NestReportSnapshot.cs b/OpenNest.Reporting/NestReportSnapshot.cs new file mode 100644 index 0000000..b05bb28 --- /dev/null +++ b/OpenNest.Reporting/NestReportSnapshot.cs @@ -0,0 +1,38 @@ +using System.Collections.Immutable; + +namespace OpenNest.Reporting; + +public sealed record ReportPoint(double X, double Y); + +public sealed record ReportBounds(double Left, double Bottom, double Right, double Top) +{ + public double Length => Right - Left; + public double Width => Top - Bottom; +} + +/// Native line or arc; angles are radians and a null center denotes a line. +public sealed record ReportSegment(ReportPoint Start, ReportPoint End, ReportPoint? Center, + double Radius, double StartAngle, double SweepAngle); + +public sealed record ReportContour(ImmutableArray Segments, bool Closed); + +public sealed record ReportGeometry(ImmutableArray Contours, ReportBounds Bounds) +{ + public bool StrokeOnly => Contours.Any(contour => !contour.Closed); +} + +public sealed record ReportDrawing(string Id, string Name, long Required, long Nested, + long Shortage, long Extra, ImmutableArray Plates, ReportGeometry Geometry); + +public sealed record ReportPlacement(string ReportId, ReportGeometry Geometry); + +public sealed record ReportPlate(int Number, ReportBounds Bounds, int Copies, double PartSpacing, + double Utilization, ImmutableArray Parts, ImmutableArray Cutoffs); + +/// A detached value snapshot. No domain objects or mutable geometry are retained. +public sealed record NestReportSnapshot(string Name, string Customer, string Notes, string Material, + string Grade, double Thickness, string Units, DateTimeOffset GeneratedAt, + ImmutableArray Drawings, ImmutableArray Plates) +{ + public long TotalSheets => Plates.Aggregate(0L, (total, plate) => checked(total + plate.Copies)); +} diff --git a/OpenNest.Reporting/OpenNest.Reporting.csproj b/OpenNest.Reporting/OpenNest.Reporting.csproj new file mode 100644 index 0000000..b864a17 --- /dev/null +++ b/OpenNest.Reporting/OpenNest.Reporting.csproj @@ -0,0 +1,14 @@ + + + net8.0 + enable + enable + + + + + + + + + diff --git a/OpenNest.Reporting/ReportFonts.cs b/OpenNest.Reporting/ReportFonts.cs new file mode 100644 index 0000000..dea132b --- /dev/null +++ b/OpenNest.Reporting/ReportFonts.cs @@ -0,0 +1,59 @@ +using PdfSharp.Fonts; + +namespace OpenNest.Reporting; + +/// Process-wide, embedded fonts; never consult platform font directories. +internal static class ReportFonts +{ + internal const string Family = "OpenNest Report Sans"; + private static readonly object Sync = new(); + private static readonly BundledResolver Resolver = new(); + + internal static void Initialize() + { + lock (Sync) + { + if (GlobalFontSettings.FontResolver == null) + GlobalFontSettings.FontResolver = Resolver; + else if (!ReferenceEquals(GlobalFontSettings.FontResolver, Resolver)) + throw new InvalidOperationException("Nest reports require their bundled font resolver before PDF fonts are created. Another resolver is already installed."); + // MigraDoc otherwise asks the resolver for a platform "Courier New" error font. + MigraDoc.PredefinedFontsAndChars.ErrorFontName = Family; + } + } + + internal static void ValidateText(string text, string field) + { + // Slice 1 deliberately supports printable Latin-1, not general Unicode shaping. + // Reject unavailable text explicitly instead of substituting .notdef boxes or hiding it. + foreach (var rune in text.EnumerateRunes()) + { + var value = rune.Value; + if (value is '\r' or '\n' or '\t' || value is >= 0x20 and <= 0x7e + || value is >= 0xa0 and <= 0xff && value != 0xad) + continue; + throw new InvalidOperationException($"{field}: unsupported report character U+{value:X4}. This report slice supports printable Latin-1 text only."); + } + } + + private sealed class BundledResolver : IFontResolver + { + public FontResolverInfo? ResolveTypeface(string familyName, bool isBold, bool isItalic) + { + if (!string.Equals(familyName, Family, StringComparison.OrdinalIgnoreCase) || isItalic) + return null; + return new FontResolverInfo(isBold ? "DejaVuSans-Bold" : "DejaVuSans"); + } + + public byte[] GetFont(string faceName) + { + if (faceName is not ("DejaVuSans" or "DejaVuSans-Bold")) + throw new InvalidOperationException($"Unknown report font face: {faceName}."); + using var source = typeof(ReportFonts).Assembly.GetManifestResourceStream($"OpenNest.Reporting.Fonts.{faceName}.ttf") + ?? throw new InvalidOperationException($"Bundled report font missing: {faceName}."); + using var bytes = new MemoryStream(); + source.CopyTo(bytes); + return bytes.ToArray(); + } + } +} diff --git a/OpenNest.Reporting/THIRD-PARTY-NOTICES.txt b/OpenNest.Reporting/THIRD-PARTY-NOTICES.txt new file mode 100644 index 0000000..81836bc --- /dev/null +++ b/OpenNest.Reporting/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,1335 @@ +PDFsharp and MigraDoc 6.2.4 +Source: https://github.com/empira/PDFsharp/tree/v6.2.4 + +Copyright (c) 2001-2026 empira Software GmbH, Troisdorf (Cologne Area), Germany + +http://docs.pdfsharp.net + +MIT License + +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. + + +LICENSE.TXT applies to: +Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2 +Microsoft.Extensions.Logging.Abstractions/8.0.3 +System.Security.Cryptography.Pkcs/8.0.1 + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +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. + + +THIRD-PARTY-NOTICES.TXT applies to: +Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2 +Microsoft.Extensions.Logging.Abstractions/8.0.3 +System.Security.Cryptography.Pkcs/8.0.1 + +.NET Runtime uses third-party libraries or other resources that may be +distributed under licenses different than the .NET Runtime software. + +In the event that we accidentally failed to list a required notice, please +bring it to our attention. Post an issue or email us: + + dotnet@microsoft.com + +The attached notices are provided for information only. + +License notice for ASP.NET +------------------------------- + +Copyright (c) .NET Foundation. All rights reserved. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/dotnet/aspnetcore/blob/main/LICENSE.txt + +License notice for Slicing-by-8 +------------------------------- + +http://sourceforge.net/projects/slicing-by-8/ + +Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + + +This software program is licensed subject to the BSD License, available at +http://www.opensource.org/licenses/bsd-license.html. + + +License notice for Unicode data +------------------------------- + +https://www.unicode.org/license.html + +Copyright © 1991-2022 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in https://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +License notice for Zlib +----------------------- + +https://github.com/madler/zlib +https://zlib.net/zlib_license.html + +/* zlib.h -- interface of the 'zlib' general purpose compression library + version 1.3.1, January 22nd, 2024 + + Copyright (C) 1995-2022 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +*/ + +License notice for Mono +------------------------------- + +http://www.mono-project.com/docs/about-mono/ + +Copyright (c) .NET Foundation Contributors + +MIT License + +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. + +License notice for International Organization for Standardization +----------------------------------------------------------------- + +Portions (C) International Organization for Standardization 1986: + Permission to copy in any form is granted for use with + conforming SGML systems and applications as defined in + ISO 8879, provided this notice is included in all copies. + +License notice for Intel +------------------------ + +"Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Xamarin and Novell +------------------------------------- + +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) + +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. + +Copyright (c) 2011 Novell, Inc (http://www.novell.com) + +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. + +Third party notice for W3C +-------------------------- + +"W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE +Status: This license takes effect 13 May, 2015. +This work is being provided by the copyright holders under the following license. +License +By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions. +Permission to copy, modify, and distribute this work, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the work or portions thereof, including modifications: +The full text of this NOTICE in a location viewable to users of the redistributed or derivative work. +Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software and Document Short Notice should be included. +Notice of any changes or modifications, through a copyright statement on the new code or document such as "This software or document includes material copied from or derived from [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +Disclaimers +THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. Title to copyright in this work will at all times remain with copyright holders." + +License notice for Bit Twiddling Hacks +-------------------------------------- + +Bit Twiddling Hacks + +By Sean Eron Anderson +seander@cs.stanford.edu + +Individually, the code snippets here are in the public domain (unless otherwise +noted) — feel free to use them however you please. The aggregate collection and +descriptions are © 1997-2005 Sean Eron Anderson. The code and descriptions are +distributed in the hope that they will be useful, but WITHOUT ANY WARRANTY and +without even the implied warranty of merchantability or fitness for a particular +purpose. + +License notice for Brotli +-------------------------------------- + +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +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. + +compress_fragment.c: +Copyright (c) 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +decode_fuzzer.c: +Copyright (c) 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + +License notice for Json.NET +------------------------------- + +https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +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. + +License notice for vectorized base64 encoding / decoding +-------------------------------------------------------- + +Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2013-2017, Alfred Klomp +Copyright (c) 2015-2017, Wojciech Mula +Copyright (c) 2016-2017, Matthieu Darbois +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for vectorized hex parsing +-------------------------------------------------------- + +Copyright (c) 2022, Geoff Langdale +Copyright (c) 2022, Wojciech Mula +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for RFC 3492 +--------------------------- + +The punycode implementation is based on the sample code in RFC 3492 + +Copyright (C) The Internet Society (2003). All Rights Reserved. + +This document and translations of it may be copied and furnished to +others, and derivative works that comment on or otherwise explain it +or assist in its implementation may be prepared, copied, published +and distributed, in whole or in part, without restriction of any +kind, provided that the above copyright notice and this paragraph are +included on all such copies and derivative works. However, this +document itself may not be modified in any way, such as by removing +the copyright notice or references to the Internet Society or other +Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for +copyrights defined in the Internet Standards process must be +followed, or as required to translate it into languages other than +English. + +The limited permissions granted above are perpetual and will not be +revoked by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an +"AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING +TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION +HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF +MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Copyright(C) The Internet Society 1997. All Rights Reserved. + +This document and translations of it may be copied and furnished to others, +and derivative works that comment on or otherwise explain it or assist in +its implementation may be prepared, copied, published and distributed, in +whole or in part, without restriction of any kind, provided that the above +copyright notice and this paragraph are included on all such copies and +derivative works.However, this document itself may not be modified in any +way, such as by removing the copyright notice or references to the Internet +Society or other Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for copyrights +defined in the Internet Standards process must be followed, or as required +to translate it into languages other than English. + +The limited permissions granted above are perpetual and will not be revoked +by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an "AS IS" +basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK FORCE +DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY +RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A +PARTICULAR PURPOSE. + +License notice for Algorithm from RFC 4122 - +A Universally Unique IDentifier (UUID) URN Namespace +---------------------------------------------------- + +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & +Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1998 Microsoft. +To anyone who acknowledges that this file is provided "AS IS" +without any express or implied warranty: permission to use, copy, +modify, and distribute this file for any purpose is hereby +granted without fee, provided that the above copyright notices and +this notice appears in all source code copies, and that none of +the names of Open Software Foundation, Inc., Hewlett-Packard +Company, Microsoft, or Digital Equipment Corporation be used in +advertising or publicity pertaining to distribution of the software +without specific, written prior permission. Neither Open Software +Foundation, Inc., Hewlett-Packard Company, Microsoft, nor Digital +Equipment Corporation makes any representations about the +suitability of this software for any purpose." + +License notice for The LLVM Compiler Infrastructure (Legacy License) +-------------------------------------------------------------------- + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +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: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +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 +CONTRIBUTORS 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 WITH THE +SOFTWARE. + +License notice for Bob Jenkins +------------------------------ + +By Bob Jenkins, 1996. bob_jenkins@burtleburtle.net. You may use this +code any way you wish, private, educational, or commercial. It's free. + +License notice for Greg Parker +------------------------------ + +Greg Parker gparker@cs.stanford.edu December 2000 +This code is in the public domain and may be copied or modified without +permission. + +License notice for libunwind based code +---------------------------------------- + +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. + +License notice for Printing Floating-Point Numbers (Dragon4) +------------------------------------------------------------ + +/****************************************************************************** + Copyright (c) 2014 Ryan Juckett + http://www.ryanjuckett.com/ + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +******************************************************************************/ + +License notice for Printing Floating-point Numbers (Grisu3) +----------------------------------------------------------- + +Copyright 2012 the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xxHash +------------------------- + +xxHash - Extremely Fast Hash algorithm +Header File +Copyright (C) 2012-2021 Yann Collet + +BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You can contact the author at: + - xxHash homepage: https://www.xxhash.com + - xxHash source repository: https://github.com/Cyan4973/xxHash + +License notice for Berkeley SoftFloat Release 3e +------------------------------------------------ + +https://github.com/ucb-bar/berkeley-softfloat-3 +https://github.com/ucb-bar/berkeley-softfloat-3/blob/master/COPYING.txt + +License for Berkeley SoftFloat Release 3e + +John R. Hauser +2018 January 20 + +The following applies to the whole of SoftFloat Release 3e as well as to +each source file individually. + +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the +University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions, and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS", AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xoshiro RNGs +-------------------------------- + +Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) + +To the extent possible under law, the author has dedicated all copyright +and related and neighboring rights to this software to the public domain +worldwide. This software is distributed without any warranty. + +See . + +License for fastmod (https://github.com/lemire/fastmod), ibm-fpgen (https://github.com/nigeltao/parse-number-fxx-test-data) and fastrange (https://github.com/lemire/fastrange) +-------------------------------------- + + Copyright 2018 Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License for sse4-strstr (https://github.com/WojciechMula/sse4-strstr) +-------------------------------------- + + Copyright (c) 2008-2016, Wojciech Mula + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for The C++ REST SDK +----------------------------------- + +C++ REST SDK + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +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. + +License notice for MessagePack-CSharp +------------------------------------- + +MessagePack for C# + +MIT License + +Copyright (c) 2017 Yoshifumi Kawai + +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. + +License notice for lz4net +------------------------------------- + +lz4net + +Copyright (c) 2013-2017, Milosz Krajewski + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Nerdbank.Streams +----------------------------------- + +The MIT License (MIT) + +Copyright (c) Andrew Arnott + +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. + +License notice for RapidJSON +---------------------------- + +Tencent is pleased to support the open source community by making RapidJSON available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. + +Licensed under the MIT License (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + +http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for DirectX Math Library +--------------------------------------- + +https://github.com/microsoft/DirectXMath/blob/master/LICENSE + + The MIT License (MIT) + +Copyright (c) 2011-2020 Microsoft Corp + +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. + +License notice for ldap4net +--------------------------- + +The MIT License (MIT) + +Copyright (c) 2018 Alexander Chermyanin + +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. + +License notice for vectorized sorting code +------------------------------------------ + +MIT License + +Copyright (c) 2020 Dan Shechter + +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. + +License notice for musl +----------------------- + +musl as a whole is licensed under the following standard MIT license: + +Copyright © 2005-2020 Rich Felker, et al. + +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. + + +License notice for "Faster Unsigned Division by Constants" +------------------------------ + +Reference implementations of computing and using the "magic number" approach to dividing +by constants, including codegen instructions. The unsigned division incorporates the +"round down" optimization per ridiculous_fish. + +This is free and unencumbered software. Any copyright is dedicated to the Public Domain. + + +License notice for mimalloc +----------------------------------- + +MIT License + +Copyright (c) 2019 Microsoft Corporation, Daan Leijen + +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. + +License notice for The LLVM Project +----------------------------------- + +Copyright 2019 LLVM Project + +Licensed under the Apache License, Version 2.0 (the "License") with LLVM Exceptions; +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://llvm.org/LICENSE.txt + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for Apple header files +------------------------------------- + +Copyright (c) 1980, 1986, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by the University of + California, Berkeley and its contributors. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +License notice for JavaScript queues +------------------------------------- + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: +the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; +moral rights retained by the original author(s) and/or performer(s); +publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; +rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; +rights protecting the extraction, dissemination, use and reuse of data in a Work; +database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and +other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. +4. Limitations and Disclaimers. +a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. +b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. +c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. +d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + + +License notice for FastFloat algorithm +------------------------------------- +MIT License +Copyright (c) 2021 csFastFloat authors +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. + +License notice for MsQuic +-------------------------------------- + +Copyright (c) Microsoft Corporation. +Licensed under the MIT License. + +Available at +https://github.com/microsoft/msquic/blob/main/LICENSE + +License notice for m-ou-se/floatconv +------------------------------- + +Copyright (c) 2020 Mara Bos +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for code from The Practice of Programming +------------------------------- + +Copyright (C) 1999 Lucent Technologies + +Excerpted from 'The Practice of Programming +by Brian W. Kernighan and Rob Pike + +You may use this code for any purpose, as long as you leave the copyright notice and book citation attached. + +Notice for Euclidean Affine Functions and Applications to Calendar +Algorithms +------------------------------- + +Aspects of Date/Time processing based on algorithm described in "Euclidean Affine Functions and Applications to Calendar +Algorithms", Cassio Neri and Lorenz Schneider. https://arxiv.org/pdf/2102.06959.pdf + +License notice for amd/aocl-libm-ose +------------------------------- + +Copyright (C) 2008-2020 Advanced Micro Devices, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +License notice for fmtlib/fmt +------------------------------- + +Formatting library for C++ + +Copyright (c) 2012 - present, Victor Zverovich + +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. + +License for Jb Evain +--------------------- + +Copyright (c) 2006 Jb Evain (jbevain@gmail.com) + +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. + +--- Optional exception to the license --- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into a machine-executable object form of such +source code, you may redistribute such embedded portions in such object form +without including the above copyright and permission notices. + + +License for MurmurHash3 +-------------------------------------- + +https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp + +MurmurHash3 was written by Austin Appleby, and is placed in the public +domain. The author hereby disclaims copyright to this source + +License for Fast CRC Computation +-------------------------------------- + +https://github.com/intel/isa-l/blob/33a2d9484595c2d6516c920ce39a694c144ddf69/crc/crc32_ieee_by4.asm +https://github.com/intel/isa-l/blob/33a2d9484595c2d6516c920ce39a694c144ddf69/crc/crc64_ecma_norm_by8.asm + +Copyright(c) 2011-2015 Intel Corporation All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Intel Corporation nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License for C# Implementation of Fast CRC Computation +----------------------------------------------------- + +https://github.com/SixLabors/ImageSharp/blob/f4f689ce67ecbcc35cebddba5aacb603e6d1068a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs + +Copyright (c) Six Labors. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/SixLabors/ImageSharp/blob/f4f689ce67ecbcc35cebddba5aacb603e6d1068a/LICENSE diff --git a/OpenNest.Tests/OpenNest.Tests.csproj b/OpenNest.Tests/OpenNest.Tests.csproj index 844af08..125eaa6 100644 --- a/OpenNest.Tests/OpenNest.Tests.csproj +++ b/OpenNest.Tests/OpenNest.Tests.csproj @@ -29,6 +29,7 @@ + diff --git a/OpenNest.Tests/Reporting/AtomicReportFileTests.cs b/OpenNest.Tests/Reporting/AtomicReportFileTests.cs new file mode 100644 index 0000000..e82f707 --- /dev/null +++ b/OpenNest.Tests/Reporting/AtomicReportFileTests.cs @@ -0,0 +1,63 @@ +using OpenNest.Reporting; + +namespace OpenNest.Tests.Reporting; + +public sealed class AtomicReportFileTests : IDisposable +{ + private readonly string directory = Path.Combine(Path.GetTempPath(), "opennest-report-" + Guid.NewGuid().ToString("N")); + + public AtomicReportFileTests() => Directory.CreateDirectory(directory); + + [Fact] + public void SuccessfulRenderReplacesExistingFileOnlyAfterRendering() + { + var path = Path.Combine(directory, "existing.pdf"); + File.WriteAllText(path, "original"); + AtomicReportFile.Write(path, stream => + { + Assert.Equal("original", File.ReadAllText(path)); + stream.Write("replacement"u8); + }); + Assert.Equal("replacement", File.ReadAllText(path)); + Assert.Equal(new[] { path }, Directory.GetFiles(directory)); + } + + [Fact] + public void LateRenderFailurePreservesExistingFileAndRemovesTemporarySibling() + { + var path = Path.Combine(directory, "existing.pdf"); + File.WriteAllText(path, "original"); + Assert.Throws(() => AtomicReportFile.Write(path, stream => + { + stream.Write("partial PDF"u8); + Assert.Equal(2, Directory.GetFiles(directory).Length); + throw new IOException("injected late render failure"); + })); + Assert.Equal("original", File.ReadAllText(path)); + Assert.Equal(new[] { path }, Directory.GetFiles(directory)); + } + + [Fact] + public void FailedNewExportLeavesNoOutput() + { + var path = Path.Combine(directory, "new.pdf"); + Assert.Throws(() => AtomicReportFile.Write(path, stream => + { + stream.Write("partial"u8); + throw new IOException("injected write failure"); + })); + Assert.Empty(Directory.GetFiles(directory)); + } + + [Fact] + public void FailedReplacementPreservesDirectoryTargetAndCleansTemporaryFile() + { + var path = Path.Combine(directory, "not-a-file.pdf"); + Directory.CreateDirectory(path); + Assert.ThrowsAny(() => AtomicReportFile.Write(path, stream => stream.Write("PDF"u8))); + Assert.True(Directory.Exists(path)); + Assert.Empty(Directory.GetFiles(directory)); + } + + public void Dispose() => Directory.Delete(directory, true); +} diff --git a/OpenNest.Tests/Reporting/NestPdfWriterTests.cs b/OpenNest.Tests/Reporting/NestPdfWriterTests.cs new file mode 100644 index 0000000..7477618 --- /dev/null +++ b/OpenNest.Tests/Reporting/NestPdfWriterTests.cs @@ -0,0 +1,264 @@ +using System.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; +using OpenNest.Geometry; +using OpenNest.Reporting; +using PdfSharp.Pdf.IO; + +namespace OpenNest.Tests.Reporting; + +public sealed class NestPdfWriterTests : IDisposable +{ + private readonly string directory = Path.Combine(Path.GetTempPath(), "opennest-pdf-" + Guid.NewGuid().ToString("N")); + + public NestPdfWriterTests() => Directory.CreateDirectory(directory); + + [Fact] + public void Write_OnePlateJobProducesPortraitSummaryAndLandscapeVectorPlatePage() + { + var path = WriteReport(NestReportTestData.CreateNest()); + + using (var pdf = PdfReader.Open(path, PdfDocumentOpenMode.Import)) + { + Assert.Equal(2, pdf.PageCount); + Assert.Equal((612, 792), EffectiveSize(pdf.Pages[0])); + Assert.Equal((792, 612), EffectiveSize(pdf.Pages[1])); + } + var bytes = Encoding.Latin1.GetString(File.ReadAllBytes(path)); + // Subset-embedded faces; PDF names escape the space as #20. + Assert.Matches(@"/BaseFont\s*/[A-Z]{6}\+DejaVu#20Sans(?![,\w])", bytes); + Assert.Matches(@"/BaseFont\s*/[A-Z]{6}\+DejaVu#20Sans,Bold\b", bytes); + Assert.Contains("/FontFile2", bytes); + // Thumbnails and diagrams are vectors, never raster screenshots. + Assert.DoesNotMatch(@"/Subtype\s*/Image", bytes); + Assert.Matches(@"/Subtype\s*/Form", bytes); + } + + [SkippableFact] + public void Write_PdfTextCarriesDocumentLocalIdsQuantitiesAndPageNumbers() + { + var path = WriteReport(NestReportTestData.CreateNest()); + var summary = ExtractText(path, 1); + var plate = ExtractText(path, 2); + + Assert.Contains("Report test job", summary); + Assert.Contains("Test customer", summary); + Assert.Contains("Steel A36", summary); + Assert.Contains("0.125 in", summary); + Assert.Contains("2026-09-29 12:00 +00:00", summary); + Assert.Matches(@"Distinct layouts:\s+1", summary); + Assert.Matches(@"Total physical sheets:\s+2", summary); + Assert.Matches(@"1\s+48 x 24 in\s+2\s+4\s+\d+\.\d%", summary); + Assert.Matches(@"R001\s+Bracket\s+5\s+4\s+1\s+0\s+1", summary); + Assert.Matches(@"R002\s+Rotated\s+1\s+2\s+0\s+1\s+1", summary); + Assert.Matches(@"R003\s+Bracket\s+7\s+2\s+5\s+0\s+1", summary); + Assert.Matches(@"R004\s+Unplaced\s+3\s+0\s+3\s+0\s+-", summary); + Assert.Contains("Page 1 of 2", summary); + + Assert.Contains("Plate 1", plate); + Assert.Contains("Report test job", plate); + Assert.Matches(@"Copies:\s+2", plate); + Assert.Matches(@"R001\s+Bracket\s+2\s+4", plate); + Assert.Matches(@"R002\s+Rotated\s+1\s+2", plate); + Assert.Matches(@"R003\s+Bracket\s+1\s+2", plate); + Assert.DoesNotContain("R004", plate); + Assert.DoesNotContain("Cutoff test", plate + summary); + Assert.Contains("not a dimensioned cutting drawing", plate); + Assert.Contains("not a geometry or CNC approval", plate); + Assert.Contains("Page 2 of 2", plate); + + // Repeated export of the same snapshot is textually identical. + Assert.Equal(summary + plate, ExtractText(WriteReport(NestReportTestData.CreateNest(), "again.pdf"), 1) + + ExtractText(Path.Combine(directory, "again.pdf"), 2)); + } + + [SkippableFact] + public void Write_EmptyAndDemandOnlyJobsProduceExplicitSummaryOnlyReports() + { + var nest = new Nest("Empty test") { Units = Units.Millimeters }; + var empty = ExtractText(WriteReport(nest, "empty.pdf"), 1); + Assert.Contains("No plates in this job.", empty); + Assert.Contains("No parts in this job.", empty); + Assert.Contains("Page 1 of 1", empty); + + nest.Drawings.Add(NestReportTestData.Rectangle("Alpha", 20, 10, 2)); + var demand = ExtractText(WriteReport(nest, "demand.pdf"), 1); + Assert.Contains("No plates in this job.", demand); + Assert.Matches(@"R001\s+Alpha\s+2\s+0\s+2\s+0\s+-", demand); + Assert.Contains("Page 1 of 1", demand); + } + + [Fact] + public void Write_TabbedLeadInPartStrokesEachOpenContourSeparately() + { + var nest = NestReportTestData.CreateTabbedNest(); + var geometry = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt).Plates[0].Parts[0].Geometry; + Assert.True(geometry.StrokeOnly); + var path = WriteReport(nest); + using var pdf = PdfReader.Open(path, PdfDocumentOpenMode.Import); + Assert.Equal(2, pdf.PageCount); + // The vector diagram is the last content stream on the plate page. Each stroke-only + // contour must begin its own subpath; a missing move-to draws a false connecting line. + var diagram = ContentStreams(pdf.Pages[1]).Last(); + Assert.Equal(geometry.Contours.Length, Regex.Matches(diagram, @"(?m)^[-\d. ]+ m$").Count); + } + + [Fact] + public void Write_LateFailureLeavesExistingReportUnchangedAndNoTemporaryFile() + { + var path = Path.Combine(directory, "existing.report.pdf"); + var original = "%PDF previous report"u8.ToArray(); + File.WriteAllBytes(path, original); + var snapshot = NestReportBuilder.Capture(NestReportTestData.CreateNest(), NestReportTestData.GeneratedAt); + + Assert.Throws(() => NestPdfWriter.Write(snapshot, path, stream => new FailingStream(stream, 512))); + + Assert.Equal(original, File.ReadAllBytes(path)); + Assert.Equal(new[] { path }, Directory.GetFiles(directory)); + } + + [Fact] + public void Write_SuccessfulExportReplacesExistingReport() + { + var path = Path.Combine(directory, "existing.report.pdf"); + File.WriteAllText(path, "old"); + WriteReport(NestReportTestData.CreateNest(), "existing.report.pdf"); + Assert.StartsWith("%PDF-", Encoding.ASCII.GetString(File.ReadAllBytes(path), 0, 5)); + Assert.Equal(new[] { path }, Directory.GetFiles(directory)); + } + + [Theory] + [InlineData("second-plate")] + [InlineData("summary-overflow")] + [InlineData("plate-table-overflow")] + [InlineData("illegible-label")] + public void Write_UnsupportedLayoutFailsBeforeReplacingDestination(string scenario) + { + var nest = NestReportTestData.CreateNest(); + var plate = nest.Plates[0]; + switch (scenario) + { + case "second-plate": + nest.Plates.Add(new Plate(24, 48) { Quantity = 1 }); + nest.Plates[1].Parts.Add(new Part(plate.Parts[0].BaseDrawing)); + break; + case "summary-overflow": + for (var i = 0; i < 40; i++) + nest.Drawings.Add(NestReportTestData.Rectangle($"Demand {i:D2}", 1, 1)); + break; + case "plate-table-overflow": + for (var i = 0; i < 12; i++) + // Large enough for readable labels, so only the table can overflow. + plate.Parts.Add(new Part(NestReportTestData.Rectangle($"Small {i:D2}", 2, 2), new Vector(2 + i * 3, 16))); + break; + case "illegible-label": + plate.Parts.Add(new Part(NestReportTestData.Rectangle("Tiny", 0.05, 0.05), new Vector(44, 20))); + break; + } + var path = Path.Combine(directory, "keep.report.pdf"); + File.WriteAllText(path, "keep"); + var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + + var error = Assert.Throws(() => NestPdfWriter.Write(snapshot, path)); + + Assert.Contains("not supported", error.Message); + Assert.Equal("keep", File.ReadAllText(path)); + Assert.Equal(new[] { path }, Directory.GetFiles(directory)); + } + + [Fact] + public void Write_UnsupportedTextIsIdentifiedBeforeReplacingDestination() + { + var nest = NestReportTestData.CreateNest(); + nest.Plates[0].Parts[2].BaseDrawing.Name = "Rotated 中"; + var path = Path.Combine(directory, "keep.report.pdf"); + File.WriteAllText(path, "keep"); + var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + + var error = Assert.Throws(() => NestPdfWriter.Write(snapshot, path)); + + Assert.Contains("R002", error.Message); + Assert.Contains("U+4E2D", error.Message); + Assert.Equal("keep", File.ReadAllText(path)); + Assert.Equal(new[] { path }, Directory.GetFiles(directory)); + } + + public void Dispose() => Directory.Delete(directory, true); + + private string WriteReport(Nest nest, string name = "test.report.pdf") + { + var path = Path.Combine(directory, name); + NestPdfWriter.Write(NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt), path); + return path; + } + + private static (int, int) EffectiveSize(PdfSharp.Pdf.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); + } + + private static IEnumerable ContentStreams(PdfSharp.Pdf.PdfPage page) + { + foreach (var item in page.Contents.Elements) + { + var dictionary = (item as PdfSharp.Pdf.Advanced.PdfReference)?.Value as PdfSharp.Pdf.PdfDictionary + ?? item as PdfSharp.Pdf.PdfDictionary; + if (dictionary?.Stream != null) + yield return Encoding.Latin1.GetString(dictionary.Stream.UnfilteredValue); + } + } + + /// Poppler text extraction; skipped where poppler-utils is not installed. + private static string ExtractText(string path, int page) + { + var info = new ProcessStartInfo("pdftotext") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + foreach (var argument in new[] { "-layout", "-f", page.ToString(), "-l", page.ToString(), path, "-" }) + info.ArgumentList.Add(argument); + Process process; + try + { + process = Process.Start(info)!; + } + catch (System.ComponentModel.Win32Exception) + { + Skip.If(true, "pdftotext (poppler-utils) is not installed."); + throw; + } + using (process) + { + var text = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + Assert.Equal(0, process.ExitCode); + return Regex.Replace(text, "[ \t]+\n", "\n"); + } + } + + private sealed class FailingStream(Stream inner, long limit) : Stream + { + private long written; + public override bool CanRead => false; + public override bool CanSeek => inner.CanSeek; + public override bool CanWrite => true; + public override long Length => inner.Length; + public override long Position { get => inner.Position; set => inner.Position = value; } + public override void Flush() => inner.Flush(); + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin); + public override void SetLength(long value) => inner.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + { + if (written + count > limit) + throw new IOException("injected late report write failure"); + written += count; + inner.Write(buffer, offset, count); + } + } +} diff --git a/OpenNest.Tests/Reporting/NestReportBuilderTests.cs b/OpenNest.Tests/Reporting/NestReportBuilderTests.cs new file mode 100644 index 0000000..481ace8 --- /dev/null +++ b/OpenNest.Tests/Reporting/NestReportBuilderTests.cs @@ -0,0 +1,167 @@ +using System.Text.Json; +using OpenNest.CNC; +using OpenNest.Geometry; +using OpenNest.IO; +using OpenNest.Reporting; + +namespace OpenNest.Tests.Reporting; + +public class NestReportBuilderTests +{ + [Fact] + public void Capture_ReconcilesOrderedReferenceUnionWithoutCountingCutoffs() + { + var nest = NestReportTestData.CreateNest(); + var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + + Assert.Equal("Report test job", snapshot.Name); + Assert.Equal("Test customer", snapshot.Customer); + Assert.Equal("Synthetic report test data", snapshot.Notes); + Assert.Equal("Steel", snapshot.Material); + Assert.Equal("A36", snapshot.Grade); + Assert.Equal(0.125, snapshot.Thickness); + Assert.Equal("in", snapshot.Units); + Assert.Equal(NestReportTestData.GeneratedAt, snapshot.GeneratedAt); + Assert.Equal(new[] { "R001", "R002", "R003", "R004" }, snapshot.Drawings.Select(d => d.Id)); + Assert.Equal(new[] { "Bracket", "Rotated", "Bracket", "Unplaced" }, snapshot.Drawings.Select(d => d.Name)); + Assert.Equal(new long[] { 5, 1, 7, 3 }, snapshot.Drawings.Select(d => d.Required)); + Assert.Equal(new long[] { 4, 2, 2, 0 }, snapshot.Drawings.Select(d => d.Nested)); + Assert.Equal(new long[] { 1, 0, 5, 3 }, snapshot.Drawings.Select(d => d.Shortage)); + Assert.Equal(new long[] { 0, 1, 0, 0 }, snapshot.Drawings.Select(d => d.Extra)); + Assert.All(snapshot.Drawings.Take(3), d => Assert.Equal(new[] { 1 }, d.Plates)); + Assert.Empty(snapshot.Drawings[3].Plates); + var plate = Assert.Single(snapshot.Plates); + Assert.Equal(new[] { "R001", "R001", "R002", "R003" }, plate.Parts.Select(p => p.ReportId)); + Assert.Single(plate.Cutoffs); + Assert.Equal(2, snapshot.TotalSheets); + Assert.Equal(new ReportBounds(0, 0, 48, 24), plate.Bounds); + Assert.Equal(0.125, plate.PartSpacing); + Assert.Equal(nest.Plates[0].Utilization(), plate.Utilization); + } + + [Fact] + public void Capture_RecountsAcrossPlateCopiesWithWideIntegersAndUniquePlateNumbers() + { + var nest = NestReportTestData.CreateNest(); + var drawing = nest.Plates[0].Parts[0].BaseDrawing; + // Change copies after adding: the legacy cached Nested count stays stale. + nest.Plates[0].Quantity = int.MaxValue; + var second = new Plate(24, 48) { Quantity = 3 }; + second.Parts.Add(new Part(drawing)); + second.Parts.Add(new Part(drawing)); + nest.Plates.Add(second); + var cached = drawing.Quantity.Nested; + + var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + + Assert.Equal(checked(2L * int.MaxValue + 6), snapshot.Drawings[0].Nested); + Assert.Equal(new[] { 1, 2 }, snapshot.Drawings[0].Plates); + Assert.Equal(checked((long)int.MaxValue + 3), snapshot.TotalSheets); + Assert.Equal(cached, drawing.Quantity.Nested); + } + + [Fact] + public void Capture_IsRepeatableDoesNotMutateSourcesAndRetainsNoMutableReferences() + { + var nest = NestReportTestData.CreateNest(); + var part = nest.Plates[0].Parts[0]; + var drawing = part.BaseDrawing; + var source = Fingerprint(nest); + var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + var serialized = JsonSerializer.Serialize(snapshot); + + Assert.Equal(source, Fingerprint(nest)); + Assert.Equal(serialized, JsonSerializer.Serialize(NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt))); + Assert.NotEmpty(snapshot.Plates); + nest.Name = "Edited"; + nest.Material.Name = "Edited material"; + nest.Plates[0].Size = new Size(100, 200); + nest.Plates[0].Quantity = 42; + part.Location = new Vector(500, 600); + ((LinearMove)part.Program.Codes[1]).EndPoint = new Vector(99, 99); + ((LinearMove)drawing.Program.Codes[1]).EndPoint = new Vector(88, 88); + drawing.Name = "Edited drawing"; + drawing.Quantity.Required = 123; + nest.Drawings.Clear(); + nest.Plates.Clear(); + Assert.Equal(serialized, JsonSerializer.Serialize(snapshot)); + } + + [Fact] + public void Capture_EmptyAndDemandOnlyJobsKeepExplicitEmptyCollectionsAndOrdinalOrder() + { + var nest = new Nest("Empty test"); + var empty = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + Assert.Equal("Empty test", empty.Name); + Assert.Empty(empty.Drawings); + Assert.Empty(empty.Plates); + Assert.Equal(0, empty.TotalSheets); + nest.Drawings.Add(NestReportTestData.Rectangle("zeta", 1, 1, 5)); + nest.Drawings.Add(NestReportTestData.Rectangle("Alpha", 1, 1, 2)); + var demand = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + Assert.Equal(new[] { "Alpha", "zeta" }, demand.Drawings.Select(d => d.Name)); + Assert.All(demand.Drawings, d => { Assert.Equal(0, d.Nested); Assert.Equal(d.Required, d.Shortage); }); + Assert.Empty(demand.Plates); + } + + [Theory] + [InlineData(0, 48, 1)] + [InlineData(24, -1, 1)] + [InlineData(double.NaN, 48, 1)] + [InlineData(24, double.PositiveInfinity, 1)] + [InlineData(24, 48, 0)] + [InlineData(24, 48, -1)] + public void Capture_RejectsInvalidPlateBeforeProducingSnapshot(double width, double length, int copies) + { + var nest = NestReportTestData.CreateNest(); + nest.Plates[0].Size = new Size(width, length); + nest.Plates[0].Quantity = copies; + var before = Fingerprint(nest); + var error = Assert.Throws(() => NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt)); + Assert.Contains("Plate 1", error.Message); + Assert.Equal(before, Fingerprint(nest)); + } + + [Theory] + [InlineData(1, 0, 0, 48, 24)] + [InlineData(2, -48, 0, 0, 24)] + [InlineData(3, -48, -24, 0, 0)] + [InlineData(4, 0, -24, 48, 0)] + public void Capture_UsesQuadrantAwareStockBounds(int quadrant, double left, double bottom, double right, double top) + { + var nest = NestReportTestData.CreateNest(); + nest.Plates[0].Quadrant = quadrant; + var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + Assert.Equal(new ReportBounds(left, bottom, right, top), Assert.Single(snapshot.Plates).Bounds); + } + + internal static string Fingerprint(Nest nest) => JsonSerializer.Serialize(new + { + nest.Name, + nest.Customer, + nest.Notes, + nest.Material, + nest.Thickness, + nest.Units, + nest.DateCreated, + nest.DateLastModified, + Drawings = nest.Drawings.Select(d => new { d.Id, d.Name, d.Quantity, Main = NestWriter.GetProgramText(d.Program), Subs = NestWriter.GetSubProgramsText(d.Program) }), + Plates = nest.Plates.Select(p => new + { + p.Size, + p.Quantity, + p.PartSpacing, + p.Quadrant, + Parts = p.Parts.Select(part => new + { + part.BaseDrawing.Id, + part.Location, + part.Rotation, + part.HasManualLeadIns, + part.LeadInsLocked, + Main = NestWriter.GetProgramText(part.Program), + Subs = NestWriter.GetSubProgramsText(part.Program), + }), + }), + }, new JsonSerializerOptions { IncludeFields = true, NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals }); +} diff --git a/OpenNest.Tests/Reporting/NestReportGeometryTests.cs b/OpenNest.Tests/Reporting/NestReportGeometryTests.cs new file mode 100644 index 0000000..01997f7 --- /dev/null +++ b/OpenNest.Tests/Reporting/NestReportGeometryTests.cs @@ -0,0 +1,162 @@ +using System.Text.Json; +using OpenNest.CNC; +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.IO; +using OpenNest.Reporting; + +namespace OpenNest.Tests.Reporting; + +public class NestReportGeometryTests +{ + [Fact] + public void Capture_PreservesNativeCircleHoleAndAppliesBakedRotationAndLocationOnce() + { + var nest = NestReportTestData.CreateNest(); + var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + var holed = snapshot.Plates[0].Parts[0].Geometry; + Assert.False(holed.StrokeOnly); + Assert.Equal(2, holed.Contours.Length); + Assert.All(holed.Contours, c => Assert.True(c.Closed)); + var circle = Assert.Single(holed.Contours.SelectMany(c => c.Segments).Where(s => s.Center != null)); + Assert.Equal(new ReportPoint(7, 7), circle.Center); + Assert.Equal(1, circle.Radius); + Assert.Equal(-2 * System.Math.PI, circle.SweepAngle); + Assert.Equal(new ReportBounds(2, 2, 12, 12), holed.Bounds); + var turned = snapshot.Plates[0].Parts[2].Geometry; + AssertBounds(new ReportBounds(28, 2, 30, 6), turned.Bounds); + AssertBounds(new ReportBounds(0, 0, 4, 2), snapshot.Drawings[1].Geometry.Bounds); + } + + [Fact] + public void Capture_RealTabbedLeadInHoleRemainsOpenAndNeverMutatesPrograms() + { + var nest = NestReportTestData.CreateTabbedNest(); + var part = nest.Plates[0].Parts[0]; + var converted = ConvertProgram.ToGeometry(part.Program); + Assert.Contains(converted, e => e.Layer == SpecialLayers.Leadin); + Assert.Contains(converted, e => e.Layer == SpecialLayers.Leadout); + Assert.NotEmpty(part.Program.SubPrograms); + // Deliberately stale settings must not control closure detection. + part.CuttingParameters.TabsEnabled = false; + var before = NestReportBuilderTests.Fingerprint(nest); + + var snapshot = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt); + var geometry = Assert.Single(snapshot.Plates[0].Parts).Geometry; + + Assert.True(geometry.StrokeOnly); + var open = Assert.Single(geometry.Contours.Where(c => !c.Closed)); + Assert.NotEqual(open.Segments[0].Start, open.Segments[^1].End); + var gapX = open.Segments[^1].End.X - open.Segments[0].Start.X; + var gapY = open.Segments[^1].End.Y - open.Segments[0].Start.Y; + Assert.Equal(0.15, System.Math.Sqrt(gapX * gapX + gapY * gapY), 6); + Assert.Single(geometry.Contours.Where(c => c.Closed)); + AssertBounds(new ReportBounds(10, 5, 20, 15), geometry.Bounds); + var hole = Assert.Single(geometry.Contours.SelectMany(c => c.Segments).Where(s => s.Center != null)); + Assert.Equal(15, hole.Center!.X, 6); + Assert.Equal(10, hole.Center.Y, 6); + var outlineCount = converted.Count(e => SpecialLayers.IsMaterial(e.Layer) && e.Layer != SpecialLayers.Leadin && e.Layer != SpecialLayers.Leadout); + Assert.Equal(outlineCount, geometry.Contours.Sum(c => c.Segments.Length)); + Assert.False(snapshot.Drawings[0].Geometry.StrokeOnly); + Assert.Equal(before, NestReportBuilderTests.Fingerprint(nest)); + } + + [Fact] + public void Capture_PreservesNativeArcSweepAndTrueBounds() + { + var nest = NestReportTestData.CreateNest(); + var program = nest.Plates[0].Parts[0].Program; + program.Codes.Clear(); + program.Codes.Add(new RapidMove(2, 0)); + program.Codes.Add(new ArcMove(0, 2, 0, 0, RotationType.CCW)); + var geometry = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt).Plates[0].Parts[0].Geometry; + var arc = Assert.Single(Assert.Single(geometry.Contours).Segments); + Assert.Equal(new ReportPoint(2, 2), arc.Center); + Assert.Equal(2, arc.Radius); + Assert.Equal(0, arc.StartAngle); + Assert.Equal(System.Math.PI / 2, arc.SweepAngle); + AssertBounds(new ReportBounds(2, 2, 4, 4), geometry.Bounds); + Assert.True(geometry.StrokeOnly); + } + + [Fact] + public void Capture_ExcludedLayersCannotConnectSeparateMaterialPaths() + { + var nest = NestReportTestData.CreateNest(); + var codes = nest.Plates[0].Parts[0].Program.Codes; + codes.Clear(); + codes.Add(new RapidMove(0, 0)); + codes.Add(new LinearMove(1, 0) { Layer = LayerType.Display }); + codes.Add(new LinearMove(1, 1) { Layer = LayerType.Scribe }); + codes.Add(new LinearMove(2, 1)); + codes.Add(new LinearMove(100, 100) { Layer = LayerType.Leadout }); + var geometry = NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt).Plates[0].Parts[0].Geometry; + Assert.Equal(2, geometry.Contours.Length); + Assert.All(geometry.Contours, c => { Assert.False(c.Closed); Assert.Single(c.Segments); }); + Assert.Equal(new ReportBounds(2, 2, 4, 3), geometry.Bounds); + } + + [Theory] + [InlineData("missing")] + [InlineData("nonfinite")] + [InlineData("arc-center")] + [InlineData("zero-radius")] + [InlineData("unequal-radii")] + [InlineData("rotation")] + [InlineData("unsupported")] + [InlineData("null-code")] + [InlineData("null-subprogram")] + [InlineData("cyclic-subprogram")] + [InlineData("nonfinite-offset")] + public void Capture_RejectsMalformedPlacedGeometryWithIdentification(string damage) + { + var nest = NestReportTestData.CreateNest(); + var part = nest.Plates[0].Parts[0]; + var codes = part.Program.Codes; + switch (damage) + { + case "missing": codes.Clear(); break; + case "nonfinite": ((LinearMove)codes[1]).EndPoint = new Vector(double.NaN, 1); break; + case "arc-center": codes.Add(new ArcMove(1, 0, double.PositiveInfinity, 0)); break; + case "zero-radius": codes.Add(new ArcMove(6, 5, 6, 5)); break; + case "unequal-radii": codes.Add(new ArcMove(10, 5, 5, 5)); break; + case "rotation": codes.Add(new ArcMove(6, 5, 5, 5, (RotationType)42)); break; + case "unsupported": codes.Add(new UnsupportedCode()); break; + case "null-code": codes.Add(null!); break; + case "null-subprogram": codes.Add(new SubProgramCall { Id = 42 }); break; + case "cyclic-subprogram": codes.Add(new SubProgramCall { Id = 42, Program = part.Program }); break; + case "nonfinite-offset": part.Location = new Vector(double.NaN, 0); break; + } + var count = codes.Count; + var error = Assert.Throws(() => NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt)); + Assert.Contains("Plate 1", error.Message); + Assert.Contains("part 1", error.Message); + Assert.Contains("Bracket", error.Message); + Assert.Equal(count, codes.Count); + } + + [Fact] + public void Capture_RejectsUnrenderableDemandOnlyDrawingByName() + { + var nest = new Nest(); + var drawing = new Drawing("Missing demand geometry"); + drawing.Quantity.Required = 3; + nest.Drawings.Add(drawing); + var error = Assert.Throws(() => NestReportBuilder.Capture(nest, NestReportTestData.GeneratedAt)); + Assert.Contains("Missing demand geometry", error.Message); + } + + private static void AssertBounds(ReportBounds expected, ReportBounds actual) + { + Assert.Equal(expected.Left, actual.Left, 6); + Assert.Equal(expected.Bottom, actual.Bottom, 6); + Assert.Equal(expected.Right, actual.Right, 6); + Assert.Equal(expected.Top, actual.Top, 6); + } + + private sealed class UnsupportedCode : ICode + { + public CodeType Type => (CodeType)999; + public ICode Clone() => new UnsupportedCode(); + } +} diff --git a/OpenNest.Tests/Reporting/NestReportTestData.cs b/OpenNest.Tests/Reporting/NestReportTestData.cs new file mode 100644 index 0000000..c64fb75 --- /dev/null +++ b/OpenNest.Tests/Reporting/NestReportTestData.cs @@ -0,0 +1,101 @@ +using OpenNest.CNC; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Geometry; + +namespace OpenNest.Tests.Reporting; + +/// Small, synthetic report examples; never production job data. +public static class NestReportTestData +{ + public static DateTimeOffset GeneratedAt { get; } = new(2026, 9, 29, 12, 0, 0, TimeSpan.Zero); + + public static Nest CreateNest() + { + var nest = new Nest("Report test job") + { + Customer = "Test customer", + Notes = "Synthetic report test data", + Material = new Material("Steel", "A36"), + Thickness = 0.125, + Units = Units.Inches, + DateCreated = new DateTime(2026, 9, 1), + DateLastModified = new DateTime(2026, 9, 28), + }; + var holed = CreateHoledDrawing("Bracket", 5); + var rotated = Rectangle("Rotated", 4, 2, 1); + var sameName = Rectangle("Bracket", 3, 2, 7); + var unplaced = Rectangle("Unplaced", 2, 1, 3); + nest.Drawings.Add(unplaced); + nest.Drawings.Add(rotated); + nest.Drawings.Add(holed); + // DrawingCollection is name-keyed; this reference deliberately exists only in placements. + var plate = new Plate(24, 48) { Quantity = 2, PartSpacing = 0.125 }; + plate.Parts.Add(new Part(holed, new Vector(2, 2))); + plate.Parts.Add(new Part(holed, new Vector(14, 2))); + var turned = new Part(rotated); + turned.Rotate(System.Math.PI / 2); + turned.Location = new Vector(30, 2); + plate.Parts.Add(turned); + plate.Parts.Add(new Part(sameName, new Vector(32, 2))); + var cutoffProgram = new Program(); + cutoffProgram.Codes.Add(new RapidMove(40, 0)); + cutoffProgram.Codes.Add(new LinearMove(40, 24)); + var cutoff = new Drawing("Cutoff test", cutoffProgram) { IsCutOff = true }; + cutoff.Quantity.Required = 99; + plate.Parts.Add(new Part(cutoff)); + nest.Drawings.Add(cutoff); + nest.Plates.Add(plate); + return nest; + } + + public static Nest CreateTabbedNest() + { + var drawing = CreateHoledDrawing("Tabbed with hole", 1); + var part = new Part(drawing); + part.Rotate(System.Math.PI / 2); + part.Offset(20, 5); + part.ApplyLeadIns(new CuttingParameters + { + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, + ExternalLeadOut = new LineLeadOut { Length = 0.25 }, + ArcCircleLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 90 }, + TabsEnabled = true, + TabConfig = new NormalTab { Size = 0.15 }, + }, new Vector(-5, -5)); + part.LeadInsLocked = true; + var nest = new Nest("Tabbed report test") { Units = Units.Inches }; + nest.Drawings.Add(drawing); + var plate = new Plate(24, 48) { Quantity = 1 }; + plate.Parts.Add(part); + nest.Plates.Add(plate); + return nest; + } + + public static Drawing CreateHoledDrawing(string name, int required) + { + var program = RectangleProgram(10, 10); + program.Codes.Add(new RapidMove(6, 5)); + program.Codes.Add(new ArcMove(new Vector(6, 5), new Vector(5, 5), RotationType.CW)); + var drawing = new Drawing(name, program); + drawing.Quantity.Required = required; + return drawing; + } + + public static Drawing Rectangle(string name, double length, double width, int required = 1) + { + var drawing = new Drawing(name, RectangleProgram(length, width)); + drawing.Quantity.Required = required; + return drawing; + } + + private static Program RectangleProgram(double length, double width) + { + var program = new Program(); + program.Codes.Add(new RapidMove(0, 0)); + program.Codes.Add(new LinearMove(0, width)); + program.Codes.Add(new LinearMove(length, width)); + program.Codes.Add(new LinearMove(length, 0)); + program.Codes.Add(new LinearMove(0, 0)); + return program; + } +} diff --git a/OpenNest.Tests/Reporting/ReportFontsTests.cs b/OpenNest.Tests/Reporting/ReportFontsTests.cs new file mode 100644 index 0000000..6acc99e --- /dev/null +++ b/OpenNest.Tests/Reporting/ReportFontsTests.cs @@ -0,0 +1,47 @@ +using OpenNest.Reporting; +using PdfSharp.Drawing; +using PdfSharp.Fonts; +using PdfSharp.Pdf; + +namespace OpenNest.Tests.Reporting; + +public class ReportFontsTests +{ + [Fact] + public void BundledFontsEmbedWithoutSystemFontResolutionAcrossRepeatedExports() + { + ReportFonts.Initialize(); + var resolver = GlobalFontSettings.FontResolver; + for (var i = 0; i < 2; i++) + { + ReportFonts.Initialize(); + Assert.Same(resolver, GlobalFontSettings.FontResolver); + using var doc = new PdfDocument(); + var page = doc.AddPage(); + using (var gfx = XGraphics.FromPdfPage(page)) + { + gfx.DrawString("Résumé Ø TEST DATA", new XFont(ReportFonts.Family, 10), XBrushes.Black, 20, 20); + gfx.DrawString("Bold", new XFont(ReportFonts.Family, 10, XFontStyleEx.Bold), XBrushes.Black, 20, 40); + } + using var stream = new MemoryStream(); + doc.Save(stream, false); + Assert.True(stream.Length > 1000); + } + } + + [Theory] + [InlineData("ASCII, 0.25 in; R001\nRésumé Ø Æ")] + [InlineData("")] + public void SupportedTextPasses(string text) => ReportFonts.ValidateText(text, "part R001"); + + [Theory] + [InlineData("中文")] + [InlineData("bad\u0000text")] + [InlineData("hidden\u00ADhyphen")] + public void UnsupportedTextHasAnIdentifiedErrorInsteadOfMissingGlyphs(string text) + { + var error = Assert.Throws(() => ReportFonts.ValidateText(text, "part R001")); + Assert.Contains("part R001", error.Message); + Assert.Contains("U+", error.Message); + } +} diff --git a/OpenNest.sln b/OpenNest.sln index acd7290..07707ba 100644 --- a/OpenNest.sln +++ b/OpenNest.sln @@ -46,6 +46,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.WinForms.Tests", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.FrontEnd.Tests", "OpenNest.FrontEnd.Tests\OpenNest.FrontEnd.Tests.csproj", "{0CD494FD-F37F-420D-B2C6-DC1E0D50BCEB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Reporting", "OpenNest.Reporting\OpenNest.Reporting.csproj", "{5C3708F5-DD69-436D-B412-7FF360DDB3DC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -284,6 +286,18 @@ Global {0CD494FD-F37F-420D-B2C6-DC1E0D50BCEB}.Release|x64.Build.0 = Release|Any CPU {0CD494FD-F37F-420D-B2C6-DC1E0D50BCEB}.Release|x86.ActiveCfg = Release|Any CPU {0CD494FD-F37F-420D-B2C6-DC1E0D50BCEB}.Release|x86.Build.0 = Release|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Debug|x64.Build.0 = Debug|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Debug|x86.Build.0 = Debug|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Release|Any CPU.Build.0 = Release|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Release|x64.ActiveCfg = Release|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Release|x64.Build.0 = Release|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Release|x86.ActiveCfg = Release|Any CPU + {5C3708F5-DD69-436D-B412-7FF360DDB3DC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/nest-reports.md b/docs/nest-reports.md new file mode 100644 index 0000000..238879a --- /dev/null +++ b/docs/nest-reports.md @@ -0,0 +1,116 @@ +# Nest report PDF library + +The first delivery is a bounded, cross-platform library slice, not yet a desktop +menu command or a general report layout engine. Capture a stable `Nest` on its +owning thread, then render only the detached snapshot: + +```csharp +var snapshot = NestReportBuilder.Capture(nest, DateTimeOffset.Now); +NestPdfWriter.Write(snapshot, destinationPath); +``` + +Reporting does not select a post, generate CNC, refresh drawing quantities, change +the selected plate, or certify that a layout passed geometry/pre-post checks. + +## Snapshot contract + +- IDs (`R001`, ...) are local to the report. Visit non-cutoff placements in plate + and part order, then append unseen demanded drawings ordered by ordinal name and + source path. Identity is by reference: distinct same-named drawings stay distinct, + including placed drawings absent from the name-keyed drawing collection. +- Recount nested quantities from placements times sheet copies exactly once using + checked wide integers. Include unplaced demand; report shortage and extra + separately. Cached `Quantity.Nested` is not authoritative. +- Distinguish distinct layouts from physical sheets. Per-sheet quantities do not + include copies; total quantities do. Utilization uses net part area divided by + full sheet area, excluding cutoffs. +- Actual placed programs already contain rotations. Capture their material paths + with the location applied once. Preserve holes and intentional tab gaps; exclude + rapid, scribe, lead-in and lead-out paths. Keep Cut and Display contours. +- If any material contour of a part is open, show all its contours without fill. + Do not close the gap or substitute the drawing's nominal outline. Cutoffs are + separate strokes with no product ID or quantity. +- Snapshots retain values, not live drawings, parts, CNC programs or mutable + geometry. Invalid/missing/nonfinite geometry fails with drawing/plate context. + +## Initial layout and safety limits + +Slice 1 supports an empty/demand-only job or one plate layout: a Letter portrait +summary page and a Letter landscape plate page, each with "Page X of Y". The +writer rejects, with `NotSupportedException` and before touching the destination: + +- more than one distinct plate layout; +- a summary (job fields, plate list, part rows with thumbnails) longer than one page; +- a plate page (header, diagram, part table) longer than one page; +- a part ID label that does not fit inside its part's fitted bounds at 7 pt, or + that overlaps another label. + +It never shrinks text, truncates rows or drops labels to make a layout fit. +Multi-plate pagination, dense-label callouts and the desktop command belong to +subsequent slices. Labels are centered on the part's bounds, which can place them +inside a central hole; smarter placement is part of the dense-label work. + +Summary thumbnails and the sheet diagram are vector paths, never raster images. +Each thumbnail is a small PDFsharp page embedded by MigraDoc as a form XObject. +The diagram is drawn into a fixed-height table row reserved in MigraDoc's flow, +located after layout with `DocumentRenderer.GetRenderInfoFromPage`. Closed parts +use one even-odd filled path, so holes stay unfilled. Parts with any open contour, +and cutoffs (dashed), stroke each contour as its own path: PDFsharp's +`StartFigure` does not start a new subpath after an open figure, and would +otherwise draw a false segment across a tab gap. + +A report is fully rendered to a unique temporary sibling before replacement of +its destination. A failed render or write leaves an existing report untouched and +removes temporary output. Applications should obtain overwrite consent before +calling the library. + +Advanced timing, cutting distances, pierce counts, weights, costs, gas use, and +machine/NC identity are intentionally omitted until their semantics are verified. +The PDF can be opened or printed through a normal PDF viewer; its fitted diagram +is not a dimensioned cutting drawing. + +## Backend, fonts and redistribution + +- Official `PDFsharp-MigraDoc` Core **6.2.4**, with `PDFsharp` **6.2.4**, targeting + net8.0. No GDI/WPF dependency or printer driver is used. MigraDoc owns flowing + document layout; PDFsharp draws vectors into its measured reserved areas. +- DejaVu Sans **2.37**, regular and bold, is bundled unmodified as assembly + resources. Source: https://dejavu-fonts.github.io/ ; binary provenance: + Ubuntu `fonts-dejavu-core` **2.37-8**. Both required faces are embedded in PDFs; + neither generation nor viewing needs a platform font installation. +- A custom resolver is installed once before fonts are created. Repeated exports + reuse it. An already-installed foreign resolver is an explicit integration + error, never silently replaced. A host that already uses PDFsharp needs resolver + composition before adding report support. MigraDoc's predefined error font is + pointed at the bundled family; its default (`Courier New`) is a platform font + the bundled resolver deliberately cannot supply. +- The initial text contract is printable ASCII/Latin-1 and line/tab separators. + Other codepoints, controls and soft hyphens fail with the field and codepoint; + there is no silent missing-glyph substitution. Broader Unicode/shaping support + is deferred rather than implied by the font's larger character inventory. +- PDFsharp/MigraDoc are MIT-licensed. The actual v6.2.4 license and the Microsoft + transitive dependency notices are in `OpenNest.Reporting/THIRD-PARTY-NOTICES.txt`. + Font permission/redistribution terms are in `OpenNest.Reporting/Fonts/LICENSE.txt` + (Bitstream Vera permission with DejaVu changes in the public domain). Preserve + both files in `ReportingNotices/` when publishing; embedded fonts do not replace + the obligation to ship their notices. Package notices are deduplicated, not + rewritten or shortened. + +## Verification + +```sh +dotnet test OpenNest.Tests/OpenNest.Tests.csproj --filter FullyQualifiedName~Reporting +``` + +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`, +`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. +Pixel inspection supplements geometry assertions; it is not a CNC-validation +result. + +Windows compilation is not runtime acceptance. The later desktop adapter must be +tested on Windows for busy-operation guards, cancellation, overwrite handling, +unchanged job state and PDF viewing/printing, including the packaged application.