diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..25af869 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# Default line endings for paths that do not have a nested root .editorconfig. +# MuPDF.NET/.editorconfig stays root=true and owns C# settings under that folder. +root = true + +[*] +end_of_line = lf +charset = utf-8 + +[*.{bat,cmd}] +end_of_line = crlf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ea2e25 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,36 @@ +# Cross-platform line endings (Windows + Linux). +# Overrides core.autocrlf: store LF in git and check out LF in the working tree. +* text=auto eol=lf + +# Unix shells: CRLF breaks `set -eu` under dash/sh. +*.sh text eol=lf + +# Windows command scripts +*.bat text eol=crlf +*.cmd text eol=crlf + +# Binary — never convert line endings +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.pdf binary +*.dll binary +*.exe binary +*.so binary +*.dylib binary +*.a binary +*.lib binary +*.pdb binary +*.nupkg binary +*.snupkg binary +*.zip binary +*.gz binary +*.7z binary +*.ttf binary +*.otf binary +*.woff binary +*.woff2 binary +*.eot binary +*.bin binary diff --git a/Demo/SampleMenu.cs b/Demo/SampleMenu.cs index ba6561b..fd5ebac 100644 --- a/Demo/SampleMenu.cs +++ b/Demo/SampleMenu.cs @@ -68,6 +68,7 @@ private sealed record Sample( new("Regression & diagnostics", "issue-213", "[diag] Drawing paths / line width", _ => Program.TestIssue213(), Diagnostic: true), new("Regression & diagnostics", "issue-1880", "[diag] Read Data Matrix barcodes", _ => Program.TestIssue1880(), Diagnostic: true), new("Regression & diagnostics", "issue-234", "[diag] Pixmap scale + insert image", _ => Program.TestIssue234(), Diagnostic: true), + new("Regression & diagnostics", "issue-256", "[diag] Widget / Search / GetKeyXref / MediaBox leak (#256)", a => Program.TestIssue256(a), Diagnostic: true), new("Regression & diagnostics", "pixmap-parallel", "[diag] Parallel Pixmap.ToBytes", _ => Program.TestPixmapParallel(), Diagnostic: true), new("Regression & diagnostics", "gettables-parallel", "[diag] Parallel Utils.GetTables", _ => Program.TestGetTablesParallel(), Diagnostic: true), new("Regression & diagnostics", "jbig2", "[diag] JBIG2 image recompression", _ => Program.TestRecompressJBIG2(), Diagnostic: true), diff --git a/Demo/Samples/Regression/Program.Issue256.cs b/Demo/Samples/Regression/Program.Issue256.cs new file mode 100644 index 0000000..3042162 --- /dev/null +++ b/Demo/Samples/Regression/Program.Issue256.cs @@ -0,0 +1,307 @@ +using System.Diagnostics; + +namespace Demo +{ + internal partial class Program + { + /// + /// Issue #256 — widget enumeration, TextPage.Search, GetKeyXref(AP/N), + /// and Page.MediaBox (Search then MediaBox per widget). + /// + internal static void TestIssue256(string[] args) + { + Console.WriteLine("\n=== issue-256: widgets / Search / GetKeyXref / MediaBox memory ==="); + Console.WriteLine("https://github.com/ArtifexSoftware/MuPDF.NET/issues/256"); + + string[] rest = args ?? Array.Empty(); + if (rest.Length > 0 && string.Equals(rest[0], "issue-256", StringComparison.OrdinalIgnoreCase)) + rest = rest.Skip(1).ToArray(); + + int iterations = 80; + string mode = "all"; + string needle = "the"; + if (rest.Length > 0 && int.TryParse(rest[0], out int n)) + iterations = n; + if (rest.Length > 1) + mode = rest[1]; + if (rest.Length > 2) + needle = rest[2]; + + if (string.Equals(mode, "all", StringComparison.OrdinalIgnoreCase)) + { + RunIssue256Mode("widgets", iterations, needle); + RunIssue256Mode("search", iterations, needle); + RunIssue256Mode("getkey10", iterations, needle); + RunIssue256Mode("mediabox", iterations, needle); + RunIssue256Mode("mediabox-once", iterations, needle); + RunIssue256Mode("mediabox-widget", iterations, needle); + return; + } + + RunIssue256Mode(mode, iterations, needle); + } + + private static bool Issue256IsMediaBoxMode(string mode) => + mode == "mediabox" || mode == "mediabox-once" || mode == "mediabox-widget"; + + private static void RunIssue256Mode(string mode, int iterations, string needle) + { + byte[] data = mode == "search" + ? Issue256BuildTextPdf() + : Issue256IsMediaBoxMode(mode) + ? Issue256BuildFormAndTextPdf() + : Issue256BuildWidgetPdf(); + Console.WriteLine($"--- {mode} iterations={iterations} pdfBytes={data.Length} ---"); + + using (var probe = new Document(stream: data)) + { + Page page = probe[0]; + if (mode == "search") + Issue256SearchOnce(page, needle); + else if (Issue256IsMediaBoxMode(mode)) + Issue256MediaBoxOnce(page, needle, mode); + else + Issue256WidgetsOnce(probe, page, mode); + page.Dispose(); + } + + Issue256Stabilize(); + long startPrivate = Process.GetCurrentProcess().PrivateMemorySize64; + + for (int i = 1; i <= iterations; i++) + { + if (Issue256IsMediaBoxMode(mode)) + { + Issue256MediaBoxRepro(data, needle, mode); + } + else + { + using var doc = new Document(stream: data); + Page page = doc[0]; + if (mode == "search") + Issue256SearchPage(page, needle); + else + Issue256WidgetsPage(doc, page, mode); + page.Dispose(); + doc.Close(); + } + } + + Issue256Stabilize(); + long endPrivate = Process.GetCurrentProcess().PrivateMemorySize64; + Console.WriteLine( + $" deltaPrivateKB={(endPrivate - startPrivate) / 1024} (widgets/Search/GetKeyXref/MediaBox after dispose)"); + } + + private static void Issue256WidgetsOnce(Document doc, Page page, string mode) + { + int n = 0; + int repeats = mode == "getkey10" ? 10 : (mode == "getkey" ? 1 : 0); + List widgets = page.GetWidgets().ToList(); + try + { + foreach (Widget w in widgets) + { + n++; + (string kind, string raw) = (null, null); + for (int r = 0; r < Math.Max(repeats, 1); r++) + { + if (repeats > 0) + (kind, raw) = doc.GetKeyXref(w.Xref, "AP/N"); + else + { + _ = w.FieldName; + _ = w.Rect; + } + } + if (n == 1) + { + if (repeats > 0) + Console.WriteLine($" first xref={w.Xref} AP/N kind={kind} repeats={repeats}"); + else + Console.WriteLine($" first FieldName={w.FieldName} Rect={w.Rect}"); + } + } + } + finally + { + foreach (Widget w in widgets) + w.Dispose(); + } + Console.WriteLine($" widgetsOnPage0={n}"); + } + + private static void Issue256WidgetsPage(Document doc, Page page, string mode) + { + int repeats = mode == "getkey10" ? 10 : (mode == "getkey" ? 1 : 0); + List widgets = page.GetWidgets().ToList(); + try + { + foreach (Widget w in widgets) + { + if (repeats > 0) + { + for (int r = 0; r < repeats; r++) + _ = doc.GetKeyXref(w.Xref, "AP/N"); + } + else + { + _ = w.FieldName; + _ = w.Rect; + } + } + } + finally + { + foreach (Widget w in widgets) + w.Dispose(); + } + } + + private static void Issue256SearchOnce(Page page, string needle) + { + using TextPage tp = page.GetTextPage(); + string text = tp.ExtractText() ?? ""; + var quads = TextPage.Search(tp, needle, hitMax: 1); + Console.WriteLine($" extractChars={text.Length} searchHits={quads.Count}"); + } + + private static void Issue256SearchPage(Page page, string needle) + { + using TextPage tp = page.GetTextPage(); + _ = tp.ExtractText(); + for (int k = 0; k < 20; k++) + { + var quads = TextPage.Search(tp, needle, hitMax: 1); + if (quads.Count > 0) + _ = quads[0].Rect; + } + } + + private static void Issue256MediaBoxOnce(Page page, string needle, string mode) + { + using TextPage tp = page.GetTextPage(); + var hits = TextPage.Search(tp, needle, hitMax: 1); + Console.WriteLine($" searchHits={hits.Count} MediaBox={page.MediaBox}"); + var widgets = page.GetWidgets().ToList(); + try + { + Console.WriteLine($" widgetsOnPage0={widgets.Count} mode={mode}"); + if (widgets.Count > 0) + Console.WriteLine($" first widget Rect={widgets[0].Rect} intersects={page.MediaBox.Intersects(widgets[0].Rect)}"); + } + finally + { + foreach (Widget w in widgets) + w.Dispose(); + } + } + + /// + /// https://github.com/ArtifexSoftware/MuPDF.NET/issues/256#issuecomment-5728502983 + /// + private static void Issue256MediaBoxRepro(byte[] data, string needle, string mode) + { + bool searchFirst = mode != "mediabox"; + bool mediaBoxPerWidget = mode != "mediabox-once"; + + if (searchFirst) + { + using (var doc = new Document(stream: data)) + { + foreach (Page page in doc) + { + using TextPage tp = page.GetTextPage(); + var hits = TextPage.Search(tp, needle, hitMax: 1); + if (hits.Count > 0) + _ = page.Rect.Intersects(hits[0].Rect.Transform(page.RotationMatrix)); + page.Dispose(); + } + doc.Close(); + } + } + + using (var doc = new Document(stream: data)) + { + foreach (Page page in doc) + { + if (mediaBoxPerWidget) + { + foreach (Widget w in page.GetWidgets()) + _ = page.MediaBox.Intersects(w.Rect); + } + else + { + Rect mb = page.MediaBox; + foreach (Widget w in page.GetWidgets()) + _ = mb.Intersects(w.Rect); + } + page.Dispose(); + } + doc.Close(); + } + } + + private static void Issue256Stabilize() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + private static byte[] Issue256BuildWidgetPdf() + { + using var doc = new Document(); + Page page = doc.NewPage(); + for (int i = 0; i < 8; i++) + { + var w = new Widget(page) + { + FieldName = "text_" + i, + FieldType = (int)WidgetType.Text, + Rect = new Rect(50, 50 + i * 28, 400, 72 + i * 28), + FieldValue = "value " + i, + }; + page.AddWidget(w); + } + return doc.Write(); + } + + private static byte[] Issue256BuildTextPdf() + { + using var doc = new Document(); + Page page = doc.NewPage(); + var html = new StringBuilder(); + html.Append(""); + for (int i = 0; i < 40; i++) + html.Append("

the quick brown fox jumps over the lazy dog ").Append(i).Append("

"); + html.Append(""); + page.InsertHtmlbox(page.Rect, html.ToString()); + return doc.Write(); + } + + private static byte[] Issue256BuildFormAndTextPdf() + { + using var doc = new Document(); + Page page = doc.NewPage(); + var html = new StringBuilder(); + html.Append(""); + for (int i = 0; i < 40; i++) + html.Append("

the quick brown fox jumps over the lazy dog ").Append(i).Append("

