From 285c6f0481b8395bc5a743ad96fa532812e74c61 Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Mon, 4 May 2026 09:52:51 +0100 Subject: [PATCH 1/7] Update NuGet packages and bump version --- Tabula.Csv/Tabula.Csv.csproj | 52 ++++++++++++++++---------------- Tabula.Json/Tabula.Json.csproj | 52 ++++++++++++++++---------------- Tabula.Tests/Tabula.Tests.csproj | 4 +-- Tabula/Tabula.csproj | 52 ++++++++++++++++---------------- 4 files changed, 80 insertions(+), 80 deletions(-) diff --git a/Tabula.Csv/Tabula.Csv.csproj b/Tabula.Csv/Tabula.Csv.csproj index 9f8ccae..603eaac 100644 --- a/Tabula.Csv/Tabula.Csv.csproj +++ b/Tabula.Csv/Tabula.Csv.csproj @@ -1,32 +1,32 @@  - - netstandard2.0;net462;net471;net6.0;net8.0 - Extract tables from PDF files (port of tabula-java using PdfPig). Csv and Tsv writers. - https://github.com/BobLd/tabula-sharp - 0.1.5 - BobLd - pdf, extract, table, tabula, pdfpig, parse, extraction, csv, tsv, excel, export - MIT - https://github.com/BobLd/tabula-sharp - True - True - README.md - + + netstandard2.0;netstandard2.1;net462;net471;net6.0;net8.0;net9.0 + Extract tables from PDF files (port of tabula-java using PdfPig). Csv and Tsv writers. + https://github.com/BobLd/tabula-sharp + 1.0.0 + BobLd + pdf, extract, table, tabula, pdfpig, parse, extraction, csv, tsv, excel, export + MIT + https://github.com/BobLd/tabula-sharp + True + True + README.md + - - - True - \ - - + + + True + \ + + - - - + + + - - - + + + - + \ No newline at end of file diff --git a/Tabula.Json/Tabula.Json.csproj b/Tabula.Json/Tabula.Json.csproj index 06cb3aa..9e94d05 100644 --- a/Tabula.Json/Tabula.Json.csproj +++ b/Tabula.Json/Tabula.Json.csproj @@ -1,32 +1,32 @@  - - netstandard2.0;net462;net471;net6.0;net8.0 - Extract tables from PDF files (port of tabula-java using PdfPig). Json writer. - https://github.com/BobLd/tabula-sharp - 0.1.5 - BobLd - BobLd - pdf, extract, table, tabula, pdfpig, parse, extraction, json, export - MIT - https://github.com/BobLd/tabula-sharp - True - README.md - + + netstandard2.0;netstandard2.1;net462;net471;net6.0;net8.0;net9.0 + Extract tables from PDF files (port of tabula-java using PdfPig). Json writer. + https://github.com/BobLd/tabula-sharp + 1.0.0 + BobLd + BobLd + pdf, extract, table, tabula, pdfpig, parse, extraction, json, export + MIT + https://github.com/BobLd/tabula-sharp + True + README.md + - - - True - \ - - + + + True + \ + + - - - + + + - - - + + + - + \ No newline at end of file diff --git a/Tabula.Tests/Tabula.Tests.csproj b/Tabula.Tests/Tabula.Tests.csproj index 5816e9b..97a5781 100644 --- a/Tabula.Tests/Tabula.Tests.csproj +++ b/Tabula.Tests/Tabula.Tests.csproj @@ -9,9 +9,9 @@ - + - + all diff --git a/Tabula/Tabula.csproj b/Tabula/Tabula.csproj index fa30e1e..6a3490e 100644 --- a/Tabula/Tabula.csproj +++ b/Tabula/Tabula.csproj @@ -1,31 +1,31 @@  - - netstandard2.0;net462;net471;net6.0;net8.0 - Extract tables from PDF files (port of tabula-java using PdfPig). - https://github.com/BobLd/tabula-sharp - 0.1.5 - BobLd - BobLd - pdf, extract, table, tabula, pdfpig, parse, extraction, export - MIT - https://github.com/BobLd/tabula-sharp - README.md - True - True - True - Tabula.snk - + + netstandard2.0;netstandard2.1;net462;net471;net6.0;net8.0;net9.0 + Extract tables from PDF files (port of tabula-java using PdfPig). + https://github.com/BobLd/tabula-sharp + 1.0.0 + BobLd + BobLd + pdf, extract, table, tabula, pdfpig, parse, extraction, export + MIT + https://github.com/BobLd/tabula-sharp + README.md + True + True + True + Tabula.snk + - - - True - \ - - + + + True + \ + + - - - + + + - + \ No newline at end of file From 87aa14802165a32e8f7435c235328f0709b95638 Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Mon, 4 May 2026 09:59:52 +0100 Subject: [PATCH 2/7] Make TextElement's Letter property public and fix #54 --- Tabula/TextElement.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tabula/TextElement.cs b/Tabula/TextElement.cs index ca4c806..833b9ec 100644 --- a/Tabula/TextElement.cs +++ b/Tabula/TextElement.cs @@ -39,6 +39,11 @@ public TextElement(PdfRectangle pdfRectangle, FontDetails font, double fontSize, } public string GetText() => text; + /// + /// Gets the PdfPig letter associated. + /// + public Letter Letter => letter; + /// /// The direction of the text (0, 90, 180, or 270). Can be any number with PdfPig. /// From 77382c02530a1d0fa081598f446999073ad59cd7 Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Mon, 4 May 2026 10:27:47 +0100 Subject: [PATCH 3/7] Refine whitespace filter in TextStripper (ports tabula-java 21a4932b) and prevent infinite loop in SimpleNurminenDetectionAlgorithm, fix #30 --- Tabula.Tests/GitHubIssues.cs | 22 +++++++++++++++ Tabula.Tests/Resources/issue.pdf | Bin 0 -> 12114 bytes Tabula.Tests/Tabula.Tests.csproj | 3 ++ .../SimpleNurminenDetectionAlgorithm.cs | 16 +++++------ Tabula/TableRectangle.cs | 26 ++++++++---------- Tabula/TextStripper.cs | 14 ++++++++-- 6 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 Tabula.Tests/GitHubIssues.cs create mode 100644 Tabula.Tests/Resources/issue.pdf diff --git a/Tabula.Tests/GitHubIssues.cs b/Tabula.Tests/GitHubIssues.cs new file mode 100644 index 0000000..c81a555 --- /dev/null +++ b/Tabula.Tests/GitHubIssues.cs @@ -0,0 +1,22 @@ +using Tabula.Detectors; +using UglyToad.PdfPig; +using Xunit; + +namespace Tabula.Tests +{ + public class GithubIssues + { + [Fact] + public void Issue30() + { + using (PdfDocument document = PdfDocument.Open("Resources/issue.pdf", new ParsingOptions() { ClipPaths = true })) + { + PageArea page = ObjectExtractor.Extract(document, 1); + + SimpleNurminenDetectionAlgorithm detector = new SimpleNurminenDetectionAlgorithm(); + var regions = detector.Detect(page); + Assert.Single(regions); + } + } + } +} diff --git a/Tabula.Tests/Resources/issue.pdf b/Tabula.Tests/Resources/issue.pdf new file mode 100644 index 0000000000000000000000000000000000000000..57b0cea899ab0ce968401cdfb8a724cd40f73fe8 GIT binary patch literal 12114 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 literal 0 HcmV?d00001 diff --git a/Tabula.Tests/Tabula.Tests.csproj b/Tabula.Tests/Tabula.Tests.csproj index 97a5781..59bbdbd 100644 --- a/Tabula.Tests/Tabula.Tests.csproj +++ b/Tabula.Tests/Tabula.Tests.csproj @@ -957,6 +957,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/Tabula/Detectors/SimpleNurminenDetectionAlgorithm.cs b/Tabula/Detectors/SimpleNurminenDetectionAlgorithm.cs index e7bef85..9006338 100644 --- a/Tabula/Detectors/SimpleNurminenDetectionAlgorithm.cs +++ b/Tabula/Detectors/SimpleNurminenDetectionAlgorithm.cs @@ -110,7 +110,7 @@ public IReadOnlyList Detect(PageArea page) List allEdges = new List(horizontalRulings); allEdges.AddRange(verticalRulings); - List tableAreas = new List(); + HashSet tableAreas = new HashSet(); // if we found some edges, try to find some tables based on them if (allEdges.Count > 0) @@ -236,13 +236,14 @@ public IReadOnlyList Detect(PageArea page) // get rid of any text lines contained within existing tables, this allows us to find more tables //for (Iterator iterator = lines.iterator(); iterator.hasNext();) - foreach (var textRow in lines.ToList()) + for (int i = lines.Count - 1; i >= 0; i--) { - foreach (TableRectangle table in tableAreas) + var textRow = lines[i]; + foreach (var table in tableAreas) { if (table.Contains(textRow)) { - lines.Remove(textRow); + lines.RemoveAt(i); break; } } @@ -278,8 +279,7 @@ public IReadOnlyList Detect(PageArea page) if (table != null) { - foundTable = true; - tableAreas.Add(table); + foundTable = tableAreas.Add(table); } } } while (foundTable); @@ -725,7 +725,7 @@ private TextEdges getTextEdges(List lines) return new TextEdges(leftTextEdges, midTextEdges, rightTextEdges); } - private List getTableAreasFromCells(List cells) + private HashSet getTableAreasFromCells(List cells) { List> cellGroups = new List>(); foreach (TableRectangle cell in cells) @@ -764,7 +764,7 @@ private List getTableAreasFromCells(List cells) } // create table areas based on cell group - List tableAreas = new List(); + HashSet tableAreas = new HashSet(); foreach (List cellGroup in cellGroups) { // less than four cells should not make a table diff --git a/Tabula/TableRectangle.cs b/Tabula/TableRectangle.cs index e1e1245..7687277 100644 --- a/Tabula/TableRectangle.cs +++ b/Tabula/TableRectangle.cs @@ -11,7 +11,7 @@ namespace Tabula /// /// A tabula rectangle. /// - public class TableRectangle : IComparable + public class TableRectangle : IComparable, IEquatable { /// /// Sort top to bottom (as in reading order). @@ -212,30 +212,26 @@ public void SetBottom(double bottom) public override int GetHashCode() { - int prime = 31; - int result = base.GetHashCode(); - // need to implement hash and equal in PdfPig's PdfRectangle - result = prime * result + BoundingBox.BottomLeft.GetHashCode(); - result = prime * result + BoundingBox.TopLeft.GetHashCode(); - result = prime * result + BoundingBox.TopRight.GetHashCode(); - result = prime * result + BoundingBox.BottomRight.GetHashCode(); - result = prime * result + BitConverter.ToInt32(BitConverter.GetBytes(BoundingBox.Rotation), 0); - return result; + return BoundingBox.GetHashCode(); } public override bool Equals(object obj) { if (obj is TableRectangle other) { - if (!this.BoundingBox.BottomLeft.Equals(other.BoundingBox.BottomLeft)) return false; - if (!this.BoundingBox.TopLeft.Equals(other.BoundingBox.TopLeft)) return false; - if (!this.BoundingBox.TopRight.Equals(other.BoundingBox.TopRight)) return false; - if (!this.BoundingBox.BottomRight.Equals(other.BoundingBox.BottomRight)) return false; - return true; + return Equals(other); } + return false; } + public bool Equals(TableRectangle other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return BoundingBox.Equals(other.BoundingBox); + } + public override string ToString() { StringBuilder sb = new StringBuilder(); diff --git a/Tabula/TextStripper.cs b/Tabula/TextStripper.cs index b449b0d..0d61e78 100644 --- a/Tabula/TextStripper.cs +++ b/Tabula/TextStripper.cs @@ -14,6 +14,8 @@ public sealed class TextStripper { private const string NBSP = "\u00A0"; private static float AVG_HEIGHT_MULT_THRESHOLD = 6.0f; + private static float MAX_BLANK_FONT_SIZE = 40.0f; + private static float MIN_BLANK_FONT_SIZE = 2.0f; /// /// Process the page. @@ -54,9 +56,17 @@ public static TextStripperResult Process(Page page) totalHeight += Math.Max(te.Height, 1); // added by bobld: min height value to 1 double avgHeight = totalHeight / countHeight; - if (avgHeight > 0 && te.Height >= (avgHeight * AVG_HEIGHT_MULT_THRESHOLD) && (te.GetText()?.Trim().Equals("") != false)) + if (te.GetText()?.Trim().Equals("") != false) { - continue; + if (avgHeight > 0 && te.Height >= (avgHeight * AVG_HEIGHT_MULT_THRESHOLD)) + { + continue; + } + + if (letter.PointSize > MAX_BLANK_FONT_SIZE || letter.PointSize < MIN_BLANK_FONT_SIZE) + { + continue; + } } textElements.Add(te); From 08892148661b39afe88fa9adcff89577869bf160 Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Mon, 4 May 2026 14:29:38 +0100 Subject: [PATCH 4/7] Attempt at fixing infinite loop in SpreadsheetExtractionAlgorithm (#50) --- .claude/settings.local.json | 8 ++++++++ .../SpreadsheetExtractionAlgorithm.cs | 20 +++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..f9eb3d6 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build *)", + "Bash(dotnet test *)" + ] + } +} diff --git a/Tabula/Extractors/SpreadsheetExtractionAlgorithm.cs b/Tabula/Extractors/SpreadsheetExtractionAlgorithm.cs index a24e9df..4cca586 100644 --- a/Tabula/Extractors/SpreadsheetExtractionAlgorithm.cs +++ b/Tabula/Extractors/SpreadsheetExtractionAlgorithm.cs @@ -287,7 +287,6 @@ public static List FindSpreadsheetsFromCells(List pointSet = new HashSet(); Dictionary edgesH = new Dictionary(); Dictionary edgesV = new Dictionary(); - int i = 0; cells = new List(new HashSet(cells)); @@ -316,26 +315,27 @@ public static List FindSpreadsheetsFromCells(List pointsSortY = new List(pointSet); pointsSortY.Sort(new POINT_COMPARER()); - while (i < pointSet.Count) + // Pair consecutive points sharing a Y / X coordinate. Using a for-loop with an + // unconditional increment (rather than the upstream tabula-java nested while) + // prevents an infinite loop when a coordinate is NaN or Infinity, since Feq + // returns false for any NaN comparison and would otherwise leave i unchanged. + for (int i = 0; i + 1 < pointSet.Count; i++) { - float currY = (float)pointsSortY[i].Y; - while (i < pointSet.Count && Utils.Feq(pointsSortY[i].Y, currY)) + if (Utils.Feq(pointsSortY[i].Y, pointsSortY[i + 1].Y)) { edgesH[pointsSortY[i]] = pointsSortY[i + 1]; edgesH[pointsSortY[i + 1]] = pointsSortY[i]; - i += 2; + i++; } } - i = 0; - while (i < pointSet.Count) + for (int i = 0; i + 1 < pointSet.Count; i++) { - float currX = (float)pointsSortX[i].X; - while (i < pointSet.Count && Utils.Feq(pointsSortX[i].X, currX)) + if (Utils.Feq(pointsSortX[i].X, pointsSortX[i + 1].X)) { edgesV[pointsSortX[i]] = pointsSortX[i + 1]; edgesV[pointsSortX[i + 1]] = pointsSortX[i]; - i += 2; + i++; } } From e13f43586e05aaf1b93643f5f17c4532db7cfd2a Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Mon, 4 May 2026 14:38:15 +0100 Subject: [PATCH 5/7] Remove obsolete PdfPig properties and misc --- Tabula/ObjectExtractor.cs | 2 +- Tabula/PageIterator.cs | 15 +-------------- Tabula/TextStripper.cs | 38 +++++++++++++++++++------------------- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/Tabula/ObjectExtractor.cs b/Tabula/ObjectExtractor.cs index ab45c9e..7fd5dbb 100644 --- a/Tabula/ObjectExtractor.cs +++ b/Tabula/ObjectExtractor.cs @@ -75,7 +75,7 @@ public static PageArea ExtractPage(Page page) // se.processPage(p); var rulings = new List(); - foreach (var path in page.ExperimentalAccess.Paths) + foreach (var path in page.Paths) { if (!path.IsFilled && !path.IsStroked) continue; // strokeOrFillPath operator => filter stroke and filled diff --git a/Tabula/PageIterator.cs b/Tabula/PageIterator.cs index 9b3c0cd..334cfdb 100644 --- a/Tabula/PageIterator.cs +++ b/Tabula/PageIterator.cs @@ -28,20 +28,7 @@ public PageIterator(PdfDocument pdfDocument, IEnumerable pages) : base() } /// - public PageArea Current - { - get - { - try - { - return ObjectExtractor.ExtractPage(this.pdfDocument, pageIndexIterator.Current); - } - catch (Exception ex) - { - throw ex; - } - } - } + public PageArea Current => ObjectExtractor.ExtractPage(this.pdfDocument, pageIndexIterator.Current); /// object IEnumerator.Current => Current; diff --git a/Tabula/TextStripper.cs b/Tabula/TextStripper.cs index 0d61e78..1a528aa 100644 --- a/Tabula/TextStripper.cs +++ b/Tabula/TextStripper.cs @@ -44,7 +44,7 @@ public static TextStripperResult Process(Page page) double wos = GetExpectedWhitespaceSize(letter); - TextElement te = new TextElement(GetBbox(letter), letter.Font, letter.PointSize, c, wos, letter.GlyphRectangle.Rotation) + TextElement te = new TextElement(GetBbox(letter), letter.FontDetails, letter.PointSize, c, wos, letter.BoundingBox.Rotation) { letter = letter }; @@ -90,10 +90,10 @@ private static bool IsPrintable(string s) private static bool IsSpecial(char c) { -#if NETCOREAPP3_1 - return c >= System.Text.Unicode.UnicodeRanges.Specials.FirstCodePoint && c < (System.Text.Unicode.UnicodeRanges.Specials.FirstCodePoint + System.Text.Unicode.UnicodeRanges.Specials.Length); -#else +#if NETSTANDARD || NETFRAMEWORK return c >= '\uFFF0' && c <= '\uFFFF'; +#else + return c >= System.Text.Unicode.UnicodeRanges.Specials.FirstCodePoint && c < (System.Text.Unicode.UnicodeRanges.Specials.FirstCodePoint + System.Text.Unicode.UnicodeRanges.Specials.Length); #endif } @@ -112,39 +112,39 @@ private static PdfRectangle GetBbox(Letter letter) { case TextOrientation.Horizontal: double add = 0; - if (letter.GlyphRectangle.Height == 0) + if (letter.BoundingBox.Height == 0) { add = 1; // force minimum height to be 1 } else if (letter.Value == " ") { - add = -(letter.GlyphRectangle.Height - 1); // force height of space to be 1 + add = -(letter.BoundingBox.Height - 1); // force height of space to be 1 } return new PdfRectangle(Utils.Round(letter.StartBaseLine.X, 2), Utils.Round(letter.StartBaseLine.Y, 2), - Utils.Round(letter.StartBaseLine.X + Math.Max(letter.Width, letter.GlyphRectangle.Width), 2), - Utils.Round(letter.GlyphRectangle.TopLeft.Y + add, 2)); + Utils.Round(letter.StartBaseLine.X + Math.Max(letter.Width, letter.BoundingBox.Width), 2), + Utils.Round(letter.BoundingBox.TopLeft.Y + add, 2)); case TextOrientation.Rotate180: // need to force min height = 1 and height of space to be 1 return new PdfRectangle(Utils.Round(letter.StartBaseLine.X, 2), Utils.Round(letter.StartBaseLine.Y, 2), - Utils.Round(letter.StartBaseLine.X - Math.Max(letter.Width, letter.GlyphRectangle.Width), 2), - Utils.Round(letter.GlyphRectangle.TopRight.Y, 2)); + Utils.Round(letter.StartBaseLine.X - Math.Max(letter.Width, letter.BoundingBox.Width), 2), + Utils.Round(letter.BoundingBox.TopRight.Y, 2)); case TextOrientation.Rotate90: // need to force min height = 1 and height of space to be 1 - return new PdfRectangle(new PdfPoint(Utils.Round(letter.StartBaseLine.X + letter.GlyphRectangle.Height, 2), Utils.Round(letter.GlyphRectangle.BottomLeft.Y, 2)), - new PdfPoint(Utils.Round(letter.StartBaseLine.X + letter.GlyphRectangle.Height, 2), Utils.Round(letter.EndBaseLine.Y, 2)), - new PdfPoint(Utils.Round(letter.StartBaseLine.X, 2), Utils.Round(letter.GlyphRectangle.BottomLeft.Y, 2)), + return new PdfRectangle(new PdfPoint(Utils.Round(letter.StartBaseLine.X + letter.BoundingBox.Height, 2), Utils.Round(letter.BoundingBox.BottomLeft.Y, 2)), + new PdfPoint(Utils.Round(letter.StartBaseLine.X + letter.BoundingBox.Height, 2), Utils.Round(letter.EndBaseLine.Y, 2)), + new PdfPoint(Utils.Round(letter.StartBaseLine.X, 2), Utils.Round(letter.BoundingBox.BottomLeft.Y, 2)), new PdfPoint(Utils.Round(letter.StartBaseLine.X, 2), Utils.Round(letter.EndBaseLine.Y, 2))); case TextOrientation.Rotate270: // need to force min height = 1 and height of space to be 1 - return new PdfRectangle(new PdfPoint(Utils.Round(letter.StartBaseLine.X - letter.GlyphRectangle.Height, 2), Utils.Round(letter.StartBaseLine.Y, 2)), - new PdfPoint(Utils.Round(letter.StartBaseLine.X - letter.GlyphRectangle.Height, 2), Utils.Round(letter.GlyphRectangle.BottomRight.Y, 2)), + return new PdfRectangle(new PdfPoint(Utils.Round(letter.StartBaseLine.X - letter.BoundingBox.Height, 2), Utils.Round(letter.StartBaseLine.Y, 2)), + new PdfPoint(Utils.Round(letter.StartBaseLine.X - letter.BoundingBox.Height, 2), Utils.Round(letter.BoundingBox.BottomRight.Y, 2)), new PdfPoint(Utils.Round(letter.StartBaseLine.X, 2), Utils.Round(letter.StartBaseLine.Y, 2)), - new PdfPoint(Utils.Round(letter.StartBaseLine.X, 2), Utils.Round(letter.GlyphRectangle.BottomRight.Y, 2))); + new PdfPoint(Utils.Round(letter.StartBaseLine.X, 2), Utils.Round(letter.BoundingBox.BottomRight.Y, 2))); case TextOrientation.Other: default: @@ -161,8 +161,8 @@ private static PdfRectangle GetBoundingBoxOther(Letter letter) { letter.StartBaseLine, letter.EndBaseLine, - letter.GlyphRectangle.TopLeft, - letter.GlyphRectangle.TopRight + letter.BoundingBox.TopLeft, + letter.BoundingBox.TopRight }; // Candidates bounding boxes @@ -174,7 +174,7 @@ private static PdfRectangle GetBoundingBoxOther(Letter letter) // Find the orientation of the OBB, using the baseline angle // Assumes line order is correct - var baseLineAngle = Distances.BoundAngle180(Distances.Angle(letter.GlyphRectangle.BottomLeft, letter.GlyphRectangle.BottomRight)); + var baseLineAngle = Distances.BoundAngle180(Distances.Angle(letter.BoundingBox.BottomLeft, letter.BoundingBox.BottomRight)); double deltaAngle = Math.Abs(Distances.BoundAngle180(obb.Rotation - baseLineAngle)); double deltaAngle1 = Math.Abs(Distances.BoundAngle180(obb1.Rotation - baseLineAngle)); From 94b8755e8101da3dc0862cb87494208d569d6547 Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Mon, 4 May 2026 14:40:12 +0100 Subject: [PATCH 6/7] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a56f0c..ecfd7b2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Linux](https://github.com/BobLd/tabula-sharp/workflows/Linux/badge.svg) ![Mac OS](https://github.com/BobLd/tabula-sharp/workflows/Mac%20OS/badge.svg) -- Supports netstandard2.0, net462, net471, net6.0, net8.0 +- Supports netstandard2.0 / 2.1, net462, net471, net6.0, net8.0 and net9.0 - No java bindings NuGet packages available on the [releases](https://github.com/BobLd/tabula-sharp/releases) page and on www.nuget.org: From cde6ec5f3b647367f7a6cd951c3c7ad80c5e72d1 Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Mon, 4 May 2026 14:43:09 +0100 Subject: [PATCH 7/7] Target net9 in tests and update .NET version from 8.0.x to 9.0.x in test pipeline --- .github/workflows/dotnet-linux.yml | 6 +- .github/workflows/dotnet-mac.yml | 6 +- .github/workflows/dotnet-windows.yml | 2 +- Tabula.Tests/Tabula.Tests.csproj | 2092 +++++++++++++------------- 4 files changed, 1053 insertions(+), 1053 deletions(-) diff --git a/.github/workflows/dotnet-linux.yml b/.github/workflows/dotnet-linux.yml index 0fde9f2..06aee17 100644 --- a/.github/workflows/dotnet-linux.yml +++ b/.github/workflows/dotnet-linux.yml @@ -16,12 +16,12 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Install mscorefonts run: sudo apt-get -y install ttf-mscorefonts-installer - name: Install dependencies run: dotnet restore - name: Build net8.0 - run: dotnet build --configuration Release --no-restore --framework net8.0 + run: dotnet build --configuration Release --no-restore --framework net9.0 - name: Test net8.0 - run: dotnet test --no-restore --verbosity minimal --framework net8.0 + run: dotnet test --no-restore --verbosity minimal --framework net9.0 diff --git a/.github/workflows/dotnet-mac.yml b/.github/workflows/dotnet-mac.yml index 43a5d41..f2457c2 100644 --- a/.github/workflows/dotnet-mac.yml +++ b/.github/workflows/dotnet-mac.yml @@ -14,10 +14,10 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Install dependencies run: dotnet restore - name: Build net8.0 - run: dotnet build --configuration Release --no-restore --framework net8.0 + run: dotnet build --configuration Release --no-restore --framework net9.0 - name: Test net8.0 - run: dotnet test --no-restore --verbosity minimal --framework net8.0 + run: dotnet test --no-restore --verbosity minimal --framework net9.0 diff --git a/.github/workflows/dotnet-windows.yml b/.github/workflows/dotnet-windows.yml index e527dbc..bfc74d5 100644 --- a/.github/workflows/dotnet-windows.yml +++ b/.github/workflows/dotnet-windows.yml @@ -14,7 +14,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/Tabula.Tests/Tabula.Tests.csproj b/Tabula.Tests/Tabula.Tests.csproj index 59bbdbd..c905667 100644 --- a/Tabula.Tests/Tabula.Tests.csproj +++ b/Tabula.Tests/Tabula.Tests.csproj @@ -1,1055 +1,1055 @@  - - net8.0 + + net9.0 - false + false - 0.1.2 - + 0.1.2 + - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - - - + + + + + - - - Always - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - Always - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + + + Always + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Always + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + +