Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions Demo/SampleMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
307 changes: 307 additions & 0 deletions Demo/Samples/Regression/Program.Issue256.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
using System.Diagnostics;

namespace Demo
{
internal partial class Program
{
/// <summary>
/// Issue #256 — widget enumeration, TextPage.Search, GetKeyXref(AP/N),
/// and Page.MediaBox (Search then MediaBox per widget).
/// </summary>
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<string>();
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<Widget> 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<Widget> 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();
}
}

/// <summary>
/// https://github.com/ArtifexSoftware/MuPDF.NET/issues/256#issuecomment-5728502983
/// </summary>
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("<html><body>");
for (int i = 0; i < 40; i++)
html.Append("<p>the quick brown fox jumps over the lazy dog ").Append(i).Append("</p>");
html.Append("</body></html>");
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("<html><body>");
for (int i = 0; i < 40; i++)
html.Append("<p>the quick brown fox jumps over the lazy dog ").Append(i).Append("</p>");
html.Append("</body></html>");
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();
}
}
}
12 changes: 12 additions & 0 deletions MuPDF.NET.PDF4LLM/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Loading