"); + html.Append(""); + page.InsertHtmlbox(page.Rect, html.ToString()); + for (int i = 0; i < 8; i++) + { + var w = new Widget(page) + { + FieldName = "text_" + i, + FieldType = (int)WidgetType.Text, + Rect = new Rect(50, 50 + i * 28, 400, 72 + i * 28), + FieldValue = "value " + i, + }; + page.AddWidget(w); + } + return doc.Write(); + } + } +} diff --git a/MuPDF.NET.PDF4LLM/README.md b/MuPDF.NET.PDF4LLM/README.md index 48e34cb..24e8c98 100644 --- a/MuPDF.NET.PDF4LLM/README.md +++ b/MuPDF.NET.PDF4LLM/README.md @@ -13,6 +13,7 @@ The public API lives in the **`MuPDF.NET.PDF4LLM`** namespace. The main entry po | Full documentation | https://docs.pdf4llm.com/ | | .NET getting started | https://docs.pdf4llm.com/dotnet/getting-started/installation | | MuPDF.NET API reference | https://mupdfnet.readthedocs.io/ | +| Sample apps (`MuPDF.NET.Examples`) | https://github.com/ArtifexSoftware/MuPDF.NET.Examples | ## Installation @@ -68,6 +69,17 @@ bool layoutReady = PyMuPdfLayout.IsAvailable; // Python import probe bool layoutActive = MuPDF4LLM.LayoutAvailable; // provider registered ``` +## Examples + +Runnable console samples for this package (Markdown, JSON/layout, OCR, tables, LlamaIndex, Markdown-to-PDF) live in **[MuPDF.NET.Examples](https://github.com/ArtifexSoftware/MuPDF.NET.Examples)** under `MuPDF.NET.PDF4LLM/`. + +```powershell +git clone https://github.com/ArtifexSoftware/MuPDF.NET.Examples.git +cd MuPDF.NET.Examples +dotnet restore +dotnet run --project MuPDF.NET.PDF4LLM\01-ToMarkdown +``` + ## Quick start ```csharp diff --git a/MuPDF.NET.Test/Test256.cs b/MuPDF.NET.Test/Test256.cs new file mode 100644 index 0000000..5f23948 --- /dev/null +++ b/MuPDF.NET.Test/Test256.cs @@ -0,0 +1,378 @@ +using System; +using System.Diagnostics; +using System.Text; +using Xunit; + +namespace MuPDF.NET.Test +{ + /// + /// Regression for . + /// Widget enumeration, TextPage.Search, GetKeyXref(AP/N), and + /// Page.MediaBox (follow-up: Search then MediaBox once per widget) must + /// remain correct and must not grow private memory unboundedly across open/close loops. + /// + [Collection("MuPDF.NET native")] + public class Test256 + { + private const int MemoryIterations = 80; + + [Fact] + public void test_256_widgets_and_getkey_xref() + { + byte[] data = BuildWidgetPdf(); + using var doc = new Document(stream: data); + Page page = doc[0]; + var widgets = page.GetWidgets().ToList(); + int n = 0; + string firstName = null; + string firstKind = null; + string tenthKind = null; + try + { + foreach (Widget w in widgets) + { + n++; + if (n == 1) + { + firstName = w.FieldName; + (firstKind, _) = doc.GetKeyXref(w.Xref, "AP/N"); + for (int r = 0; r < 9; r++) + (tenthKind, _) = doc.GetKeyXref(w.Xref, "AP/N"); + } + } + } + finally + { + foreach (Widget w in widgets) + w.Dispose(); + } + page.Dispose(); + + Assert.Equal(8, n); + Assert.Equal("text_0", firstName); + Assert.False(string.IsNullOrEmpty(firstKind)); + Assert.Equal(firstKind, tenthKind); + } + + [Fact] + public void test_256_search() + { + byte[] data = BuildTextPdf(); + using var doc = new Document(stream: data); + Page page = doc[0]; + using TextPage tp = page.GetTextPage(); + string text = tp.ExtractText() ?? ""; + var quads = TextPage.Search(tp, "the", hitMax: 1); + page.Dispose(); + + Assert.True(text.Length > 100, $"extractChars={text.Length}"); + Assert.True(quads.Count >= 1, $"searchHits={quads.Count}"); + } + + [Fact] + public void test_256_widgets_memory() + { + AssertMemoryStable("widgets", BuildWidgetPdf()); + } + + [Fact] + public void test_256_getkey10_memory() + { + AssertMemoryStable("getkey10", BuildWidgetPdf()); + } + + [Fact] + public void test_256_search_memory() + { + AssertMemoryStable("search", BuildTextPdf()); + } + + /// + /// Reporter follow-up: Search, then a fresh document with + /// page.MediaBox.Intersects(w.Rect) once per widget. + /// + [Fact] + public void test_256_mediabox_per_widget_after_search() + { + byte[] data = BuildFormAndTextPdf(); + using var doc = new Document(stream: data); + Page page = doc[0]; + using TextPage tp = page.GetTextPage(); + var hits = TextPage.Search(tp, "the", hitMax: 1); + Assert.True(hits.Count >= 1, $"searchHits={hits.Count}"); + Assert.True(page.Rect.Intersects(hits[0].Rect.Transform(page.RotationMatrix))); + + var widgets = page.GetWidgets().ToList(); + int n = 0; + try + { + foreach (Widget w in widgets) + { + n++; + Assert.True(page.MediaBox.Intersects(w.Rect), $"widget {n} Rect={w.Rect} MediaBox={page.MediaBox}"); + } + } + finally + { + foreach (Widget w in widgets) + w.Dispose(); + } + page.Dispose(); + Assert.Equal(8, n); + } + + [Fact] + public void test_256_mediabox_only_memory() + { + AssertMemoryStable("mediabox", BuildFormAndTextPdf()); + } + + [Fact] + public void test_256_mediabox_once_per_page_after_search_memory() + { + AssertMemoryStable("mediabox-once", BuildFormAndTextPdf()); + } + + [Fact] + public void test_256_mediabox_per_widget_after_search_memory() + { + AssertMemoryStable("mediabox-widget", BuildFormAndTextPdf()); + } + + /// + /// Remaining #256-class owning wrappers: Annot.Rect, + /// XrefSetKey, PageCropBox, GetSvgImage. + /// + [Fact] + public void test_256_annot_rect_xrefsetkey_cropbox_svg() + { + using var doc = new Document(); + Page page = doc.NewPage(); + Annot annot = page.AddTextAnnot(new Point(72, 72), "note"); + try + { + Assert.False(annot.Rect.IsEmpty); + Assert.True(annot.Xref > 0); + Assert.True(page.Rect.Intersects(annot.Rect)); + + Rect crop = doc.PageCropBox(0); + Assert.True(crop.Width > 0 && crop.Height > 0); + + doc.XrefSetKey(page.Xref, "Rotate", "90"); + var (kind, value) = doc.XrefGetKey(page.Xref, "Rotate"); + Assert.Equal("int", kind); + Assert.Equal("90", value); + + string svg = page.GetSvgImage(); + Assert.Contains(" 0) + { + for (int r = 0; r < repeats; r++) + _ = doc.GetKeyXref(w.Xref, "AP/N"); + } + else + { + _ = w.FieldName; + _ = w.Rect; + } + } + } + finally + { + foreach (Widget w in widgets) + w.Dispose(); + } + } + + private static void SearchPage(Page page) + { + using TextPage tp = page.GetTextPage(); + _ = tp.ExtractText(); + for (int k = 0; k < 20; k++) + { + var quads = TextPage.Search(tp, "the", hitMax: 1); + if (quads.Count > 0) + _ = quads[0].Rect; + } + } + + /// + /// Matches + /// . + /// Pass 1 searches then uses page.Rect / RotationMatrix. + /// Pass 2 opens a fresh document and reads page.MediaBox once per widget + /// (or once per page when is mediabox-once). + /// mediabox is pass 2 only. + /// + private static void RunMediaBoxRepro(byte[] data, string mode) + { + bool searchFirst = mode != "mediabox"; + bool mediaBoxPerWidget = mode != "mediabox-once"; + + if (searchFirst) + { + using (var doc = new Document(stream: data)) + { + foreach (Page page in doc) + { + using TextPage tp = page.GetTextPage(); + var hits = TextPage.Search(tp, "the", hitMax: 1); + if (hits.Count > 0) + _ = page.Rect.Intersects(hits[0].Rect.Transform(page.RotationMatrix)); + page.Dispose(); + } + doc.Close(); + } + } + + using (var doc = new Document(stream: data)) + { + foreach (Page page in doc) + { + var widgets = page.GetWidgets().ToList(); + try + { + if (mediaBoxPerWidget) + { + foreach (Widget w in widgets) + _ = page.MediaBox.Intersects(w.Rect); + } + else + { + Rect mb = page.MediaBox; + foreach (Widget w in widgets) + _ = mb.Intersects(w.Rect); + } + } + finally + { + foreach (Widget w in widgets) + w.Dispose(); + } + page.Dispose(); + } + doc.Close(); + } + } + + private static void Stabilize() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + private static long PrivateBytes() + { + using var process = Process.GetCurrentProcess(); + process.Refresh(); + return process.PrivateMemorySize64; + } + + private static byte[] BuildWidgetPdf() + { + using var doc = new Document(); + Page page = doc.NewPage(); + for (int i = 0; i < 8; i++) + { + var w = new Widget(page) + { + FieldName = "text_" + i, + FieldType = (int)WidgetType.Text, + Rect = new Rect(50, 50 + i * 28, 400, 72 + i * 28), + FieldValue = "value " + i, + }; + page.AddWidget(w); + } + return doc.Write(); + } + + private static byte[] BuildTextPdf() + { + using var doc = new Document(); + Page page = doc.NewPage(); + var html = new StringBuilder(); + html.Append(""); + for (int i = 0; i < 40; i++) + html.Append("

the quick brown fox jumps over the lazy dog ").Append(i).Append("

"); + html.Append(""); + page.InsertHtmlbox(page.Rect, html.ToString()); + return doc.Write(); + } + + /// One page with searchable text and form widgets (reporter MediaBox loop). + private static byte[] BuildFormAndTextPdf() + { + using var doc = new Document(); + Page page = doc.NewPage(); + var html = new StringBuilder(); + html.Append(""); + for (int i = 0; i < 40; i++) + html.Append("

the quick brown fox jumps over the lazy dog ").Append(i).Append("

