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("