"); + html.Append(""); + page.InsertHtmlbox(page.Rect, html.ToString()); + for (int i = 0; i < 8; i++) + { + var w = new Widget(page) + { + FieldName = "text_" + i, + FieldType = (int)WidgetType.Text, + Rect = new Rect(50, 50 + i * 28, 400, 72 + i * 28), + FieldValue = "value " + i, + }; + page.AddWidget(w); + } + return doc.Write(); + } + } +} diff --git a/MuPDF.NET.Test/WidgetTest.cs b/MuPDF.NET.Test/WidgetTest.cs index af3889b..f04d7a5 100644 --- a/MuPDF.NET.Test/WidgetTest.cs +++ b/MuPDF.NET.Test/WidgetTest.cs @@ -40,6 +40,7 @@ public void Text() Widget first = page.FirstWidget; Assert.Equal("Text", first.FieldTypeString); + Assert.Equal("Textfield-1", first.FieldName); doc.Save(Out("Text.pdf")); } diff --git a/MuPDF.NET/.editorconfig b/MuPDF.NET/.editorconfig index acec1f8..7823e4a 100644 --- a/MuPDF.NET/.editorconfig +++ b/MuPDF.NET/.editorconfig @@ -12,7 +12,7 @@ indent_style = space tab_width = 4 # New line preferences -end_of_line = crlf +end_of_line = lf insert_final_newline = false #### .NET Coding Conventions #### @@ -234,7 +234,7 @@ dotnet_naming_style.begins_with_i.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 indent_size = 4 -end_of_line = crlf +end_of_line = lf dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion diff --git a/MuPDF.NET/Annot.cs b/MuPDF.NET/Annot.cs index aafa307..4f4a9f8 100644 --- a/MuPDF.NET/Annot.cs +++ b/MuPDF.NET/Annot.cs @@ -115,13 +115,20 @@ public Rect Rect { get { - var r = mupdf.mupdf.pdf_bound_annot(NativeAnnot); + using var r = mupdf.mupdf.pdf_bound_annot(NativeAnnot); return Helpers.TransformRect(new Rect(r), DerotatePageMatrix); } } /// Annotation xref number. - public int Xref => mupdf.mupdf.pdf_to_num(mupdf.mupdf.pdf_annot_obj(NativeAnnot)); + public int Xref + { + get + { + using var obj = mupdf.mupdf.pdf_annot_obj(NativeAnnot); + return mupdf.mupdf.pdf_to_num(obj); + } + } /// Flags field ( / set_flags). public int Flags @@ -1363,7 +1370,7 @@ public void SetAP(string which, byte[] buffer) var fzBuf = Helpers.BufferFromBytes(buffer); var stream = mupdf.mupdf.pdf_add_stream(pdf, fzBuf, new mupdf.PdfObj(), 0); - var r = mupdf.mupdf.pdf_annot_rect(NativeAnnot); + using var r = mupdf.mupdf.pdf_annot_rect(NativeAnnot); var bbox = mupdf.mupdf.pdf_new_array(pdf, 4); mupdf.mupdf.pdf_array_push_real(bbox, r.x0); mupdf.mupdf.pdf_array_push_real(bbox, r.y0); diff --git a/MuPDF.NET/CHANGELOG.md b/MuPDF.NET/CHANGELOG.md index 0f4e154..7770491 100644 --- a/MuPDF.NET/CHANGELOG.md +++ b/MuPDF.NET/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### [3.28.2.3] - 2026-09-18 + +- Native leaks (#256, also 3.28.2.1 / 3.28.2.2): dispose owning SWIG wrappers so `pdf_drop_obj` / C++ destructors run. Widgets (`pdf_load_field_name2`, `pdf_annot_obj`, `Dispose`), `TextPage.Search` (`BorrowStextBlock` / `FirstStextLinePtr`), `GetKeyXref` / `XrefGetKey` / `XrefGetKeys` / `XrefSetKey`, `Annot.Rect` / `page.Rect` / `GetSvgImage` / text-trace / drawings / `pdf_to_rect`, `Widget.SyncFlags`, convert-to-PDF links. Stop `PdfObjBorrowed` / `FzRectBorrowed` on dict/load/annot getters. + ### [3.28.2] - 2026-08-14 Aligned MuPDF.NET with **PyMuPDF 1.28.2** and **MuPDF 1.28.2** (`MuPDF.NativeAssets` / `ArtifexMuPDFVersion` **1.28.2**; package **3.28.2**). diff --git a/MuPDF.NET/Document.cs b/MuPDF.NET/Document.cs index 40de8d1..19809a5 100644 --- a/MuPDF.NET/Document.cs +++ b/MuPDF.NET/Document.cs @@ -975,45 +975,56 @@ public void SetMetadata(Dictionary m) } var pdf = NativePdfDocument; - var trailer = mupdf.mupdf.pdf_trailer(pdf); - var infoKey = mupdf.mupdf.pdf_new_name("Info"); + using var trailer = mupdf.mupdf.pdf_trailer(pdf); + using var infoKey = mupdf.mupdf.pdf_new_name("Info"); var info = Helpers.PdfDictGet(trailer, infoKey); mupdf.PdfObj infoObj; - if (infoXref == 0) + mupdf.PdfObj loaded = null; + try { - // MuPDF: info_xref = doc.get_new_xref(); doc.UpdateObject(info_xref, "<<>>"); - infoXref = GetNewXref(); - UpdateObject(infoXref, "<<>>"); - XrefSetKey(-1, "Info", $"{infoXref} 0 R"); - info = Helpers.PdfDictGet(trailer, infoKey); - } + if (infoXref == 0) + { + // MuPDF: info_xref = doc.get_new_xref(); doc.UpdateObject(info_xref, "<<>>"); + infoXref = GetNewXref(); + UpdateObject(infoXref, "<<>>"); + XrefSetKey(-1, "Info", $"{infoXref} 0 R"); + info?.Dispose(); + info = Helpers.PdfDictGet(trailer, infoKey); + } - if (info.m_internal != null && mupdf.mupdf.pdf_is_indirect(info) != 0) - { - infoXref = mupdf.mupdf.pdf_to_num(info); - infoObj = mupdf.mupdf.pdf_load_object(pdf, infoXref); - } - else - infoObj = info; + if (info.m_internal != null && mupdf.mupdf.pdf_is_indirect(info) != 0) + { + infoXref = mupdf.mupdf.pdf_to_num(info); + loaded = mupdf.mupdf.pdf_load_object(pdf, infoXref); + infoObj = loaded; + } + else + infoObj = info; - foreach (var kv in m) - { - if (!keymap.TryGetValue(kv.Key, out var pdfKey) || pdfKey == null) - continue; + foreach (var kv in m) + { + if (!keymap.TryGetValue(kv.Key, out var pdfKey) || pdfKey == null) + continue; - var nameObj = mupdf.mupdf.pdf_new_name(pdfKey); - if (string.IsNullOrEmpty(kv.Value) || string.Equals(kv.Value, "none", StringComparison.OrdinalIgnoreCase) - || string.Equals(kv.Value, "null", StringComparison.OrdinalIgnoreCase)) - infoObj.pdf_dict_del(nameObj); - else - infoObj.pdf_dict_put_text_string(nameObj, kv.Value); - } + using var nameObj = mupdf.mupdf.pdf_new_name(pdfKey); + if (string.IsNullOrEmpty(kv.Value) || string.Equals(kv.Value, "none", StringComparison.OrdinalIgnoreCase) + || string.Equals(kv.Value, "null", StringComparison.OrdinalIgnoreCase)) + infoObj.pdf_dict_del(nameObj); + else + infoObj.pdf_dict_put_text_string(nameObj, kv.Value); + } - if (infoXref > 0) - pdf.pdf_update_object(infoXref, infoObj); + if (infoXref > 0) + pdf.pdf_update_object(infoXref, infoObj); - InitDoc(); + InitDoc(); + } + finally + { + loaded?.Dispose(); + info?.Dispose(); + } } // ─── TOC ──────────────────────────────────────────────────────── @@ -1126,7 +1137,7 @@ private void _extend_toc_items(List<(int level, string title, int page, Dictiona if (itemdict == null) throw new ValueErrorException("need non-simple TOC format"); itemdict["xref"] = xrefs[i]; - var bm = mupdf.mupdf.pdf_load_object(pdf, xref); + using var bm = mupdf.mupdf.pdf_load_object(pdf, xref); int flags = mupdf.mupdf.pdf_to_int(Helpers.PdfDictGet(bm, mupdf.mupdf.pdf_new_name("F"))); if (flags == 1) itemdict[italic] = true; @@ -2783,12 +2794,16 @@ public string XrefObject(int xref, bool compressed = false, bool ascii = false) obj = mupdf.mupdf.pdf_load_object(pdf, xref); else obj = mupdf.mupdf.pdf_trailer(pdf); - int compress = compressed ? 1 : 0; - int asciiVal = ascii ? 1 : 0; - using (var res = Helpers.JmObjectToBuffer(mupdf.mupdf.pdf_resolve_indirect(obj), compress, asciiVal)) + using (obj) { - string text = Helpers.JmEscapeStrFromBuffer(res); - return text; + int compress = compressed ? 1 : 0; + int asciiVal = ascii ? 1 : 0; + using var resolved = mupdf.mupdf.pdf_resolve_indirect(obj); + using (var res = Helpers.JmObjectToBuffer(resolved, compress, asciiVal)) + { + string text = Helpers.JmEscapeStrFromBuffer(res); + return text; + } } } /// @@ -2905,14 +2920,28 @@ private static string PdfObjToKeyValueString(mupdf.PdfObj sub) EnsureNotClosed(); EnsureValidXrefDict(xref); var pdf = NativePdfDocument; - var obj = xref > 0 ? mupdf.mupdf.pdf_load_object(pdf, xref) : mupdf.mupdf.pdf_trailer(pdf); + using var obj = xref > 0 ? mupdf.mupdf.pdf_load_object(pdf, xref) : mupdf.mupdf.pdf_trailer(pdf); if (obj.m_internal == null) return ("null", "null"); - // Prefer path lookup; fall back to direct name. - var sub = Helpers.PdfDictGetp(obj, key); - if (sub.m_internal == null && !string.IsNullOrEmpty(key) && key[0] != '/') - sub = Helpers.PdfDictGet(obj, mupdf.mupdf.pdf_new_name(key)); - if (sub.m_internal == null) return ("null", "null"); + // Owning SWIG wrappers: pdf_dict_getp / pdf_dict_get keep the result. + // PdfObjBorrowed used to strip ownership without pdf_drop_obj, leaking one + // keep per distinct key (same extra keep on a later call does not grow RSS). + using var subPath = mupdf.mupdf.pdf_dict_getp(obj, key); + if (subPath.m_internal != null) + return PdfObjKeyTypeAndValue(subPath); + + if (string.IsNullOrEmpty(key) || key[0] == '/') + return ("null", "null"); + + using var nameKey = mupdf.mupdf.pdf_new_name(key); + using var subName = mupdf.mupdf.pdf_dict_get(obj, nameKey); + if (subName.m_internal == null) + return ("null", "null"); + return PdfObjKeyTypeAndValue(subName); + } + + private static (string type, string value) PdfObjKeyTypeAndValue(mupdf.PdfObj sub) + { if (mupdf.mupdf.pdf_is_indirect(sub) != 0) return ("xref", $"{mupdf.mupdf.pdf_to_num(sub)} 0 R"); if (mupdf.mupdf.pdf_is_int(sub) != 0) return ("int", $"{mupdf.mupdf.pdf_to_int(sub)}"); if (mupdf.mupdf.pdf_is_real(sub) != 0) return ("float", PdfObjToKeyValueString(sub)); @@ -2935,11 +2964,14 @@ public List XrefGetKeys(int xref) EnsureNotClosed(); EnsureValidXrefDict(xref); var pdf = NativePdfDocument; - var obj = xref > 0 ? mupdf.mupdf.pdf_load_object(pdf, xref) : mupdf.mupdf.pdf_trailer(pdf); + using var obj = xref > 0 ? mupdf.mupdf.pdf_load_object(pdf, xref) : mupdf.mupdf.pdf_trailer(pdf); int n = mupdf.mupdf.pdf_dict_len(obj); var rc = new List(n); for (int i = 0; i < n; i++) - rc.Add(mupdf.mupdf.pdf_to_name(Helpers.PdfDictGetKey(obj, i))); + { + using var keyObj = mupdf.mupdf.pdf_dict_get_key(obj, i); + rc.Add(mupdf.mupdf.pdf_to_name(keyObj)); + } return rc; } @@ -2978,9 +3010,9 @@ public void XrefSetKey(int xref, string key, string value) throw new ValueErrorException("bad 'value'"); EnsureValidXrefDict(xref); var pdf = NativePdfDocument; - var obj = xref > 0 ? mupdf.mupdf.pdf_load_object(pdf, xref) : mupdf.mupdf.pdf_trailer(pdf); + using var obj = xref > 0 ? mupdf.mupdf.pdf_load_object(pdf, xref) : mupdf.mupdf.pdf_trailer(pdf); // MuPDF JM_set_object_value: "null" writes a PDF null object (key remains in the dict). - var newObj = Helpers.JmSetObjectValue(pdf, obj, key, value); + using var newObj = Helpers.JmSetObjectValue(pdf, obj, key, value); if (newObj?.m_internal == null) return; if (xref != -1) @@ -2991,10 +3023,9 @@ public void XrefSetKey(int xref, string key, string value) int n = mupdf.mupdf.pdf_dict_len(newObj); for (int i = 0; i < n; i++) { - mupdf.mupdf.pdf_dict_put( - obj, - Helpers.PdfDictGetKey(newObj, i), - Helpers.PdfDictGetVal(newObj, i)); + using var dictKey = mupdf.mupdf.pdf_dict_get_key(newObj, i); + using var dictVal = mupdf.mupdf.pdf_dict_get_val(newObj, i); + mupdf.mupdf.pdf_dict_put(obj, dictKey, dictVal); } } /// @@ -4145,18 +4176,22 @@ private void JM_gather_forms(mupdf.PdfDocument doc, mupdf.PdfObj dict_, List PageAnnotXrefs(int n) public (string name, string ext, string type, byte[] content) ExtractFont(int xref) { var pdf = NativePdfDocument; - var obj = mupdf.mupdf.pdf_load_object(pdf, xref); + using var obj = mupdf.mupdf.pdf_load_object(pdf, xref); string name = "", ext = "", type = ""; byte[] content = Array.Empty(); @@ -5538,9 +5573,12 @@ void re_target(mupdf.PdfDocument pdfDoc, mupdf.PdfObj acroFlds, int x1, mupdf.Pd if (kids2.pdf_is_array() == 0) { var widget = mupdf.mupdf.pdf_load_object(pdfDoc, x2); - widget.pdf_dict_del(mupdf.mupdf.pdf_new_name("T")); - widget.pdf_dict_put(mupdf.mupdf.pdf_new_name("Parent"), w1_ind); - kids1.pdf_array_push(w2_ind); + using (widget) + { + widget.pdf_dict_del(mupdf.mupdf.pdf_new_name("T")); + widget.pdf_dict_put(mupdf.mupdf.pdf_new_name("Parent"), w1_ind); + kids1.pdf_array_push(w2_ind); + } } else { @@ -5590,8 +5628,8 @@ void new_target(mupdf.PdfDocument pdfDoc, mupdf.PdfObj acroFlds, int x1, mupdf.P acroFlds.pdf_array_push(new_ind); } - var w1 = mupdf.mupdf.pdf_load_object(pdf, xref1); - var w2 = mupdf.mupdf.pdf_load_object(pdf, xref2); + using var w1 = mupdf.mupdf.pdf_load_object(pdf, xref1); + using var w2 = mupdf.mupdf.pdf_load_object(pdf, xref2); var kids1 = Helpers.PdfObjDictGet(w1,mupdf.mupdf.pdf_new_name("Kids")); var kids2 = Helpers.PdfObjDictGet(w2,mupdf.mupdf.pdf_new_name("Kids")); @@ -5660,7 +5698,7 @@ void deduplicate_names(mupdf.PdfDocument pdf, mupdf.PdfObj acro_fields, bool joi else { string newname = name + $" [{xref1}]"; // append this to the name - var wobject = mupdf.mupdf.pdf_load_object(pdf, xref1); + using var wobject = mupdf.mupdf.pdf_load_object(pdf, xref1); wobject.pdf_dict_put_text_string(mupdf.mupdf.pdf_new_name("T"), newname); } } @@ -5671,7 +5709,8 @@ void deduplicate_names(mupdf.PdfDocument pdf, mupdf.PdfObj acro_fields, bool joi mupdf.PdfObj get_acroform(Document doc) { var pdf = doc.NativePdfDocument; - return Helpers.PdfDictGetp(mupdf.mupdf.pdf_trailer(pdf), "Root/AcroForm"); + using var trailer = mupdf.mupdf.pdf_trailer(pdf); + return Helpers.PdfDictGetp(trailer, "Root/AcroForm"); } mupdf.PdfObj acro; @@ -5728,7 +5767,7 @@ mupdf.PdfObj get_acroform(Document doc) { if (wtype != AnnotationType.Widget) continue; - var w_obj = mupdf.mupdf.pdf_load_object(srcpdf, xref); + using var w_obj = mupdf.mupdf.pdf_load_object(srcpdf, xref); w_obj.pdf_dict_del(mupdf.mupdf.pdf_new_name("P")); var (parent_xref, old_kids) = kids_xrefs(w_obj); @@ -5746,7 +5785,7 @@ mupdf.PdfObj get_acroform(Document doc) foreach (int xref in parents.Keys) { - var parent = mupdf.mupdf.pdf_load_object(srcpdf, xref); + using var parent = mupdf.mupdf.pdf_load_object(srcpdf, xref); var parent_graft = gm.pdf_graft_mapped_object(parent); var parent_tar = mupdf.mupdf.pdf_add_object(tarpdf, parent_graft); var kids_xrefs_new = get_kids(parent_tar, new List()); @@ -5778,7 +5817,7 @@ mupdf.PdfObj get_acroform(Document doc) foreach (int xref in w_xrefs) { - var w_obj = mupdf.mupdf.pdf_load_object(srcpdf, xref); + using var w_obj = mupdf.mupdf.pdf_load_object(srcpdf, xref); var is_aac = mupdf.mupdf.pdf_is_dict(w_obj.pdf_dict_getp("AA/C")); int parent_xref = Helpers.PdfObjDictGet(w_obj,mupdf.mupdf.pdf_new_name("Parent")).pdf_to_num(); mupdf.PdfObj w_obj_tar_ind; @@ -7613,17 +7652,19 @@ public Rect PageCropBox(int pno) var pdf = NativePdfDocument; if (n >= pageCount) throw new ValueErrorException(Constants.MSG_BAD_PAGENO); - var page_obj = mupdf.mupdf.pdf_lookup_page_obj(pdf, n); - var cropbox = Helpers.PdfDictGetInheritable(page_obj, mupdf.mupdf.pdf_new_name("CropBox")); + using var page_obj = mupdf.mupdf.pdf_lookup_page_obj(pdf, n); + using var cropName = mupdf.mupdf.pdf_new_name("CropBox"); + using var cropbox = mupdf.mupdf.pdf_dict_get_inheritable(page_obj, cropName); if (cropbox.m_internal != null) { - var r = mupdf.mupdf.pdf_to_rect(cropbox); + using var r = mupdf.mupdf.pdf_to_rect(cropbox); return new Rect(r.x0, r.y0, r.x1, r.y1); } - var mb = Helpers.PdfDictGetInheritable(page_obj, mupdf.mupdf.pdf_new_name("MediaBox")); + using var mbName = mupdf.mupdf.pdf_new_name("MediaBox"); + using var mb = mupdf.mupdf.pdf_dict_get_inheritable(page_obj, mbName); if (mb.m_internal != null) { - var r = mupdf.mupdf.pdf_to_rect(mb); + using var r = mupdf.mupdf.pdf_to_rect(mb); return new Rect(r.x0, r.y0, r.x1, r.y1); } return new Rect(0, 0, 595, 842); diff --git a/MuPDF.NET/Helpers.cs b/MuPDF.NET/Helpers.cs index 6d31521..62a650a 100644 --- a/MuPDF.NET/Helpers.cs +++ b/MuPDF.NET/Helpers.cs @@ -545,18 +545,22 @@ mupdf.FzFont fertig(mupdf.FzFont font) return fertig(font); } - /// FontDescriptor for Type0/CID or simple fonts. + /// FontDescriptor for Type0/CID or simple fonts. Caller must dispose the result. internal static mupdf.PdfObj JM_get_font_descriptor(mupdf.PdfObj fontObj) { if (fontObj.m_internal == null) return new mupdf.PdfObj(); - var desft = PdfDictGet(fontObj, mupdf.mupdf.pdf_new_name("DescendantFonts")); + using var desftKey = mupdf.mupdf.pdf_new_name("DescendantFonts"); + using var desft = PdfDictGet(fontObj, desftKey); if (desft.m_internal != null) { - var first = mupdf.mupdf.pdf_resolve_indirect(mupdf.mupdf.pdf_array_get(desft, 0)); - return PdfDictGet(first, mupdf.mupdf.pdf_new_name("FontDescriptor")); + using var firstArr = mupdf.mupdf.pdf_array_get(desft, 0); + using var first = mupdf.mupdf.pdf_resolve_indirect(firstArr); + using var fdKey = mupdf.mupdf.pdf_new_name("FontDescriptor"); + return PdfDictGet(first, fdKey); } - return PdfDictGet(fontObj, mupdf.mupdf.pdf_new_name("FontDescriptor")); + using var descKey = mupdf.mupdf.pdf_new_name("FontDescriptor"); + return PdfDictGet(fontObj, descKey); } /// Returns embedded font file bytes for a font xref. @@ -564,8 +568,8 @@ internal static byte[] JM_get_fontbuffer(mupdf.PdfDocument pdf, int xref) { if (xref < 1) return null; - var o = mupdf.mupdf.pdf_load_object(pdf, xref); - var desc = JM_get_font_descriptor(o); + using var o = mupdf.mupdf.pdf_load_object(pdf, xref); + using var desc = JM_get_font_descriptor(o); if (desc.m_internal == null) { message("invalid font - FontDescriptor missing"); @@ -1110,7 +1114,11 @@ internal static mupdf.FzColorspace DeviceColorspace(int componentCount) return DeviceGrayColorspace; } - /// Indirect PDF objects from MuPDF getters are borrowed; SWIG must not call pdf_drop_obj. + /// + /// Strip SWIG ownership so ~PdfObj does not pdf_drop_obj. + /// Only for add/graft wrappers that already + /// transferred into the document. Getters must return owning wrappers (#256). + /// internal static mupdf.PdfObj PdfObjBorrowed(mupdf.PdfObj wrapper) { if (wrapper == null || wrapper.m_internal == null) @@ -1129,26 +1137,32 @@ internal static mupdf.PdfObj PdfObjBorrowed(mupdf.PdfObj wrapper) return new mupdf.PdfObj(handle, false); } + /// + /// C++ getters keep; return the owning SWIG wrapper so + /// can pdf_drop_obj. Do not strip + /// cMemOwn (that was the #256 leak). Callers should dispose when convenient; + /// GC still drops the extra keep if they do not. + /// internal static mupdf.PdfObj PdfAnnotObj(mupdf.PdfAnnot annot) - => PdfObjBorrowed(mupdf.mupdf.pdf_annot_obj(annot)); + => mupdf.mupdf.pdf_annot_obj(annot); internal static mupdf.PdfObj PdfPageObj(mupdf.PdfPage page) - => PdfObjBorrowed(page.obj()); + => page.obj(); internal static mupdf.PdfObj PdfLoadObject(mupdf.PdfDocument doc, int xref) - => PdfObjBorrowed(mupdf.mupdf.pdf_load_object(doc, xref)); + => mupdf.mupdf.pdf_load_object(doc, xref); internal static mupdf.PdfObj PdfTrailer(mupdf.PdfDocument doc) - => PdfObjBorrowed(mupdf.mupdf.pdf_trailer(doc)); + => mupdf.mupdf.pdf_trailer(doc); internal static mupdf.PdfObj PdfArrayGet(mupdf.PdfObj arr, int index) - => PdfObjBorrowed(mupdf.mupdf.pdf_array_get(arr, index)); + => mupdf.mupdf.pdf_array_get(arr, index); internal static mupdf.PdfObj PdfResolveIndirect(mupdf.PdfObj obj) - => PdfObjBorrowed(mupdf.mupdf.pdf_resolve_indirect(obj)); + => mupdf.mupdf.pdf_resolve_indirect(obj); internal static mupdf.PdfObj PdfLookupPageObj(mupdf.PdfDocument doc, int pageNo) - => PdfObjBorrowed(mupdf.mupdf.pdf_lookup_page_obj(doc, pageNo)); + => mupdf.mupdf.pdf_lookup_page_obj(doc, pageNo); internal static mupdf.PdfObj PdfAddObject(mupdf.PdfDocument doc, mupdf.PdfObj obj) { @@ -1284,10 +1298,10 @@ internal static void PdfArrayInsert(mupdf.PdfObj arr, mupdf.PdfObj val, int inde } internal static mupdf.PdfObj PdfDictGet(mupdf.PdfObj dict, mupdf.PdfObj key) - => PdfObjBorrowed(mupdf.mupdf.pdf_dict_get(dict, key)); + => mupdf.mupdf.pdf_dict_get(dict, key); internal static mupdf.PdfObj PdfDictGets(mupdf.PdfObj dict, string key) - => PdfObjBorrowed(mupdf.mupdf.pdf_dict_gets(dict, key)); + => mupdf.mupdf.pdf_dict_gets(dict, key); internal static void PdfDictPuts(mupdf.PdfObj dict, string key, mupdf.PdfObj val) { @@ -1483,7 +1497,7 @@ internal static void DropFzPixmap(ref mupdf.FzPixmap? pm) } internal static mupdf.PdfObj PdfDictGetInheritable(mupdf.PdfObj dict, mupdf.PdfObj key) - => PdfObjBorrowed(mupdf.mupdf.pdf_dict_get_inheritable(dict, key)); + => mupdf.mupdf.pdf_dict_get_inheritable(dict, key); internal static mupdf.PdfObj PdfDictGetInheritable(mupdf.PdfObj dict, string key) { @@ -1502,22 +1516,22 @@ internal static mupdf.PdfObj PdfObjDictGet(mupdf.PdfObj dict, string key) => PdfDictGets(dict, key); internal static mupdf.PdfObj PdfDictGetp(mupdf.PdfObj dict, string path) - => PdfObjBorrowed(mupdf.mupdf.pdf_dict_getp(dict, path)); + => mupdf.mupdf.pdf_dict_getp(dict, path); internal static mupdf.PdfObj PdfDictGetpInheritable(mupdf.PdfObj dict, string path) - => PdfObjBorrowed(mupdf.mupdf.pdf_dict_getp_inheritable(dict, path)); + => mupdf.mupdf.pdf_dict_getp_inheritable(dict, path); internal static mupdf.PdfObj PdfDictGetsInheritable(mupdf.PdfObj dict, string key) - => PdfObjBorrowed(mupdf.mupdf.pdf_dict_gets_inheritable(dict, key)); + => mupdf.mupdf.pdf_dict_gets_inheritable(dict, key); internal static mupdf.PdfObj PdfDictGetVal(mupdf.PdfObj dict, int idx) - => PdfObjBorrowed(mupdf.mupdf.pdf_dict_get_val(dict, idx)); + => mupdf.mupdf.pdf_dict_get_val(dict, idx); internal static mupdf.PdfObj PdfDictGetKey(mupdf.PdfObj dict, int idx) - => PdfObjBorrowed(mupdf.mupdf.pdf_dict_get_key(dict, idx)); + => mupdf.mupdf.pdf_dict_get_key(dict, idx); internal static mupdf.PdfObj PdfDictGeta(mupdf.PdfObj dict, mupdf.PdfObj key, mupdf.PdfObj abbrev) - => PdfObjBorrowed(mupdf.mupdf.pdf_dict_geta(dict, key, abbrev)); + => mupdf.mupdf.pdf_dict_geta(dict, key, abbrev); internal static mupdf.PdfObj PdfDictGeta(mupdf.PdfObj dict, string key, string abbrev) { @@ -1535,13 +1549,13 @@ internal static mupdf.PdfObj PdfDictGeta(mupdf.PdfObj dict, string key, string a } internal static mupdf.FzLink PdfCreateLink(mupdf.PdfPage page, mupdf.FzRect bbox, string uri) - => FzLinkBorrowed(mupdf.mupdf.pdf_create_link(page, bbox, uri)); + => mupdf.mupdf.pdf_create_link(page, bbox, uri); internal static mupdf.PdfObj PdfObjDictGet(mupdf.PdfObj dict, mupdf.PdfObj key) => PdfDictGet(dict, key); internal static mupdf.PdfObj PdfObjDictGet(mupdf.PdfObj dict, int key) - => PdfObjBorrowed(dict.pdf_dict_get(key)); + => dict.pdf_dict_get(key); internal static mupdf.PdfObj PdfObjDictGeta(mupdf.PdfObj dict, mupdf.PdfObj key, mupdf.PdfObj abbrev) => PdfDictGeta(dict, key, abbrev); @@ -1552,82 +1566,39 @@ internal static mupdf.PdfObj PdfObjDictGeta(mupdf.PdfObj dict, string key, strin internal static mupdf.PdfObj PdfObjDictGetp(mupdf.PdfObj dict, string path) => PdfDictGetp(dict, path); + /// + /// Owning from fz_bound_page. Callers must + /// it (same class of leak as #256). + /// internal static mupdf.FzRect FzBoundPage(mupdf.FzPage page) - => FzRectBorrowed(mupdf.mupdf.fz_bound_page(page)); - - /// pdf_annot_rect / pdf_bound_annot return rects owned by the annot; do not drop. - internal static mupdf.FzRect FzRectBorrowed(mupdf.FzRect wrapper) - { - if (wrapper == null || mupdf.FzRect.getCPtr(wrapper).Handle == IntPtr.Zero) - return new mupdf.FzRect(); - IntPtr handle; - try - { - handle = mupdf.FzRect.swigRelease(wrapper).Handle; - } - catch (ApplicationException) - { - handle = mupdf.FzRect.getCPtr(wrapper).Handle; - } - if (handle == IntPtr.Zero) - return new mupdf.FzRect(); - return new mupdf.FzRect(handle, false); - } + => mupdf.mupdf.fz_bound_page(page); + /// Owning rect from pdf_annot_rect. Callers must dispose. internal static mupdf.FzRect PdfAnnotRect(mupdf.PdfAnnot annot) - => FzRectBorrowed(mupdf.mupdf.pdf_annot_rect(annot)); + => mupdf.mupdf.pdf_annot_rect(annot); + /// Owning rect from pdf_bound_annot. Callers must dispose. internal static mupdf.FzRect PdfBoundAnnot(mupdf.PdfAnnot annot) - => FzRectBorrowed(mupdf.mupdf.pdf_bound_annot(annot)); - - internal static mupdf.FzMatrix FzMatrixBorrowed(mupdf.FzMatrix wrapper) - { - if (wrapper == null || mupdf.FzMatrix.getCPtr(wrapper).Handle == IntPtr.Zero) - return new mupdf.FzMatrix(); - IntPtr handle; - try - { - handle = mupdf.FzMatrix.swigRelease(wrapper).Handle; - } - catch (ApplicationException) - { - handle = mupdf.FzMatrix.getCPtr(wrapper).Handle; - } - if (handle == IntPtr.Zero) - return new mupdf.FzMatrix(); - return new mupdf.FzMatrix(handle, false); - } + => mupdf.mupdf.pdf_bound_annot(annot); + /// Owning rect from pdf_dict_get_rect. Callers must dispose. internal static mupdf.FzRect PdfDictGetRect(mupdf.PdfObj dict, mupdf.PdfObj key) - => FzRectBorrowed(mupdf.mupdf.pdf_dict_get_rect(dict, key)); + => mupdf.mupdf.pdf_dict_get_rect(dict, key); internal static mupdf.FzRect PdfDictGetRect(mupdf.PdfObj dict, string key) { - var keyObj = mupdf.mupdf.pdf_new_name(key); - try - { - return PdfDictGetRect(dict, keyObj); - } - finally - { - keyObj.Dispose(); - } + using var keyObj = mupdf.mupdf.pdf_new_name(key); + return PdfDictGetRect(dict, keyObj); } + /// Owning matrix from pdf_dict_get_matrix. Callers must dispose. internal static mupdf.FzMatrix PdfDictGetMatrix(mupdf.PdfObj dict, mupdf.PdfObj key) - => FzMatrixBorrowed(mupdf.mupdf.pdf_dict_get_matrix(dict, key)); + => mupdf.mupdf.pdf_dict_get_matrix(dict, key); internal static mupdf.FzMatrix PdfDictGetMatrix(mupdf.PdfObj dict, string key) { - var keyObj = mupdf.mupdf.pdf_new_name(key); - try - { - return PdfDictGetMatrix(dict, keyObj); - } - finally - { - keyObj.Dispose(); - } + using var keyObj = mupdf.mupdf.pdf_new_name(key); + return PdfDictGetMatrix(dict, keyObj); } internal static mupdf.PdfAnnot PdfFirstAnnot(mupdf.PdfPage page) @@ -1687,11 +1658,13 @@ internal static mupdf.FzLink FzLinkBorrowed(mupdf.FzLink wrapper) return new mupdf.FzLink(handle, false); } + /// + /// fz_load_links keeps the list head; return the owning wrapper. + /// Page-owned nodes walked via m_internal.next must not be wrapped with + /// cMemOwn true. is also owning — dispose it. + /// internal static mupdf.FzLink FzLoadLinks(mupdf.FzPage page) - => FzLinkBorrowed(mupdf.mupdf.fz_load_links(page)); - - internal static mupdf.FzLink FzLinkNext(mupdf.FzLink link) - => FzLinkBorrowed(link.next()); + => mupdf.mupdf.fz_load_links(page); internal static bool InRange(int val, int low, int high) => val >= low && val <= high; internal static bool InRange(float val, float low, float high) => val >= low && val <= high; @@ -1841,7 +1814,8 @@ internal static void JM_add_annot_id(mupdf.PdfAnnot annot, string stem, mupdf.Pd /// Return the inheritable /Rotate value of a PDF page. internal static int PageRotation(mupdf.PdfPage page) { - int rotate = PdfDictGetInheritableInt(PdfPageObj(page), "Rotate"); + using var pageObj = page.obj(); + int rotate = PdfDictGetInheritableInt(pageObj, "Rotate"); return JmNormRotation(rotate); } @@ -1873,27 +1847,27 @@ internal static Matrix JM_rotate_page_matrix(mupdf.PdfPage page) if (rotation == 0) return Matrix.Identity; // no rotation - var cb = Helpers.PdfDictGetsInheritable(PdfPageObj(page), "CropBox"); - Rect cbSize; + using var pageObj = page.obj(); + using var cb = mupdf.mupdf.pdf_dict_gets_inheritable(pageObj, "CropBox"); + mupdf.FzRect boxRect; if (cb.m_internal != null) + boxRect = mupdf.mupdf.pdf_to_rect(cb); + else { - cbSize = new Rect(mupdf.mupdf.pdf_to_rect(cb)); + using var mb = mupdf.mupdf.pdf_dict_gets_inheritable(pageObj, "MediaBox"); + boxRect = mupdf.mupdf.pdf_to_rect(mb); } - else + using (boxRect) { - var mb = Helpers.PdfDictGetsInheritable(PdfPageObj(page), "MediaBox"); - cbSize = new Rect(mupdf.mupdf.pdf_to_rect(mb)); + float w = boxRect.x1 - boxRect.x0; + float h = boxRect.y1 - boxRect.y0; + if (rotation == 90) + return new Matrix(0, 1, -1, 0, h, 0); + else if (rotation == 180) + return new Matrix(-1, 0, 0, -1, w, h); + else + return new Matrix(0, -1, 1, 0, 0, w); } - float w = cbSize.Width; - float h = cbSize.Height; - //log( '{=h w}') - if (rotation == 90) - return new Matrix(0, 1, -1, 0, h, 0); - else if (rotation == 180) - return new Matrix(-1, 0, 0, -1, w, h); - else - return new Matrix(0, -1, 1, 0, 0, w); - //log( 'returning {m=}') } internal static Matrix DerotatePageMatrix(Page page) @@ -2195,10 +2169,10 @@ internal static mupdf.PdfObj JM_xobject_from_page(mupdf.PdfDocument pdfout, mupd if (xref > 0) return PdfNewIndirect(pdfout, xref, 0); - var spageref = PdfPageObj(srcpage); - var mediabox = mupdf.mupdf.pdf_to_rect( - Helpers.PdfDictGetsInheritable(spageref, "MediaBox")); - var resourcesSrc = Helpers.PdfDictGetsInheritable(spageref, "Resources"); + using var spageref = srcpage.obj(); + using var mbObj = mupdf.mupdf.pdf_dict_gets_inheritable(spageref, "MediaBox"); + using var mediabox = mupdf.mupdf.pdf_to_rect(mbObj); + using var resourcesSrc = mupdf.mupdf.pdf_dict_gets_inheritable(spageref, "Resources"); var resources = gmap?.m_internal != null ? PdfGraftMappedObject(gmap, resourcesSrc) : PdfGraftObject(pdfout, resourcesSrc); @@ -2360,54 +2334,47 @@ Convert any MuPDF document to a PDF var page = mupdf.mupdf.fz_load_page(doc, i); try { - var mediabox = FzBoundPage(page); + using var mediabox = FzBoundPage(page); var (dev, resources, contents) = pdfout.pdf_page_write(mediabox); - mupdf.mupdf.fz_run_page(page, dev, new mupdf.FzMatrix(), new mupdf.FzCookie()); + using var identity = new mupdf.FzMatrix(); + using var cookie = new mupdf.FzCookie(); + mupdf.mupdf.fz_run_page(page, dev, identity, cookie); mupdf.mupdf.fz_close_device(dev); dev.Dispose(); var page_obj = PdfAddPage(pdfout, mediabox, rot, resources, contents); PdfInsertPage(pdfout, -1, page_obj); - // also copy links to the output PDF page - // get the PDF page we've just created - var pdf_page = pdfout.pdf_load_page(i); - - // loop through source page links - var link = FzLoadLinks(page); // load first link - while (link.m_internal != null) // break loop when link is None + using var pdf_page = pdfout.pdf_load_page(i); + using var links = FzLoadLinks(page); + for (var node = links.m_internal; node != null; node = node.next) { - string uri = link.uri(); // URI string - using (var linkRect = link.rect()) - { - var rect = new mupdf.FzRect(linkRect); // link "from" rectangle - bool isExternal = mupdf.mupdf.fz_is_external_link(uri) != 0; + string uri = node.uri; + var fr = node.rect; + using var rect = new mupdf.FzRect(fr.x0, fr.y0, fr.x1, fr.y1); + bool isExternal = mupdf.mupdf.fz_is_external_link(uri) != 0; - if (isExternal) // external links can be copied directly - { - PdfCreateLink(pdf_page, rect, uri); - } - else // internal links done when PDF is complete + if (isExternal) + { + using (PdfCreateLink(pdf_page, rect, uri)) { } + } + else + { + var outparams = new mupdf.ll_fz_resolve_link_outparams(); + var ret = mupdf.mupdf.ll_fz_resolve_link_outparams_fn(doc.m_internal, uri, outparams); + internalLinks.Add(new JmInternalLink { - // find target of internal link - var outparams = new mupdf.ll_fz_resolve_link_outparams(); - var ret = mupdf.mupdf.ll_fz_resolve_link_outparams_fn(doc.m_internal, uri, outparams); - internalLinks.Add(new JmInternalLink - { - Page = i, - Chapter = ret.chapter, - PageLoc = ret.page, - From = new mupdf.FzRect(rect), - H = rect.y1 - rect.y0, - W = rect.x1 - rect.x0, - Xp = outparams.xp, - Yp = outparams.yp, - }); - ret.Dispose(); - } + Page = i, + Chapter = ret.chapter, + PageLoc = ret.page, + From = new mupdf.FzRect(rect), + H = rect.y1 - rect.y0, + W = rect.x1 - rect.x0, + Xp = outparams.xp, + Yp = outparams.yp, + }); + ret.Dispose(); } - link = FzLinkNext(link); } - pdf_page.Dispose(); } finally { @@ -2432,7 +2399,7 @@ Convert any MuPDF document to a PDF var rect = ilink.From; var linkDest = new mupdf.FzLinkDest(dest); string uri = mupdf.mupdf.pdf_new_uri_from_explicit_dest(linkDest); - PdfCreateLink(pdf_page, rect, uri); + using (PdfCreateLink(pdf_page, rect, uri)) { } linkDest.Dispose(); dest.Dispose(); rect.Dispose(); @@ -3284,47 +3251,34 @@ internal static string JM_copy_rectangle(mupdf.FzStextPage page, mupdf.FzRect ar } /// - /// Non-owning view of a block inside a live stext page. - /// Do not use new FzStextBlock(internal_) — that wrapper is owning and its finalizer - /// can delete in-page data while other code still walks lines/chars. + /// C++ wrapper around an in-page fz_stext_block. Owns the wrapper only: + /// ~FzStextBlock does not drop in-page stext (debug instance counter / default). + /// Callers must the wrapper. /// internal static mupdf.FzStextBlock BorrowStextBlock(mupdf.fz_stext_block block) { if (block == null) return null; - global::System.IntPtr cPtr = mupdf.mupdfPINVOKE.new_FzStextBlock__SWIG_2(mupdf.fz_stext_block.getCPtr(block)); - if (mupdf.mupdfPINVOKE.SWIGPendingException.Pending) - throw mupdf.mupdfPINVOKE.SWIGPendingException.Retrieve(); - return new mupdf.FzStextBlock(cPtr, false); + return new mupdf.FzStextBlock(block); } /// First line of a text block; then walk line.next. internal static mupdf.fz_stext_line FirstStextLinePtr(mupdf.fz_stext_block block) { - var wrap = BorrowStextBlock(block); - var iter = wrap.begin(); - try - { - return iter.__deref__()?.m_internal; - } - finally - { - iter.Dispose(); - } + using var wrap = BorrowStextBlock(block); + if (wrap == null) + return null; + using var iter = wrap.begin(); + // Iterator.m_internal is the in-page line pointer. __deref__() would + // allocate a C++ FzStextLine with cMemOwn false and leak it. + return iter.m_internal; } /// First line via a cached non-owning block view (see ). internal static mupdf.fz_stext_line FirstStextLine(mupdf.FzStextBlock block) { - var iter = block.begin(); - try - { - return iter.__deref__()?.m_internal; - } - finally - { - iter.Dispose(); - } + using var iter = block.begin(); + return iter.m_internal; } /// Decodes PDF raw Unicode escape sequences in a buffer. @@ -4359,35 +4313,46 @@ internal static PdfFilterOptionsRef MakePdfFilterOptions( /// Returns the page MediaBox as a . internal static mupdf.FzRect JmMediabox(mupdf.PdfObj pageObj) { - var mediabox = mupdf.mupdf.pdf_to_rect( - Helpers.PdfDictGetsInheritable(pageObj, "MediaBox")); + // pdf_dict_gets_inheritable keeps; PdfObjBorrowed used to strip ownership + // without pdf_drop_obj (same class of leak as XrefGetKey / #256). + using var mbObj = mupdf.mupdf.pdf_dict_gets_inheritable(pageObj, "MediaBox"); + using var mediabox = mupdf.mupdf.pdf_to_rect(mbObj); + float x0 = mediabox.x0; + float y0 = mediabox.y0; + float x1 = mediabox.x1; + float y1 = mediabox.y1; if (mupdf.mupdf.fz_is_empty_rect(mediabox) != 0 || mupdf.mupdf.fz_is_infinite_rect(mediabox) != 0) { - mediabox.x0 = 0; - mediabox.y0 = 0; - mediabox.x1 = 612; - mediabox.y1 = 792; + x0 = 0; + y0 = 0; + x1 = 612; + y1 = 792; } return mupdf.mupdf.fz_make_rect( - Math.Min(mediabox.x0, mediabox.x1), - Math.Min(mediabox.y0, mediabox.y1), - Math.Max(mediabox.x0, mediabox.x1), - Math.Max(mediabox.y0, mediabox.y1)); + Math.Min(x0, x1), + Math.Min(y0, y1), + Math.Max(x0, x1), + Math.Max(y0, y1)); } /// Returns the page CropBox as a . internal static mupdf.FzRect JmCropbox(mupdf.PdfObj pageObj) { - var mediabox = JmMediabox(pageObj); - var cropbox = mupdf.mupdf.pdf_to_rect( - Helpers.PdfDictGetsInheritable(pageObj, "CropBox")); + using var mediabox = JmMediabox(pageObj); + using var cropObj = mupdf.mupdf.pdf_dict_gets_inheritable(pageObj, "CropBox"); + using var cropbox = mupdf.mupdf.pdf_to_rect(cropObj); + float mx0 = mediabox.x0, my0 = mediabox.y0, mx1 = mediabox.x1, my1 = mediabox.y1; + float x0 = cropbox.x0, y0 = cropbox.y0, x1 = cropbox.x1, y1 = cropbox.y1; if (mupdf.mupdf.fz_is_infinite_rect(cropbox) != 0 || mupdf.mupdf.fz_is_empty_rect(cropbox) != 0) - cropbox = mediabox; - float y0 = mediabox.y1 - cropbox.y1; - float y1 = mediabox.y1 - cropbox.y0; - cropbox.y0 = y0; - cropbox.y1 = y1; - return cropbox; + { + x0 = mx0; + y0 = my0; + x1 = mx1; + y1 = my1; + } + float outY0 = my1 - y1; + float outY1 = my1 - y0; + return mupdf.mupdf.fz_make_rect(x0, outY0, x1, outY1); } /// Converts rectangle-like input to . @@ -5016,7 +4981,7 @@ internal static void JmSetWidgetProperties(mupdf.PdfAnnot annot, Widget widget) // field name if (!string.IsNullOrEmpty(widget.InsertFieldName)) { - var oldName = mupdf.mupdf.pdf_load_field_name(annotObj); + var oldName = mupdf.mupdf.pdf_load_field_name2(annotObj); if (widget.InsertFieldName != oldName) PdfDictPutTextString(annotObj, "T", widget.InsertFieldName); } diff --git a/MuPDF.NET/LegacyApiShims.cs b/MuPDF.NET/LegacyApiShims.cs index f49b88c..141addd 100644 --- a/MuPDF.NET/LegacyApiShims.cs +++ b/MuPDF.NET/LegacyApiShims.cs @@ -63,10 +63,11 @@ public Rect OtherBox(string boxtype) var page = _pdf_page(required: false); if (page?.m_internal == null) return null; - var obj = mupdf.mupdf.pdf_dict_gets(page.obj(), boxtype); + using var pageObj = page.obj(); + using var obj = mupdf.mupdf.pdf_dict_gets(pageObj, boxtype); if (obj?.m_internal == null || mupdf.mupdf.pdf_is_array(obj) == 0) return null; - var r = mupdf.mupdf.pdf_to_rect(obj); + using var r = mupdf.mupdf.pdf_to_rect(obj); return new Rect(r.x0, r.y0, r.x1, r.y1); } diff --git a/MuPDF.NET/Page.cs b/MuPDF.NET/Page.cs index 7733645..07321cc 100644 --- a/MuPDF.NET/Page.cs +++ b/MuPDF.NET/Page.cs @@ -329,8 +329,9 @@ public int Number { if (_pageNumber >= 0) return _pageNumber; + using var pageObj = NativePdfPage.obj(); return mupdf.mupdf.pdf_lookup_page_number( - RequireParent().NativePdfDocument, NativePdfPage.obj()); + RequireParent().NativePdfDocument, pageObj); } } /// @@ -340,7 +341,7 @@ public Rect Rect { get { - var r = mupdf.mupdf.fz_bound_page(NativePage); + using var r = mupdf.mupdf.fz_bound_page(NativePage); return new Rect(r.x0, r.y0, r.x1, r.y1); } } @@ -477,7 +478,12 @@ public Widget FirstWidget try { var w = mupdf.mupdf.pdf_first_widget(NativePdfPage); - return w.m_internal != null ? new Widget(w, this) : null; + if (w.m_internal == null) + { + w.Dispose(); + return null; + } + return new Widget(w, this); } catch { return null; } } @@ -664,8 +670,8 @@ public Annot AddTextAnnot(Point pos, string text, string icon = "Note") var pdfPage = NativePdfPage; var fzPoint = pos.ToFzPoint(); var annot = Helpers.PdfCreateAnnot(pdfPage, mupdf.pdf_annot_type.PDF_ANNOT_TEXT); - var r0 = mupdf.mupdf.pdf_annot_rect(annot); - var r = mupdf.mupdf.fz_make_rect(fzPoint.x, fzPoint.y, fzPoint.x + (r0.x1 - r0.x0), fzPoint.y + (r0.y1 - r0.y0)); + using var r0 = mupdf.mupdf.pdf_annot_rect(annot); + using var r = mupdf.mupdf.fz_make_rect(fzPoint.x, fzPoint.y, fzPoint.x + (r0.x1 - r0.x0), fzPoint.y + (r0.y1 - r0.y0)); mupdf.mupdf.pdf_set_annot_rect(annot, r); mupdf.mupdf.pdf_set_annot_contents(annot, text); if (!string.IsNullOrEmpty(icon)) @@ -967,8 +973,8 @@ internal mupdf.PdfAnnot _add_caret_annot(object point) { // p = JM_point_from_py(point) mupdf.FzPoint p = Helpers.JM_point_from_py(point); - mupdf.FzRect r = mupdf.mupdf.pdf_annot_rect(annot); - r = new mupdf.FzRect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0); + using var r0 = mupdf.mupdf.pdf_annot_rect(annot); + using var r = new mupdf.FzRect(p.x, p.y, p.x + r0.x1 - r0.x0, p.y + r0.y1 - r0.y0); mupdf.mupdf.pdf_set_annot_rect(annot, r); } mupdf.mupdf.pdf_update_annot(annot); @@ -1127,8 +1133,8 @@ internal Annot _add_file_annot(object point, byte[] buffer_, string filename, st // raise TypeError( MSG_BAD_BUFFER) throw new ArgumentException(Constants.MSG_BAD_BUFFER); mupdf.PdfAnnot annot = Helpers.PdfCreateAnnot(page, mupdf.pdf_annot_type.PDF_ANNOT_FILE_ATTACHMENT); - mupdf.FzRect r = mupdf.mupdf.pdf_annot_rect(annot); - r = mupdf.mupdf.fz_make_rect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0); + using var r0 = mupdf.mupdf.pdf_annot_rect(annot); + using var r = mupdf.mupdf.fz_make_rect(p.x, p.y, p.x + r0.x1 - r0.x0, p.y + r0.y1 - r0.y0); mupdf.mupdf.pdf_set_annot_rect(annot, r); int flags = mupdf.mupdf.PDF_ANNOT_IS_PRINT; mupdf.mupdf.pdf_set_annot_flags(annot, flags); @@ -2031,28 +2037,24 @@ public string GetSvgImage(Matrix matrix = null, int textAsPath = 1) { // CheckParent(self) RequireParent(); - var mediabox = mupdf.mupdf.fz_bound_page(NativePage); - // ctm = JM_matrix_from_py(matrix) - var ctm = Helpers.MatrixToFz(matrix); - var tbounds = mediabox; + using var mediabox = mupdf.mupdf.fz_bound_page(NativePage); + using var ctm = Helpers.MatrixToFz(matrix); int text_option = textAsPath == 1 ? mupdf.mupdf.FZ_SVG_TEXT_AS_PATH : mupdf.mupdf.FZ_SVG_TEXT_AS_TEXT; - tbounds = mupdf.mupdf.fz_transform_rect(tbounds, ctm); + using var tbounds = mupdf.mupdf.fz_transform_rect(mediabox, ctm); - var res = mupdf.mupdf.fz_new_buffer(1024); - var output = new mupdf.FzOutput(res); - var dev = mupdf.mupdf.fz_new_svg_device( + using var res = mupdf.mupdf.fz_new_buffer(1024); + using var output = new mupdf.FzOutput(res); + using var cookie = new mupdf.FzCookie(); + using var dev = mupdf.mupdf.fz_new_svg_device( output, tbounds.x1 - tbounds.x0, // width tbounds.y1 - tbounds.y0, // height text_option, 1); - mupdf.mupdf.fz_run_page(NativePage, dev, ctm, new mupdf.FzCookie()); + mupdf.mupdf.fz_run_page(NativePage, dev, ctm, cookie); mupdf.mupdf.fz_close_device(dev); - // out.fz_close_output() output.fz_close_output(); - // text = JM_EscapeStrFromBuffer(res) - string text = Helpers.JmEscapeStrFromBuffer(res); - return text; + return Helpers.JmEscapeStrFromBuffer(res); } // ─── Text Extraction ──────────────────────────────────────────── @@ -2091,20 +2093,27 @@ private mupdf.FzStextPage CreateStextPage(Rect clip, int flags, Matrix matrix) rect = new mupdf.FzRect(mupdf.FzRect.Fixed.Fixed_INFINITE); else rect = clip.ToFzRect(); - using var ctm = matrix.ToFzMatrix(); - using var cookie = new mupdf.FzCookie(); - var stPage = new mupdf.FzStextPage(rect); - var dev = stPage.fz_new_stext_device(options); try { - mupdf.mupdf.fz_run_page(page, dev, ctm, cookie); - mupdf.mupdf.fz_close_device(dev); + using var ctm = matrix.ToFzMatrix(); + using var cookie = new mupdf.FzCookie(); + var stPage = new mupdf.FzStextPage(rect); + var dev = stPage.fz_new_stext_device(options); + try + { + mupdf.mupdf.fz_run_page(page, dev, ctm, cookie); + mupdf.mupdf.fz_close_device(dev); + } + finally + { + dev?.Dispose(); + } + return stPage; } finally { - dev?.Dispose(); + rect?.Dispose(); } - return stPage; } private TextPage BuildTextPage(Rect clip, int flags, Matrix matrix) @@ -4892,10 +4901,15 @@ internal List> GetTextTraceDict() var page = NativePage; var rc = new List>(); var dev = new JM_new_texttrace_device(rc); - var prect = mupdf.mupdf.fz_bound_page(page); + using var prect = mupdf.mupdf.fz_bound_page(page); dev.ptm = new mupdf.FzMatrix(1, 0, 0, -1, 0, prect.y1); - mupdf.mupdf.fz_run_page(page, dev, new mupdf.FzMatrix(), new mupdf.FzCookie()); - mupdf.mupdf.fz_close_device(dev); + using (dev) + { + using var ctm = new mupdf.FzMatrix(); + using var cookie = new mupdf.FzCookie(); + mupdf.mupdf.fz_run_page(page, dev, ctm, cookie); + mupdf.mupdf.fz_close_device(dev); + } if (old_rotation != 0) SetRotation(old_rotation); return rc; @@ -5353,7 +5367,8 @@ private Rect GetMediaBox() var pdfPage = NativePdfPage; if (pdfPage?.m_internal != null) { - var r = Helpers.JmMediabox(pdfPage.obj()); + using var pageObj = pdfPage.obj(); + using var r = Helpers.JmMediabox(pageObj); return new Rect(r.x0, r.y0, r.x1, r.y1); } } @@ -5368,7 +5383,8 @@ private Rect GetCropBox() var pdfPage = NativePdfPage; if (pdfPage?.m_internal != null) { - var r = Helpers.JmCropbox(pdfPage.obj()); + using var pageObj = pdfPage.obj(); + using var r = Helpers.JmCropbox(pageObj); return new Rect(r.x0, r.y0, r.x1, r.y1); } } @@ -5381,10 +5397,11 @@ private Rect GetSpecialBox(string name) try { var pdfPage = NativePdfPage; - var box = mupdf.mupdf.pdf_dict_gets(pdfPage.obj(), name); + using var pageObj = pdfPage.obj(); + using var box = mupdf.mupdf.pdf_dict_gets(pageObj, name); if (box.m_internal != null) { - var r = mupdf.mupdf.pdf_to_rect(box); + using var r = mupdf.mupdf.pdf_to_rect(box); var mb = MediaBox; return new Rect(r.x0, mb.Y1 - r.y1, r.x1, mb.Y1 - r.y0); } @@ -6165,11 +6182,13 @@ internal void replace_image(int xref, string filename = null, Pixmap pixmap = nu rc = new List>(); dev = new JM_new_lineart_device_Device(rc, clips, method); } - var prect = mupdf.mupdf.fz_bound_page(page); + using var prect = mupdf.mupdf.fz_bound_page(page); dev.ptm = new mupdf.FzMatrix(1, 0, 0, -1, 0, prect.y1); using (dev) { - mupdf.mupdf.fz_run_page(page, dev, new mupdf.FzMatrix(), new mupdf.FzCookie()); + using var ctm = new mupdf.FzMatrix(); + using var cookie = new mupdf.FzCookie(); + mupdf.mupdf.fz_run_page(page, dev, ctm, cookie); mupdf.mupdf.fz_close_device(dev); } if (oldRotation != 0) diff --git a/MuPDF.NET/TextPage.cs b/MuPDF.NET/TextPage.cs index b668259..19a1211 100644 --- a/MuPDF.NET/TextPage.cs +++ b/MuPDF.NET/TextPage.cs @@ -20,7 +20,7 @@ public partial class TextPage : IDisposable { private mupdf.FzStextPage _nativeStp; private bool _disposed; - /// Cached block views; native memory is owned by . + /// Cached C++ block wrappers; in-page stext is owned by . private List _stextBlocks; internal Page Parent { get; set; } public bool ThisOwn { get; set; } = true; @@ -679,8 +679,9 @@ public string ExtractSelection(object pointa, object pointb) // ─── Internal: Faithful port of extra.i _as_dict ──────────────── /// - /// Stable block views for the lifetime of this text page (MuPDF.NET Blocks pattern). + /// Stable C++ block wrappers for the lifetime of this text page (MuPDF.NET Blocks pattern). /// Walk via first_block/next; do not use iterator __ref__() (owning wrappers). + /// Disposed in . /// private IReadOnlyList StextBlocks { @@ -1775,7 +1776,12 @@ public void Dispose() { if (!_disposed) { - _stextBlocks = null; + if (_stextBlocks != null) + { + foreach (var b in _stextBlocks) + b?.Dispose(); + _stextBlocks = null; + } if (ThisOwn && _nativeStp != null) { _nativeStp.Dispose(); diff --git a/MuPDF.NET/Tools.cs b/MuPDF.NET/Tools.cs index ee943e8..a6f9a11 100644 --- a/MuPDF.NET/Tools.cs +++ b/MuPDF.NET/Tools.cs @@ -5,15 +5,12 @@ namespace MuPDF.NET { /// + /// Global MuPDF runtime helpers (anti-aliasing, caches, warnings, page contents). /// /// - /// MuPDF uses @staticmethod so no instance is required. Public members use PascalCase; - /// internal snake_case aliases are available for same-assembly tests. JM_* helpers live on . - /// Legacy MuPDF.NET readthedocs listed some of these under : - /// , - /// , - /// . - /// The forwards are kept for backward compatibility. + /// All members are static. Some of these used to be documented on + /// (, , ); + /// those forwards remain for compatibility. Prefer for new code. /// public static class Tools { @@ -22,16 +19,15 @@ public static class Tools /// Generates a unique annotation/object ID. public static int GenId() { - // global TOOLS_JM_UNIQUE_ID - // TOOLS_JM_UNIQUE_ID += 1 return Interlocked.Increment(ref _uniqueId); } - /// Adds bytes as a new /Contents stream and returns the new stream xref. - /// Python docstring: Add bytes as a new /Contents object for a page, and return its xref. + /// + /// Adds bytes as a new page /Contents object and returns its xref. + /// /// Target PDF page. /// Raw PDF content bytes. - /// If , append; otherwise prepend (same as argument). + /// If , append; otherwise prepend. public static int InsertContents(Page page, ReadOnlySpan newContent, bool overlay = true) { if (page == null) throw new ArgumentNullException(nameof(page)); @@ -81,13 +77,10 @@ public static int InsertContents(Page page, string utf8Content, bool overlay = t public static byte[] GetAllContents(Page page) { if (page == null) throw new ArgumentNullException(nameof(page)); - // page = _as_pdf_page(page.this) var pdfPage = Helpers.AsPdfPage(page, required: true); - // res = JM_read_contents(page.obj()) var res = Helpers.JM_read_contents(pdfPage.obj()); try { - // result = JM_BinFromBuffer(res) return Helpers.BinFromBuffer(res); } finally @@ -96,17 +89,16 @@ public static byte[] GetAllContents(Page page) } } - /// Purges the MuPDF glyph cache. - /// Python docstring: Empty the glyph cache. + /// Empties the MuPDF glyph cache. public static void GlyphCacheEmpty() => mupdf.mupdf.fz_purge_glyph_cache(); - /// Returns the linked MuPDF library version string. - /// Python docstring: Get version of MuPDF binary build. + /// Returns the version of the linked MuPDF native library. public static string MupdfVersion() => mupdf.mupdf.FZ_VERSION; /// - /// Get MuPDF warnings/errors with optional reset . + /// Returns accumulated MuPDF warnings and errors. /// + /// If , clear the stored list after reading. public static string MupdfWarnings(bool reset = true) { Helpers.EnsureMupdfWarningsHooked(); @@ -122,29 +114,44 @@ public static string MupdfWarnings(bool reset = true) /// Clear the stored MuPDF warning list. public static void ResetMupdfWarnings() { - // global JM_mupdf_warnings_store lock (Helpers.JM_mupdf_warnings_store) Helpers.JM_mupdf_warnings_store.Clear(); } - /// Sets the anti-aliasing level. - /// Python docstring: Set anti-aliasing level. + /// + /// Sets the number of anti-aliasing bits used when rendering graphics and text (0–8). + /// The value stays in effect until changed again. Used by . + /// + /// Anti-aliasing bits. Values outside 0–8 are clamped by MuPDF. public static void SetAaLevel(int level) => mupdf.mupdf.fz_set_aa_level(level); - /// Sets the minimum graphics line width. - /// Python docstring: Set the graphics minimum line width. + /// + /// Sets the minimum stroked line width in pixels when rendering graphics. + /// Hairlines thinner than this are drawn at least this wide. Used by . + /// + /// Minimum stroke width in pixels (0 = no minimum). public static void SetGraphicsMinLineWidth(float minLineWidth) => mupdf.mupdf.fz_set_graphics_min_line_width(minLineWidth); - /// Returns current anti-aliasing and minimum line-width settings. - /// Python docstring: Show anti-aliasing values. + /// + /// Returns the current anti-aliasing levels and graphics minimum line width. + /// + /// + /// Graphics AA bits, text AA bits, and minimum stroke width in pixels. + /// Typical defaults are graphics=8, text=8, graphicsMinLineWidth=0. + /// public static (int graphics, int text, float graphicsMinLineWidth) ShowAaLevel() => ( mupdf.mupdf.fz_graphics_aa_level(), mupdf.mupdf.fz_text_aa_level(), mupdf.mupdf.fz_graphics_min_line_width()); - /// Shrinks or empties the MuPDF resource store. - /// Python docstring: Free 'percent' of current store size. + /// + /// Frees a percentage of the current MuPDF resource-store size. + /// + /// + /// 0 does nothing. 1–99 shrinks the store. 100 or more empties it. + /// Least-recently-used items are removed first. + /// public static void StoreShrink(int percent) { if (percent >= 100) @@ -155,10 +162,13 @@ public static void StoreShrink(int percent) { mupdf.mupdf.fz_shrink_store((uint)(100 - percent)); } - // fixme: return gctx->store->size. } - /// Set or query small glyph heights mode. + /// + /// Sets or queries whether text search/extract uses smaller glyph bbox heights. + /// + /// New value, or to only query. + /// The current setting. public static bool SetSmallGlyphHeights(bool? on = null) { if (on != null) diff --git a/MuPDF.NET/Widget.cs b/MuPDF.NET/Widget.cs index 2a1948d..d8aef8d 100644 --- a/MuPDF.NET/Widget.cs +++ b/MuPDF.NET/Widget.cs @@ -19,6 +19,7 @@ public class Widget : IDisposable private mupdf.PdfAnnot _nativeWidget; private bool _disposed; private bool _insertMode; + private int _xref; public Page Parent { get; internal set; } internal Annot BoundAnnot { get; private set; } @@ -135,8 +136,10 @@ public string FieldName { if (_insertMode) return InsertFieldName ?? ""; - var name = mupdf.mupdf.pdf_load_field_name(mupdf.mupdf.pdf_annot_obj(_nativeWidget)); - return name ?? ""; + // pdf_load_field_name returns a C char* that SWIG never frees. + // pdf_load_field_name2 copies into std::string so the native buffer is released. + using var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + return mupdf.mupdf.pdf_load_field_name2(annotObj) ?? ""; } set { @@ -154,7 +157,8 @@ public string FieldLabel { if (_insertMode) return InsertFieldLabel ?? ""; - return GetInheritableLabel(mupdf.mupdf.pdf_annot_obj(_nativeWidget)) ?? ""; + using var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + return GetInheritableLabel(annotObj) ?? ""; } set { @@ -178,7 +182,8 @@ public string FieldValue { if (_insertMode) return InsertFieldValue ?? ""; - return mupdf.mupdf.pdf_field_value(mupdf.mupdf.pdf_annot_obj(_nativeWidget)) ?? ""; + using var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + return mupdf.mupdf.pdf_field_value(annotObj) ?? ""; } set => SetFieldValue(value); } @@ -204,15 +209,19 @@ public List ChoiceValues if (_insertMode) return InsertChoiceValues ?? new List(); var result = new List(); - var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); - var opt = mupdf.mupdf.pdf_dict_get_inheritable(obj, mupdf.mupdf.pdf_new_name("Opt")); + using var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var optKey = mupdf.mupdf.pdf_new_name("Opt"); + using var opt = mupdf.mupdf.pdf_dict_get_inheritable(obj, optKey); if (opt.m_internal == null) return result; int n = mupdf.mupdf.pdf_array_len(opt); for (int i = 0; i < n; i++) { - var item = mupdf.mupdf.pdf_array_get(opt, i); + using var item = mupdf.mupdf.pdf_array_get(opt, i); if (mupdf.mupdf.pdf_is_array(item) != 0) - result.Add(mupdf.mupdf.pdf_to_text_string(mupdf.mupdf.pdf_array_get(item, 1))); + { + using var nested = mupdf.mupdf.pdf_array_get(item, 1); + result.Add(mupdf.mupdf.pdf_to_text_string(nested)); + } else result.Add(mupdf.mupdf.pdf_to_text_string(item)); } @@ -254,7 +263,7 @@ public Rect Rect { if (_insertMode) return InsertRect; - var r = mupdf.mupdf.pdf_bound_annot(_nativeWidget); + using var r = mupdf.mupdf.pdf_bound_annot(_nativeWidget); return new Rect(r.x0, r.y0, r.x1, r.y1); } set @@ -285,7 +294,8 @@ public int FieldFlags { if (_insertMode) return InsertFieldFlags ?? 0; - return mupdf.mupdf.pdf_field_flags(mupdf.mupdf.pdf_annot_obj(_nativeWidget)); + using var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + return mupdf.mupdf.pdf_field_flags(annotObj); } set => InsertFieldFlags = value; } @@ -396,15 +406,15 @@ public string FieldDefault { if (_insertMode) return ""; - var dv = mupdf.mupdf.pdf_dict_get_text_string(mupdf.mupdf.pdf_annot_obj(_nativeWidget), - mupdf.mupdf.pdf_new_name("DV")); + using var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var dvKey = mupdf.mupdf.pdf_new_name("DV"); + var dv = mupdf.mupdf.pdf_dict_get_text_string(annotObj, dvKey); return dv ?? ""; } } /// PDF object xref of this widget. - public int Xref => - _insertMode ? 0 : mupdf.mupdf.pdf_to_num(mupdf.mupdf.pdf_annot_obj(_nativeWidget)); + public int Xref => _insertMode ? 0 : _xref; /// Check if field is read only. public bool IsReadOnly => (FieldFlags & 1) != 0; @@ -419,8 +429,9 @@ public int MaxLen { if (_insertMode) return InsertTextMaxLen; - return mupdf.mupdf.pdf_dict_get_int(mupdf.mupdf.pdf_annot_obj(_nativeWidget), - mupdf.mupdf.pdf_new_name("MaxLen")); + using var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var key = mupdf.mupdf.pdf_new_name("MaxLen"); + return mupdf.mupdf.pdf_dict_get_int(annotObj, key); } set { @@ -467,8 +478,9 @@ public bool IsSigned { if (_insertMode || FieldType != (int)WidgetType.Signature) return false; - var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); - var v = mupdf.mupdf.pdf_dict_get(obj, mupdf.mupdf.pdf_new_name("V")); + using var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var vKey = mupdf.mupdf.pdf_new_name("V"); + using var v = mupdf.mupdf.pdf_dict_get(obj, vKey); return v.m_internal != null && mupdf.mupdf.pdf_is_null(v) == 0; } } @@ -481,7 +493,12 @@ public Widget Next if (_insertMode) return null; var next = mupdf.mupdf.pdf_next_widget(_nativeWidget); - return next.m_internal != null ? new Widget(next, Parent) : null; + if (next.m_internal == null) + { + next.Dispose(); + return null; + } + return new Widget(next, Parent); } } @@ -572,7 +589,7 @@ public void Update(bool syncFlags = false) InsertFieldName = FieldName; if (InsertRect.IsEmpty || InsertRect.IsInfinite) { - var r = mupdf.mupdf.pdf_bound_annot(_nativeWidget); + using var r = mupdf.mupdf.pdf_bound_annot(_nativeWidget); InsertRect = new Rect(r.x0, r.y0, r.x1, r.y1); } if (InsertFieldType == WidgetType.RadioButton @@ -674,33 +691,37 @@ public bool SyncFlags() if (doc == null) return false; var pdf = doc.NativePdfDocument; - var pdfWidget = mupdf.mupdf.pdf_load_object(pdf, Xref); - var parentObj = mupdf.mupdf.pdf_dict_get(pdfWidget, mupdf.mupdf.pdf_new_name("Parent")); + using var pdfWidget = mupdf.mupdf.pdf_load_object(pdf, Xref); + using var parentName = mupdf.mupdf.pdf_new_name("Parent"); + using var parentObj = mupdf.mupdf.pdf_dict_get(pdfWidget, parentName); if (mupdf.mupdf.pdf_is_dict(parentObj) == 0) return false; int flags = FieldFlags; - mupdf.mupdf.pdf_dict_put_int(parentObj, mupdf.mupdf.pdf_new_name("Ff"), flags); + using var ffName = mupdf.mupdf.pdf_new_name("Ff"); + mupdf.mupdf.pdf_dict_put_int(parentObj, ffName, flags); - var kids = mupdf.mupdf.pdf_dict_get(parentObj, mupdf.mupdf.pdf_new_name("Kids")); + using var kidsName = mupdf.mupdf.pdf_new_name("Kids"); + using var kids = mupdf.mupdf.pdf_dict_get(parentObj, kidsName); if (mupdf.mupdf.pdf_is_array(kids) == 0) { Helpers.message("warning: malformed PDF, Parent has no Kids array"); return false; } int n = mupdf.mupdf.pdf_array_len(kids); + using var subtypeName = mupdf.mupdf.pdf_new_name("Subtype"); for (int i = 0; i < n; i++) { - var kid = mupdf.mupdf.pdf_array_get(kids, i); + using var kid = mupdf.mupdf.pdf_array_get(kids, i); if (mupdf.mupdf.pdf_is_dict(kid) == 0) continue; int kidXref = mupdf.mupdf.pdf_to_num(kid); if (kidXref == Xref) continue; - var subtype = mupdf.mupdf.pdf_dict_get(kid, mupdf.mupdf.pdf_new_name("Subtype")); + using var subtype = mupdf.mupdf.pdf_dict_get(kid, subtypeName); if (mupdf.mupdf.pdf_to_name(subtype) != "Widget") continue; - mupdf.mupdf.pdf_dict_put_int(kid, mupdf.mupdf.pdf_new_name("Ff"), flags); + mupdf.mupdf.pdf_dict_put_int(kid, ffName, flags); } return true; } @@ -769,7 +790,8 @@ public string OnState() if (!_insertMode && _nativeWidget?.m_internal != null) { - var onstate = mupdf.mupdf.pdf_button_field_on_state(mupdf.mupdf.pdf_annot_obj(_nativeWidget)); + using var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var onstate = mupdf.mupdf.pdf_button_field_on_state(annotObj); if (onstate.m_internal != null) { string name = mupdf.mupdf.pdf_to_name(onstate); @@ -835,12 +857,14 @@ private static List ParseAppearanceStateNames(string pdfObject) public void Reset() { // TOOLS._reset_widget(self._annot) - var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); - var dv = mupdf.mupdf.pdf_dict_get(obj, mupdf.mupdf.pdf_new_name("DV")); + using var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var dvKey = mupdf.mupdf.pdf_new_name("DV"); + using var dv = mupdf.mupdf.pdf_dict_get(obj, dvKey); + using var vKey = mupdf.mupdf.pdf_new_name("V"); if (dv.m_internal != null) - mupdf.mupdf.pdf_dict_put(obj, mupdf.mupdf.pdf_new_name("V"), dv); + mupdf.mupdf.pdf_dict_put(obj, vKey, dv); else - mupdf.mupdf.pdf_dict_del(obj, mupdf.mupdf.pdf_new_name("V")); + mupdf.mupdf.pdf_dict_del(obj, vKey); mupdf.mupdf.pdf_update_annot(_nativeWidget); } @@ -855,17 +879,19 @@ public Pixmap GetPixmap(Matrix matrix = null, Colorspace cs = null, bool alpha = private string GetTopLevelScript() { - var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); - var action = mupdf.mupdf.pdf_dict_get(obj, mupdf.mupdf.pdf_new_name("A")); + using var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var key = mupdf.mupdf.pdf_new_name("A"); + using var action = mupdf.mupdf.pdf_dict_get(obj, key); return Helpers.JmGetScript(action); } private string GetScript(string trigger) { - var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); - var aa = mupdf.mupdf.pdf_dict_get(obj, mupdf.mupdf.pdf_new_name("AA")); + using var obj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var aaKey = mupdf.mupdf.pdf_new_name("AA"); + using var aa = mupdf.mupdf.pdf_dict_get(obj, aaKey); if (aa.m_internal == null) return null; - var action = mupdf.mupdf.pdf_dict_gets(aa, trigger); + using var action = mupdf.mupdf.pdf_dict_gets(aa, trigger); return Helpers.JmGetScript(action); } @@ -874,31 +900,41 @@ internal void SyncFromNative() { if (_nativeWidget?.m_internal == null) return; - var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + _xref = mupdf.mupdf.pdf_to_num(annotObj); InsertFieldType = (WidgetType)FieldType; - var r = mupdf.mupdf.pdf_bound_annot(_nativeWidget); + using var r = mupdf.mupdf.pdf_bound_annot(_nativeWidget); InsertRect = new Rect(r.x0, r.y0, r.x1, r.y1); InsertFieldName = FieldName; InsertFieldLabel = FieldLabel; InsertFieldValue = FieldValue; InsertFieldFlags = FieldFlags; InsertBorderStyle = mupdf.mupdf.pdf_field_border_style(annotObj) ?? "S"; - InsertBorderWidth = mupdf.mupdf.pdf_to_real( - Helpers.PdfDictGetl(annotObj, mupdf.mupdf.pdf_new_name("BS"), mupdf.mupdf.pdf_new_name("W"))); - if (InsertBorderWidth == 0) - InsertBorderWidth = 1; + using (var widthObj = mupdf.mupdf.pdf_dict_getp(annotObj, "BS/W")) + { + InsertBorderWidth = mupdf.mupdf.pdf_to_real(widthObj); + if (InsertBorderWidth == 0) + InsertBorderWidth = 1; + } - var dashObj = Helpers.PdfDictGetl(annotObj, mupdf.mupdf.pdf_new_name("BS"), mupdf.mupdf.pdf_new_name("D")); - if (dashObj.m_internal != null && mupdf.mupdf.pdf_is_array(dashObj) != 0) + using (var dashObj = mupdf.mupdf.pdf_dict_getp(annotObj, "BS/D")) { - int n = mupdf.mupdf.pdf_array_len(dashObj); - InsertBorderDashes = new List(n); - for (int i = 0; i < n; i++) - InsertBorderDashes.Add(mupdf.mupdf.pdf_to_int(mupdf.mupdf.pdf_array_get(dashObj, i))); + if (dashObj.m_internal != null && mupdf.mupdf.pdf_is_array(dashObj) != 0) + { + int n = mupdf.mupdf.pdf_array_len(dashObj); + InsertBorderDashes = new List(n); + for (int i = 0; i < n; i++) + { + using var item = mupdf.mupdf.pdf_array_get(dashObj, i); + InsertBorderDashes.Add(mupdf.mupdf.pdf_to_int(item)); + } + } } - InsertFillColor = ReadColorArray(Helpers.PdfDictGetl(annotObj, mupdf.mupdf.pdf_new_name("MK"), mupdf.mupdf.pdf_new_name("BG"))); - InsertBorderColor = ReadColorArray(Helpers.PdfDictGetl(annotObj, mupdf.mupdf.pdf_new_name("MK"), mupdf.mupdf.pdf_new_name("BC"))); + using (var bg = mupdf.mupdf.pdf_dict_getp(annotObj, "MK/BG")) + InsertFillColor = ReadColorArray(bg); + using (var bc = mupdf.mupdf.pdf_dict_getp(annotObj, "MK/BC")) + InsertBorderColor = ReadColorArray(bc); InsertChoiceValues = new List(ChoiceValues); InsertTextMaxLen = MaxLen; InsertScript = GetTopLevelScript(); @@ -909,10 +945,12 @@ internal void SyncFromNative() InsertScriptBlur = GetScript("Bl"); InsertScriptFocus = GetScript("Fo"); - var da = mupdf.mupdf.pdf_to_text_string( - mupdf.mupdf.pdf_dict_get_inheritable(annotObj, mupdf.mupdf.pdf_new_name("DA"))) ?? ""; - InsertTextDa = da; - ParseDa(da); + using (var daKey = mupdf.mupdf.pdf_new_name("DA")) + using (var daObj = mupdf.mupdf.pdf_dict_get_inheritable(annotObj, daKey)) + { + InsertTextDa = mupdf.mupdf.pdf_to_text_string(daObj) ?? ""; + ParseDa(InsertTextDa); + } } private static List ToFloatList(IList value) @@ -936,36 +974,44 @@ private static List ReadColorArray(mupdf.PdfObj obj) int n = mupdf.mupdf.pdf_array_len(obj); var col = new List(n); for (int i = 0; i < n; i++) - col.Add((float)mupdf.mupdf.pdf_to_real(mupdf.mupdf.pdf_array_get(obj, i))); + { + using var item = mupdf.mupdf.pdf_array_get(obj, i); + col.Add((float)mupdf.mupdf.pdf_to_real(item)); + } return col; } - private static string GetInheritableLabel(mupdf.PdfObj node) + private static string GetInheritableLabel(mupdf.PdfObj start) { - var tu = mupdf.mupdf.pdf_new_name("TU"); - var parent = mupdf.mupdf.pdf_new_name("Parent"); - var slow = node; - int halfbeat = 11; - while (node.m_internal != null) + using var tu = mupdf.mupdf.pdf_new_name("TU"); + using var parent = mupdf.mupdf.pdf_new_name("Parent"); + mupdf.PdfObj node = start; + mupdf.PdfObj owned = null; + try { - var val = mupdf.mupdf.pdf_dict_get(node, tu); - if (val.m_internal != null) + int depth = 0; + while (node.m_internal != null && depth++ < 32) { - var label = mupdf.mupdf.pdf_to_text_string(val); - if (!string.IsNullOrEmpty(label)) - return label; - } - node = mupdf.mupdf.pdf_dict_get(node, parent); - if (node.m_internal == slow.m_internal) - break; - halfbeat--; - if (halfbeat == 0) - { - slow = mupdf.mupdf.pdf_dict_get(slow, parent); - halfbeat = 2; + using (var val = mupdf.mupdf.pdf_dict_get(node, tu)) + { + if (val.m_internal != null) + { + var label = mupdf.mupdf.pdf_to_text_string(val); + if (!string.IsNullOrEmpty(label)) + return label; + } + } + var next = mupdf.mupdf.pdf_dict_get(node, parent); + owned?.Dispose(); + owned = next; + node = next; } + return null; + } + finally + { + owned?.Dispose(); } - return null; } /// @@ -976,7 +1022,7 @@ private void TurnOffSiblingRadioButtons() if (Parent?.Parent == null) return; var doc = Parent.Parent; - var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); + using var annotObj = mupdf.mupdf.pdf_annot_obj(_nativeWidget); var (_, kidsValue) = doc.XrefGetKey(Xref, "Parent/Kids"); if (kidsValue == null || !kidsValue.StartsWith("[")) return; @@ -1035,10 +1081,14 @@ private void ParseDa(string da) /// Legacy no-argument DA parser. public void ParseDa() => ParseDa(InsertTextDa); - /// Releases managed wrapper state (native object owned by the page). + /// Releases the native annot wrapper (page still owns the annot). public void Dispose() { - if (!_disposed) { _disposed = true; } + if (_disposed) + return; + _disposed = true; + _nativeWidget?.Dispose(); + _nativeWidget = null; GC.SuppressFinalize(this); } diff --git a/README.md b/README.md index 7b9e3d8..91c1c22 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Open **`MuPDF.NET.sln`** at the repository root. - [Quick start](#quick-start) - [Key capabilities](#key-capabilities) - [Code examples](#code-examples) +- [Examples](#examples) - [API overview](#api-overview) - [Supported formats](#supported-formats) - [Building from source](#building-from-source) @@ -144,6 +145,8 @@ doc.Close(); ## Code examples +Copy-paste snippets for common tasks. Full runnable sample apps (one project per feature, NuGet-only) live in **[MuPDF.NET.Examples](https://github.com/ArtifexSoftware/MuPDF.NET.Examples)**. + ### Add a text watermark ```csharp @@ -257,6 +260,23 @@ End Module --- +## Examples + +Runnable console samples for this package, plus `MuPDF.NET.PDF4LLM` and `MuPDF.NET.Office`, are in a separate repository: + +**https://github.com/ArtifexSoftware/MuPDF.NET.Examples** + +```powershell +git clone https://github.com/ArtifexSoftware/MuPDF.NET.Examples.git +cd MuPDF.NET.Examples +dotnet restore +dotnet run --project MuPDF.NET\01-OpenSave +``` + +Each sample is a small project under `MuPDF.NET/` (for example `04-TextExtractSearch`, `08-FormWidgets`). See that repo’s README for the full list and how to batch-run against golden `Expected/` files. + +--- + ## API overview The library's primary entry points are `Document` and `Page`. Most workflows follow the pattern: open → get page → operate → save → close. @@ -360,6 +380,7 @@ See the [Getting Started](https://mupdfnet.readthedocs.io/en/latest/getting-star | Getting started guide | https://mupdfnet.readthedocs.io/en/latest/getting-started/index.html | | The Basics (cookbook) | https://mupdfnet.readthedocs.io/en/latest/the-basics/index.html | | LLM/RAG companion (`MuPDF.NET.PDF4LLM`) | https://docs.pdf4llm.com/dotnet/getting-started/installation | +| Sample apps (`MuPDF.NET.Examples`) | https://github.com/ArtifexSoftware/MuPDF.NET.Examples | --- diff --git a/Versions.props b/Versions.props index 14cb60f..796a097 100644 --- a/Versions.props +++ b/Versions.props @@ -8,7 +8,7 @@ 1.28.2 - 3.28.2 + 3.28.2.3 1.28.2