diff --git a/.gitignore b/.gitignore index ce1e0a14..13c07e94 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ mono_crash.* # Build results docs/build/ +TestDocuments/_Output/ [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ @@ -35,10 +36,9 @@ bld/ # Test-generated output (PDFs, markdown, etc.) TestDocuments/MuPDF.NET.Test/_Output/ -TestDocuments/PDF4LLM.Test/_Output/ +TestDocuments/MuPDF.NET.PDF4LLM.Test/_Output/ TestDocuments/Demo/_Output/ - # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot @@ -47,9 +47,6 @@ TestDocuments/Demo/_Output/ # Visual Studio 2017 auto generated files Generated\ Files/ -# MuPDF.NativeAssets generation files -MuPDF/packages - # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj index e1442339..1fbbb270 100644 --- a/Demo/Demo.csproj +++ b/Demo/Demo.csproj @@ -1,16 +1,20 @@ - + Exe net8.0 AnyCPU;x64;x86 - $(NoWarn);0618;CA1416 + enable + annotations + $(NoWarn);0618;CA1416;NU1603 - + + + diff --git a/Demo/GlobalUsings.cs b/Demo/GlobalUsings.cs index aef79c76..b5580c1e 100644 --- a/Demo/GlobalUsings.cs +++ b/Demo/GlobalUsings.cs @@ -7,11 +7,6 @@ global using System.Threading.Tasks; global using mupdf; global using MuPDF.NET; -global using PDF4LLM; -global using static global::PDF4LLM.PdfExtractor; -global using PDF4LLM.Helpers; -global using PDF4LLM.Llama; -global using PDF4LLM.Ocr; global using SkiaSharp; global using Box = MuPDF.NET.Box; global using Encoding = System.Text.Encoding; @@ -19,4 +14,8 @@ global using Font = MuPDF.NET.Font; global using Morph = MuPDF.NET.Morph; global using TextWriter = MuPDF.NET.TextWriter; +global using MuPDF.NET.PDF4LLM; +global using MuPDF.NET.PDF4LLM.Helpers; +global using MuPDF.NET.PDF4LLM.Llama; +global using MuPDF.NET.PDF4LLM.Ocr; global using Utils = MuPDF.NET.Utils; diff --git a/Demo/Program.cs b/Demo/Program.cs index 22125346..2280bf0d 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -1,4 +1,10 @@ -namespace Demo +using System; +using System.IO; +using System.Linq; +using MuPDF.NET; +using mupdf; + +namespace Demo { /// /// Demo entry point. With no arguments, every sample in runs (including [diag]). diff --git a/Demo/README.md b/Demo/README.md index 823d4c3b..25d25108 100644 --- a/Demo/README.md +++ b/Demo/README.md @@ -1,6 +1,6 @@ # MuPDF.NET Demo -Runnable samples for **MuPDF.NET** and **PDF4LLM**, grouped by API area under `Samples/`. +Runnable samples for **MuPDF.NET** and **MuPDF.NET.PDF4LLM**, grouped by API area under `Samples/`. ## Run @@ -11,7 +11,8 @@ dotnet run # all samples in SampleMenu.cs (default, includes dotnet run -- user # user-facing samples only (skips [diag]) dotnet run -- diagnostics # [diag] samples only dotnet run -- help # list all samples -dotnet run -- hello-new-pdf # one sample by name +dotnet run -- hello-new-pdf # one MuPDF.NET sample by name +dotnet run -- rag-markdown # one MuPDF.NET.PDF4LLM sample by name ``` ## Layout @@ -24,11 +25,7 @@ dotnet run -- hello-new-pdf # one sample by name | `Samples/PageContent/` | Images, recolor, widgets, OCR | | `Samples/ImageFilters/` | Skia image filters | | `Samples/Barcodes/` | Barcode read/write | -| `Samples/Llm/` | PDF4LLM markdown, tables, AI connector | | `Samples/Regression/` | Issue repros (diagnostics only) | +| `Samples/Llm/` | MuPDF.NET.PDF4LLM: Markdown, tables, OCR, RAG | -Input PDFs and images live in `TestDocuments/Demo/`. Generated PDFs are written to `TestDocuments/Demo/_Output/` (gitignored). - -## PDF4LLM AI sample - -The `ai-connector` sample uses `PdfExtractor.LoadAiAsync`. Set `AZURE_OPENAI_ENDPOINT` and related variables for Azure OpenAI, or run without them to use the in-memory demo pipeline. +Input PDFs and images live in `TestDocuments/Demo/` (and `TestDocuments/Demo/Llm/`). Generated PDFs are written to `TestDocuments/Demo/_Output/` (gitignored). diff --git a/Demo/SampleMenu.cs b/Demo/SampleMenu.cs index 83c8c442..539556f4 100644 --- a/Demo/SampleMenu.cs +++ b/Demo/SampleMenu.cs @@ -1,7 +1,7 @@ namespace Demo { /// - /// Demo samples grouped by MuPDF.NET / PDF4LLM feature areas. + /// Demo samples grouped by MuPDF.NET feature areas. /// Default run executes every entry in (including [diag] samples). /// Use user to skip diagnostics, or diagnostics to run only [diag] samples. /// @@ -61,23 +61,6 @@ private sealed record Sample( new("Barcodes", "write-barcode", "Write many barcode types to PDF and PNG", a => Program.TestWriteBarcode(a)), new("Barcodes", "write-barcode1", "Write CODE39/CODE128/DM with Units rects", _ => Program.TestWriteBarcode1()), - // —— PDF4LLM —— Samples/Llm - new("PDF4LLM", "rag-markdown", "PDF to Markdown (Magazine.pdf)", _ => Program.TestMuPdfRagToMarkdown()), - new("PDF4LLM", "table", "Detect tables and export markdown", _ => Program.TestTable()), - new("PDF4LLM", "table-extract-1", "Dump detected tables by page to console", _ => Program.TestTableExtract1()), - new("PDF4LLM", "table-extract-2", "Export detected tables to tables.csv", _ => Program.TestTableExtract2()), - new("PDF4LLM", "table-extract-3", "Merge continued table pages by column count", _ => Program.TestTableExtract3()), - new("PDF4LLM", "table-ocr", "Extract OCR text from Ocr.pdf", _ => Program.TestOcr()), - new("PDF4LLM", "llm-reader-save-pages", "Load markdown chunks and save per-page .md files", _ => Program.TestLLM2()), - new("PDF4LLM", "markdown-reader", "LlamaIndex PDFMarkdownReader", _ => Program.TestMarkdownReader()), - new("PDF4LLM", "ai-connector", "LoadAiAsync: Ask / Summarize / Search", a => Program.TestMicrosoftAiConnector(a).GetAwaiter().GetResult()), - new("PDF4LLM", "llm-to-markdown-fixture-370", "[diag] ToMarkdown vs test_370 expected output", a => Program.Test4LlmToMarkdownCompareExpected370(a), Diagnostic: true), - new("PDF4LLM", "llm-to-markdown-ocr-1", "[diag] ToMarkdown OCR fixture (FFFD)", a => Program.Test4LlmToMarkdownOcrFixture1(a), Diagnostic: true), - new("PDF4LLM", "llm-to-markdown-ocr-2", "[diag] ToMarkdown without OCR on FFFD fixture", a => Program.Test4LlmToMarkdownOcrFixture2(a), Diagnostic: true), - new("PDF4LLM", "llm-to-markdown-ocr-3", "[diag] ToMarkdown OCR on/off SVG fixture", a => Program.Test4LlmToMarkdownOcrFixture3(a), Diagnostic: true), - new("PDF4LLM", "llm-pdf-reader-empty", "[diag] PDFMarkdownReader empty page", a => Program.Test4LlmPdfMarkdownReaderEmptyPage(a), Diagnostic: true), - new("PDF4LLM", "llm-pdf-reader-missing-file", "[diag] PDFMarkdownReader missing file", a => Program.Test4LlmPdfMarkdownReaderMissingFile(a), Diagnostic: true), - // —— Regression & diagnostics —— Samples/Regression 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), @@ -85,6 +68,23 @@ private sealed record Sample( 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), + + // —— MuPDF.NET.PDF4LLM —— Samples/Llm + new("MuPDF.NET.PDF4LLM", "rag-markdown", "PDF to Markdown (Magazine.pdf)", _ => Program.TestMuPdfRagToMarkdown()), + new("MuPDF.NET.PDF4LLM", "table", "Detect tables and export markdown", _ => Program.TestTable()), + new("MuPDF.NET.PDF4LLM", "table-extract-1", "Dump detected tables by page to console", _ => Program.TestTableExtract1()), + new("MuPDF.NET.PDF4LLM", "table-extract-2", "Export detected tables to tables.csv", _ => Program.TestTableExtract2()), + new("MuPDF.NET.PDF4LLM", "table-extract-3", "Merge continued table pages by column count", _ => Program.TestTableExtract3()), + new("MuPDF.NET.PDF4LLM", "table-ocr", "Extract OCR text from Ocr.pdf", _ => Program.TestOcr()), + new("MuPDF.NET.PDF4LLM", "llm-reader-save-pages", "Load markdown chunks and save per-page .md files", _ => Program.TestLLM2()), + new("MuPDF.NET.PDF4LLM", "markdown-reader", "LlamaIndex PDFMarkdownReader", _ => Program.TestMarkdownReader()), + new("MuPDF.NET.PDF4LLM", "ai-connector", "LoadAiAsync: Ask / Summarize / Search", a => Program.TestMicrosoftAiConnector(a).GetAwaiter().GetResult()), + new("MuPDF.NET.PDF4LLM", "llm-to-markdown-fixture-370", "[diag] ToMarkdown vs test_370 expected output", a => Program.Test4LlmToMarkdownCompareExpected370(a), Diagnostic: true), + new("MuPDF.NET.PDF4LLM", "llm-to-markdown-ocr-1", "[diag] ToMarkdown OCR fixture (FFFD)", a => Program.Test4LlmToMarkdownOcrFixture1(a), Diagnostic: true), + new("MuPDF.NET.PDF4LLM", "llm-to-markdown-ocr-2", "[diag] ToMarkdown without OCR on FFFD fixture", a => Program.Test4LlmToMarkdownOcrFixture2(a), Diagnostic: true), + new("MuPDF.NET.PDF4LLM", "llm-to-markdown-ocr-3", "[diag] ToMarkdown OCR on/off SVG fixture", a => Program.Test4LlmToMarkdownOcrFixture3(a), Diagnostic: true), + new("MuPDF.NET.PDF4LLM", "llm-pdf-reader-empty", "[diag] PDFMarkdownReader empty page", a => Program.Test4LlmPdfMarkdownReaderEmptyPage(a), Diagnostic: true), + new("MuPDF.NET.PDF4LLM", "llm-pdf-reader-missing-file", "[diag] PDFMarkdownReader missing file", a => Program.Test4LlmPdfMarkdownReaderMissingFile(a), Diagnostic: true), }; private static readonly Dictionary ByName = BuildIndex(); diff --git a/Demo/Samples/Llm/Program.Llm.AiConnector.cs b/Demo/Samples/Llm/Program.Llm.AiConnector.cs index 2a45e395..337f63c1 100644 --- a/Demo/Samples/Llm/Program.Llm.AiConnector.cs +++ b/Demo/Samples/Llm/Program.Llm.AiConnector.cs @@ -1,19 +1,20 @@ using Microsoft.Extensions.AI; -using PDF4LLM.AI; -using PDF4LLM.AI.Models; -using PDF4LLM.AI.Options; +using MuPDF.NET.PDF4LLM; +using MuPDF.NET.PDF4LLM.AI; +using MuPDF.NET.PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Options; namespace Demo { /// - /// demo: index two PDFs, then Ask, Summarize, and Search. + /// demo: index two PDFs, then Ask, Summarize, and Search. /// Uses Azure OpenAI when AZURE_OPENAI_* env vars are set; otherwise an offline demo client. /// internal partial class Program { internal static async Task TestMicrosoftAiConnector(string[] args) { - Console.WriteLine("\n=== TestMicrosoftAiConnector (PDF4LLM.AI) ======================="); + Console.WriteLine("\n=== TestMicrosoftAiConnector (MuPDF.NET.PDF4LLM.AI) ======================="); string capitals = DemoPaths.Input("Llm/national-capitals.pdf"); string nato = DemoPaths.Input("Llm/nato-members.pdf"); @@ -22,7 +23,7 @@ internal static async Task TestMicrosoftAiConnector(string[] args) bool azure = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")); Console.WriteLine($"Pipeline: {(azure ? "Azure OpenAI" : "development (in-memory index + demo chat)")}"); - AiDocumentCollection aiDocs = await PdfExtractor.LoadAiAsync( + AiDocumentCollection aiDocs = await MuPDF4LLM.LoadAiAsync( new[] { capitals, nato }, options); diff --git a/Demo/Samples/Llm/Program.Llm.PdfMarkdownReader.Fixtures.cs b/Demo/Samples/Llm/Program.Llm.PdfMarkdownReader.Fixtures.cs index 660d10be..c8216090 100644 --- a/Demo/Samples/Llm/Program.Llm.PdfMarkdownReader.Fixtures.cs +++ b/Demo/Samples/Llm/Program.Llm.PdfMarkdownReader.Fixtures.cs @@ -1,7 +1,7 @@ namespace Demo { /// - /// demos aligned with PDF4LLM / repository reader tests. + /// demos aligned with MuPDF.NET.PDF4LLM / repository reader tests. /// internal partial class Program { diff --git a/Demo/Samples/Llm/Program.Llm.TableExtract.cs b/Demo/Samples/Llm/Program.Llm.TableExtract.cs index 02e0e674..df05ae7c 100644 --- a/Demo/Samples/Llm/Program.Llm.TableExtract.cs +++ b/Demo/Samples/Llm/Program.Llm.TableExtract.cs @@ -8,8 +8,8 @@ internal partial class Program internal static void TestTableExtract1() { - PdfExtractor.UseLayout = true; - JArray pages = GetPagesFromJson(PdfExtractor.ToJson(NationalCapitalsPdf)); + MuPDF4LLM.UseLayout = true; + JArray pages = GetPagesFromJson(MuPDF4LLM.ToJson(NationalCapitalsPdf)); foreach (JObject page in pages) { @@ -35,8 +35,8 @@ internal static void TestTableExtract1() internal static void TestTableExtract2() { - PdfExtractor.UseLayout = true; - JArray pages = GetPagesFromJson(PdfExtractor.ToJson(NationalCapitalsPdf)); + MuPDF4LLM.UseLayout = true; + JArray pages = GetPagesFromJson(MuPDF4LLM.ToJson(NationalCapitalsPdf)); var csvLines = new List(); foreach (JObject page in pages) @@ -64,8 +64,8 @@ internal static void TestTableExtract2() internal static void TestTableExtract3() { - PdfExtractor.UseLayout = true; - JArray pages = GetPagesFromJson(PdfExtractor.ToJson(NationalCapitalsPdf)); + MuPDF4LLM.UseLayout = true; + JArray pages = GetPagesFromJson(MuPDF4LLM.ToJson(NationalCapitalsPdf)); var mergedRows = new List>(); int? prevColCount = null; @@ -101,25 +101,27 @@ internal static void TestTableExtract3() internal static void TestOcr() { - PdfExtractor.UseLayout = true; - string md = PdfExtractor.ToMarkdown(@"..\..\..\..\TestDocuments\Demo\Ocr.pdf", useOcr: true, writeImages: false, embedImages: false); + MuPDF4LLM.UseLayout = true; + string ocrPdf = DemoPaths.Input("Ocr.pdf"); + string md = MuPDF4LLM.ToMarkdown(ocrPdf, useOcr: true, writeImages: false, embedImages: false); Console.WriteLine(md); - string text = PdfExtractor.ToText(@"..\..\..\..\TestDocuments\Demo\Ocr.pdf", useOcr: true); + string text = MuPDF4LLM.ToText(ocrPdf, useOcr: true); Console.WriteLine(text); } internal static void TestLLM2() { - PdfExtractor.UseLayout = true; - var reader = PdfExtractor.LlamaMarkdownReader(); - var chunks = reader.LoadData(@"..\..\..\..\TestDocuments\Demo\magazine.pdf"); + MuPDF4LLM.UseLayout = true; + var reader = MuPDF4LLM.LlamaMarkdownReader(); + var chunks = reader.LoadData(DemoPaths.Input("Magazine.pdf")); - Directory.CreateDirectory("Output"); + string outDir = DemoPaths.Output("pages"); + Directory.CreateDirectory(outDir); foreach (var chunk in chunks) { int pageNum = (int)chunk.ExtraInfo["page"]; Console.WriteLine(pageNum); - string filePath = $"output/page-{pageNum}.md"; + string filePath = Path.Combine(outDir, $"page-{pageNum}.md"); File.WriteAllText(filePath, chunk.Text, Encoding.UTF8); } } diff --git a/Demo/Samples/Llm/Program.Llm.ToMarkdown.Fixtures.cs b/Demo/Samples/Llm/Program.Llm.ToMarkdown.Fixtures.cs index b655688f..7f553e1a 100644 --- a/Demo/Samples/Llm/Program.Llm.ToMarkdown.Fixtures.cs +++ b/Demo/Samples/Llm/Program.Llm.ToMarkdown.Fixtures.cs @@ -1,7 +1,7 @@ namespace Demo { /// - /// PDF4LLM demos aligned with PDF4LLM test fixtures + /// MuPDF.NET.PDF4LLM demos aligned with MuPDF.NET.PDF4LLM test fixtures /// (golden markdown, OCR behavior). PDFs live under TestDocuments/Demo/Llm/; samples skip if missing. /// internal partial class Program @@ -13,7 +13,7 @@ private static bool LlmOcrEnvironmentLikelyAvailable() => internal static void Test4LlmToMarkdownCompareExpected370(string[] args) { _ = args; - Console.WriteLine("\n=== Test4LlmToMarkdownCompareExpected370 (PDF4LLM) ======================="); + Console.WriteLine("\n=== Test4LlmToMarkdownCompareExpected370 (MuPDF.NET.PDF4LLM) ======================="); string pdfPath = DemoPaths.Input("Llm/test_370.pdf"); string expectedPath = DemoPaths.Input("Llm/test_370_expected.md"); @@ -27,7 +27,7 @@ internal static void Test4LlmToMarkdownCompareExpected370(string[] args) Document document = new Document(pdfPath); try { - string actual = ToMarkdown( + string actual = MuPDF4LLM.ToMarkdown( document, header: false, footer: false, @@ -75,7 +75,7 @@ internal static void Test4LlmToMarkdownOcrFixture1(string[] args) string md; try { - md = ToMarkdown(doc); + md = MuPDF4LLM.ToMarkdown(doc); } finally { @@ -104,7 +104,7 @@ internal static void Test4LlmToMarkdownOcrFixture1(string[] args) } } - /// ToMarkdown(..., useOcr: false) on FFFD fixture. + /// MuPDF4LLM.ToMarkdown(..., useOcr: false) on FFFD fixture. internal static void Test4LlmToMarkdownOcrFixture2(string[] args) { _ = args; @@ -121,7 +121,7 @@ internal static void Test4LlmToMarkdownOcrFixture2(string[] args) string md; try { - md = ToMarkdown(doc, useOcr: false); + md = MuPDF4LLM.ToMarkdown(doc, useOcr: false); } finally { @@ -151,8 +151,8 @@ internal static void Test4LlmToMarkdownOcrFixture3(string[] args) string mdNoOcr; try { - md = ToMarkdown(doc); - mdNoOcr = ToMarkdown(doc, useOcr: false); + md = MuPDF4LLM.ToMarkdown(doc); + mdNoOcr = MuPDF4LLM.ToMarkdown(doc, useOcr: false); } finally { diff --git a/Demo/Samples/Llm/Program.Llm.cs b/Demo/Samples/Llm/Program.Llm.cs index 54489d11..5be94613 100644 --- a/Demo/Samples/Llm/Program.Llm.cs +++ b/Demo/Samples/Llm/Program.Llm.cs @@ -7,7 +7,7 @@ internal static void TestMarkdownReader() Console.WriteLine("\n=== TestMarkdownReader ======================="); var reader = new PDFMarkdownReader(); - string testFilePath = Path.GetFullPath("../../../../TestDocuments/Demo/columns.pdf"); + string testFilePath = DemoPaths.Input("columns.pdf"); var docs = reader.LoadData(testFilePath); @@ -17,37 +17,14 @@ internal static void TestMarkdownReader() } } - internal static void TestGetText() - { - Console.WriteLine("\n=== TestGetText ======================="); - - var reader = new PDFMarkdownReader(); - string testFilePath = Path.GetFullPath("../../../../TestDocuments/Demo/columns.pdf"); - - Document doc = new Document(testFilePath); - - for (int i = 0; i < doc.PageCount; i++) - { - Page page = doc[i]; - - var text = Utils.GetText(page, option: "dict"); - - Console.WriteLine(text); - - page.Dispose(); - } - - doc.Close(); - } - internal static void TestTable() { Console.WriteLine("\n=== TestTable ======================="); try { - string testFilePath = Path.GetFullPath("../../../../TestDocuments/Demo/err_table.pdf"); - + string testFilePath = DemoPaths.Input("err_table.pdf"); + if (!File.Exists(testFilePath)) { Console.WriteLine($"Error: Test file not found: {testFilePath}"); @@ -58,15 +35,13 @@ internal static void TestTable() Document doc = new Document(testFilePath); Console.WriteLine($"Document loaded: {doc.PageCount} page(s)"); - // Test on first page Page page = doc[0]; Console.WriteLine($"\nPage 0 - Rect: {page.Rect}"); - // Test 1: Get tables with default strategy Console.WriteLine("\n--- Test 1: Get tables with 'lines_strict' strategy ---"); List tables = Utils.GetTables( - page, - clip: page.Rect, + page, + clip: page.Rect, vertical_strategy: "lines_strict", horizontal_strategy: "lines_strict"); @@ -78,108 +53,29 @@ internal static void TestTable() { Table table = tables[i]; Console.WriteLine($"\n Table {i + 1}:"); - Console.WriteLine($" Rows: {table.row_count}"); - Console.WriteLine($" Columns: {table.col_count}"); - if (table.bbox != null) - { - Console.WriteLine($" BBox: ({table.bbox.X0:F2}, {table.bbox.Y0:F2}, {table.bbox.X1:F2}, {table.bbox.Y1:F2})"); - } - - // Display header information - if (table.header != null) - { - Console.WriteLine($" Header:"); - Console.WriteLine($" External: {table.header.external}"); - if (table.header.names != null && table.header.names.Count > 0) - { - Console.WriteLine($" Column names: {string.Join(", ", table.header.names)}"); - } - } - - // Extract table data - Console.WriteLine($"\n Extracting table data..."); - List> tableData = table.Extract(); - if (tableData != null && tableData.Count > 0) - { - Console.WriteLine($" Extracted {tableData.Count} row(s) of data"); - // Show first few rows as preview - int previewRows = Math.Min(3, tableData.Count); - for (int row = 0; row < previewRows; row++) - { - var rowData = tableData[row]; - if (rowData != null) - { - Console.WriteLine($" Row {row + 1}: {string.Join(" | ", rowData.Take(5))}"); // Show first 5 columns - } - } - if (tableData.Count > previewRows) - { - Console.WriteLine($" ... and {tableData.Count - previewRows} more row(s)"); - } - } - - // Convert to markdown - Console.WriteLine($"\n Converting to Markdown..."); + Console.WriteLine($" Rows: {table.RowCount}, Cols: {table.ColCount}"); try { - string markdown = table.ToMarkdown(clean: false, fillEmpty: true); - if (!string.IsNullOrEmpty(markdown)) - { - Console.WriteLine($" Markdown length: {markdown.Length} characters"); - // Save markdown to file - string markdownFile = $"table_{i + 1}_page0.md"; - File.WriteAllText(markdownFile, markdown, Encoding.UTF8); - Console.WriteLine($" Markdown saved to: {markdownFile}"); - - // Show preview - int previewLength = Math.Min(200, markdown.Length); - Console.WriteLine($" Preview (first {previewLength} chars):"); - Console.WriteLine($" {markdown.Substring(0, previewLength)}..."); - } + string md = table.ToMarkdown(); + Console.WriteLine($" Markdown preview:\n{md.Substring(0, Math.Min(500, md.Length))}..."); } catch (Exception ex) { - Console.WriteLine($" Error converting to markdown: {ex.Message}"); + Console.WriteLine($" ToMarkdown failed: {ex.Message}"); } } } - else - { - Console.WriteLine("No tables found. Trying with 'lines' strategy..."); - - // Test 2: Try with 'lines' strategy (less strict) - Console.WriteLine("\n--- Test 2: Get tables with 'lines' strategy ---"); - tables = Utils.GetTables( - page, - clip: page.Rect, - vertical_strategy: "lines", - horizontal_strategy: "lines"); - - Console.WriteLine($"Found {tables.Count} table(s) with 'lines' strategy"); - } - // Test 3: Try with 'text' strategy - Console.WriteLine("\n--- Test 3: Get tables with 'text' strategy ---"); - List
textTables = Utils.GetTables( - page, - clip: page.Rect, - vertical_strategy: "text", - horizontal_strategy: "text"); - - Console.WriteLine($"Found {textTables.Count} table(s) with 'text' strategy"); - - // Test 4: Get tables from all pages - Console.WriteLine("\n--- Test 4: Get tables from all pages ---"); int totalTables = 0; for (int pageNum = 0; pageNum < doc.PageCount; pageNum++) { Page currentPage = doc[pageNum]; List
pageTables = Utils.GetTables( - currentPage, - clip: currentPage.Rect, + currentPage, + clip: currentPage.Rect, vertical_strategy: "lines_strict", horizontal_strategy: "lines_strict"); - + if (pageTables.Count > 0) { Console.WriteLine($" Page {pageNum}: {pageTables.Count} table(s)"); @@ -191,7 +87,7 @@ internal static void TestTable() page.Dispose(); doc.Close(); - + Console.WriteLine("\n=== TestTable completed successfully ==="); } catch (Exception ex) @@ -204,30 +100,26 @@ internal static void TestTable() internal static void TestMuPdfRagToMarkdown() { - Console.WriteLine("\n=== TestMuPdfRagToMarkdown (legacy RAG via PDF4LLM.UseLayout=false) ======================="); + Console.WriteLine("\n=== TestMuPdfRagToMarkdown (legacy RAG via MuPDF4LLM.UseLayout=false) ======================="); try { - // Find a test PDF file - //string testFilePath = Path.GetFullPath("../../../../TestDocuments/Demo/national-capitals.pdf"); - string testFilePath = Path.GetFullPath("../../../../TestDocuments/Demo/Magazine.pdf"); + string testFilePath = DemoPaths.Input("Magazine.pdf"); Document doc = new Document(testFilePath); Console.WriteLine($"Document loaded: {doc.PageCount} page(s)"); Console.WriteLine($"Document name: {doc.Name}"); - // Test 1: legacy MuPdf_rag-style path (MuPdfRag is internal; use public API only) - Console.WriteLine("\n--- Test 1: PDF4LLM.ToMarkdown with UseLayout=false (same stack as internal MuPdfRag) ---"); + Console.WriteLine("\n--- Test 1: MuPDF4LLM.ToMarkdown with MuPDF4LLM.UseLayout =false ---"); try { - List pages = new List(); - pages.Add(0); - bool prev = UseLayout; + List pages = new List { 0 }; + bool prev = MuPDF4LLM.UseLayout; string markdown; try { - UseLayout = false; - markdown = ToMarkdown( + MuPDF4LLM.UseLayout = false; + markdown = MuPDF4LLM.ToMarkdown( doc, pages: pages, writeImages: false, @@ -246,10 +138,10 @@ internal static void TestMuPdfRagToMarkdown() } finally { - UseLayout = prev; + MuPDF4LLM.UseLayout = prev; } - string markdownFile = "TestMuPdfRag_Output.md"; + string markdownFile = DemoPaths.Output("TestMuPdfRag_Output.md"); File.WriteAllText(markdownFile, markdown, Encoding.UTF8); Console.WriteLine($"Markdown output saved to: {markdownFile}"); Console.WriteLine($"Markdown length: {markdown.Length} characters"); @@ -263,115 +155,7 @@ internal static void TestMuPdfRagToMarkdown() { Console.WriteLine($"Error in basic ToMarkdown: {ex.Message}"); } - /* - // Test 2: ToMarkdown with IdentifyHeaders - Console.WriteLine("\n--- Test 2: ToMarkdown with IdentifyHeaders ---"); - try - { - var identifyHeaders = new IdentifyHeaders(doc, pages: null, bodyLimit: 12.0f, maxLevels: 6); - string markdown = MuPdfRag.ToMarkdown( - doc, - pages: new List { 0 }, // First page only - hdrInfo: identifyHeaders, - writeImages: false, - embedImages: false, - ignoreImages: false, - filename: testFilePath, - forceText: true, - showProgress: false - ); - - string markdownFile = "TestMuPdfRag_WithHeaders.md"; - File.WriteAllText(markdownFile, markdown, Encoding.UTF8); - Console.WriteLine($"Markdown with headers saved to: {markdownFile}"); - Console.WriteLine($"Markdown length: {markdown.Length} characters"); - } - catch (Exception ex) - { - Console.WriteLine($"Error in ToMarkdown with IdentifyHeaders: {ex.Message}"); - } - - // Test 3: ToMarkdown with TocHeaders - Console.WriteLine("\n--- Test 3: ToMarkdown with TocHeaders ---"); - try - { - var tocHeaders = new TocHeaders(doc); - string markdown = MuPdfRag.ToMarkdown( - doc, - pages: new List { 0 }, // First page only - hdrInfo: tocHeaders, - writeImages: false, - embedImages: false, - ignoreImages: false, - filename: testFilePath, - forceText: true, - showProgress: false - ); - - string markdownFile = "TestMuPdfRag_WithToc.md"; - File.WriteAllText(markdownFile, markdown, Encoding.UTF8); - Console.WriteLine($"Markdown with TOC headers saved to: {markdownFile}"); - Console.WriteLine($"Markdown length: {markdown.Length} characters"); - } - catch (Exception ex) - { - Console.WriteLine($"Error in ToMarkdown with TocHeaders: {ex.Message}"); - } - - // Test 4: ToMarkdown with page separators - Console.WriteLine("\n--- Test 4: ToMarkdown with page separators ---"); - try - { - string markdown = MuPdfRag.ToMarkdown( - doc, - pages: null, // All pages - hdrInfo: null, - writeImages: false, - embedImages: false, - ignoreImages: false, - filename: testFilePath, - forceText: true, - pageSeparators: true, // Add page separators - showProgress: false - ); - string markdownFile = "TestMuPdfRag_WithSeparators.md"; - File.WriteAllText(markdownFile, markdown, Encoding.UTF8); - Console.WriteLine($"Markdown with page separators saved to: {markdownFile}"); - Console.WriteLine($"Markdown length: {markdown.Length} characters"); - } - catch (Exception ex) - { - Console.WriteLine($"Error in ToMarkdown with page separators: {ex.Message}"); - } - - // Test 5: ToMarkdown with progress bar - Console.WriteLine("\n--- Test 5: ToMarkdown with progress bar ---"); - try - { - string markdown = MuPdfRag.ToMarkdown( - doc, - pages: null, // All pages - hdrInfo: null, - writeImages: false, - embedImages: false, - ignoreImages: false, - filename: testFilePath, - forceText: true, - showProgress: true, // Show progress bar - pageSeparators: false - ); - - string markdownFile = "TestMuPdfRag_WithProgress.md"; - File.WriteAllText(markdownFile, markdown, Encoding.UTF8); - Console.WriteLine($"\nMarkdown with progress saved to: {markdownFile}"); - Console.WriteLine($"Markdown length: {markdown.Length} characters"); - } - catch (Exception ex) - { - Console.WriteLine($"Error in ToMarkdown with progress: {ex.Message}"); - } - */ doc.Close(); } catch (Exception ex) @@ -382,6 +166,5 @@ internal static void TestMuPdfRagToMarkdown() Console.WriteLine("\n=== TestMuPdfRagToMarkdown Completed ======================="); } - } } diff --git a/Demo/Samples/TextDrawing/Program.GetText.cs b/Demo/Samples/TextDrawing/Program.GetText.cs new file mode 100644 index 00000000..84cc3df3 --- /dev/null +++ b/Demo/Samples/TextDrawing/Program.GetText.cs @@ -0,0 +1,23 @@ +namespace Demo +{ + internal partial class Program + { + internal static void TestGetText() + { + Console.WriteLine("\n=== TestGetText ======================="); + + string testFilePath = DemoPaths.Input("columns.pdf"); + Document doc = new Document(testFilePath); + + for (int i = 0; i < doc.PageCount; i++) + { + Page page = doc[i]; + var text = Utils.GetText(page, option: "dict"); + Console.WriteLine(text); + page.Dispose(); + } + + doc.Close(); + } + } +} diff --git a/Directory.Build.props b/Directory.Build.props index 8de945a0..d4252c99 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ - + true diff --git a/Directory.Build.targets b/Directory.Build.targets index 61c66154..03336fab 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -21,10 +21,7 @@ namespace Artifex /// <summary>MuPDF.NET NuGet package.</summary> public const string MuPDFNet = "$(ArtifexMuPDFNetVersion)"%3B - /// <summary>PDF4LLM NuGet package.</summary> - public const string PDF4LLM = "$(ArtifexPDF4LLMVersion)"%3B - - /// <summary>pymupdf-layout PyPI package (PDF4LLM layout bridge).</summary> + /// <summary>pymupdf-layout PyPI package pin (used by companion MuPDF.NET.PDF4LLM).</summary> public const string PyMuPDFLayout = "$(ArtifexPyMuPDFLayoutVersion)"%3B } } @@ -40,4 +37,40 @@ namespace Artifex + + + $(MSBuildThisFileDirectory)build\MuPDFNetPDF4LLMVersions.g.cs + + + + + + <_VersionCs>// <auto-generated /> Source: Versions.props. Do not edit. +namespace MuPDF.NET.PDF4LLM +{ + /// <summary>Build-time version pins for MuPDF.NET.PDF4LLM (from Versions.props).</summary> + internal static class BuildVersions + { + public const string Package = "$(ArtifexMuPDFNetPDF4LLMVersion)"%3B + public const string RequiredMuPdf = "$(ArtifexMuPDFVersion)"%3B + public const string RequiredPyMuPDFLayout = "$(ArtifexPyMuPDFLayoutVersion)"%3B + public const string MuPDFNet = "$(ArtifexMuPDFNetVersion)"%3B + } +} + + + + + + + + diff --git a/PDF4LLM.Test/PDF4LLM.Test.csproj b/MuPDF.NET.PDF4LLM.Test/MuPDF.NET.PDF4LLM.Test.csproj similarity index 69% rename from PDF4LLM.Test/PDF4LLM.Test.csproj rename to MuPDF.NET.PDF4LLM.Test/MuPDF.NET.PDF4LLM.Test.csproj index 2bb9c11c..61cdb30f 100644 --- a/PDF4LLM.Test/PDF4LLM.Test.csproj +++ b/MuPDF.NET.PDF4LLM.Test/MuPDF.NET.PDF4LLM.Test.csproj @@ -2,7 +2,7 @@ net8.0 - PDF4LLM.Test + MuPDF.NET.PDF4LLM.Test enable annotations $(NoWarn);0168;0219;0162;8625;8600;8602;8604;8618;8619;NU1701 @@ -22,13 +22,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + - - - - - - - diff --git a/MuPDF.NET.PDF4LLM.Test/Pdf4LlmCollection.cs b/MuPDF.NET.PDF4LLM.Test/Pdf4LlmCollection.cs new file mode 100644 index 00000000..07814243 --- /dev/null +++ b/MuPDF.NET.PDF4LLM.Test/Pdf4LlmCollection.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace MuPDF.NET.PDF4LLM.Test +{ + /// + /// Serializes tests that mutate (mirrors Python module globals). + /// + [CollectionDefinition("MuPDF.NET.PDF4LLM", DisableParallelization = true)] + public class Pdf4LlmCollection + { + } +} diff --git a/PDF4LLM.Test/README.md b/MuPDF.NET.PDF4LLM.Test/README.md similarity index 75% rename from PDF4LLM.Test/README.md rename to MuPDF.NET.PDF4LLM.Test/README.md index bdff2bcf..bdeba199 100644 --- a/PDF4LLM.Test/README.md +++ b/MuPDF.NET.PDF4LLM.Test/README.md @@ -1,6 +1,6 @@ -# PDF4LLM.Test +# MuPDF.NET.PDF4LLM.Test -Unit tests for the PDF4LLM project. +Unit tests for the MuPDF.NET.PDF4LLM project. ## Test Structure @@ -8,7 +8,7 @@ The test project follows the same structure as `MuPDF.NET.Test` and uses NUnit a ## Test Classes -- **PDF4LLMTest**: Tests for the main `PDF4LLM` static class (`PDF4LLM` namespace) +- **MuPDF4LLM**: Tests for the main `MuPDF4LLM` static class (`MuPDF.NET.PDF4LLM` namespace) - Version information - Document conversion methods (ToMarkdown, ToJson, ToText) - LlamaIndex reader creation @@ -39,7 +39,9 @@ The test project follows the same structure as `MuPDF.NET.Test` and uses NUnit a Tests can be run using: - Visual Studio Test Explorer - `dotnet test` command -- NUnit Test Adapter +- xUnit Test Adapter + +Office/HWP integration tests (`TestOffice.cs`) use the `MuPDF.NET.Office` / `MuPDF.NET.Office.NativeAssets` NuGet packages. The test project references `MuPDF.NET.PDF4LLM` via NuGet (version from `Versions.props`), not a project reference. ## Test Resources diff --git a/PDF4LLM.Test/Test137.cs b/MuPDF.NET.PDF4LLM.Test/Test137.cs similarity index 79% rename from PDF4LLM.Test/Test137.cs rename to MuPDF.NET.PDF4LLM.Test/Test137.cs index d1317a26..0909d085 100644 --- a/PDF4LLM.Test/Test137.cs +++ b/MuPDF.NET.PDF4LLM.Test/Test137.cs @@ -2,12 +2,12 @@ using System.IO; using System.Text.RegularExpressions; using MuPDF.NET; -using PDF4LLM; +using MuPDF.NET.PDF4LLM; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class Test137 { private const string TestClassName = nameof(Test137); @@ -26,24 +26,24 @@ public void test_137() string path = Doc("test_137.pdf"); - bool prior = PdfExtractor.UseLayout; + bool prior = MuPDF4LLM.UseLayout; try { // layout package.use_layout(False) - PdfExtractor.SetUseLayout(false); + MuPDF4LLM.SetUseLayout(false); using (var document = new Document(path)) { - string md = PdfExtractor.ToMarkdown(document, embedImages: true); + string md = MuPDF4LLM.ToMarkdown(document, embedImages: true); // path_md = f'{path}.out_nolayout.md' // f.write(md) File.WriteAllText(Out("test_137.out_nolayout.md"), md); } // layout package.use_layout(True) - PdfExtractor.SetUseLayout(true); + MuPDF4LLM.SetUseLayout(true); using (var document = new Document(path)) { - string md = PdfExtractor.ToMarkdown(document, embedImages: true); + string md = MuPDF4LLM.ToMarkdown(document, embedImages: true); // path_md = f'{path}.out_layout.md' // f.write(md) File.WriteAllText(Out("test_137.out_layout.md"), md); @@ -51,7 +51,7 @@ public void test_137() } finally { - PdfExtractor.SetUseLayout(prior); + MuPDF4LLM.SetUseLayout(prior); } } @@ -67,13 +67,13 @@ public void test_to_markdown_link_malicious() string pathMdExpected = Doc("test_to_markdown_link_malicious.pdf.expected.md"); string pathMdActual = Out("test_to_markdown_link_malicious.pdf.md"); - bool prior = PdfExtractor.UseLayout; + bool prior = MuPDF4LLM.UseLayout; try { - PdfExtractor.SetUseLayout(false); + MuPDF4LLM.SetUseLayout(false); using (var document = new Document(path)) { - string md = PdfExtractor.ToMarkdown(document, embedImages: true); + string md = MuPDF4LLM.ToMarkdown(document, embedImages: true); File.WriteAllText(pathMdActual, md); string mdExpected = File.ReadAllText(pathMdExpected); Assert.Equal(NormalizeMarkdown(mdExpected), NormalizeMarkdown(md)); @@ -81,7 +81,7 @@ public void test_to_markdown_link_malicious() } finally { - PdfExtractor.SetUseLayout(prior); + MuPDF4LLM.SetUseLayout(prior); } } diff --git a/PDF4LLM.Test/Test370.cs b/MuPDF.NET.PDF4LLM.Test/Test370.cs similarity index 82% rename from PDF4LLM.Test/Test370.cs rename to MuPDF.NET.PDF4LLM.Test/Test370.cs index 2313ff43..691e3173 100644 --- a/PDF4LLM.Test/Test370.cs +++ b/MuPDF.NET.PDF4LLM.Test/Test370.cs @@ -1,12 +1,12 @@ using System; using System.IO; using MuPDF.NET; -using PDF4LLM; +using MuPDF.NET.PDF4LLM; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class Test370 { private const string TestClassName = nameof(Test370); @@ -24,14 +24,14 @@ public void test_370() string expected = File.ReadAllText(pathExpected); - bool priorLayout = PdfExtractor.UseLayout; + bool priorLayout = MuPDF4LLM.UseLayout; try { - PdfExtractor.SetUseLayout(true); + MuPDF4LLM.SetUseLayout(true); using (var document = new Document(path)) { - string actual = PdfExtractor.ToMarkdown( + string actual = MuPDF4LLM.ToMarkdown( document, writeImages: false, embedImages: false, @@ -45,7 +45,7 @@ public void test_370() File.WriteAllText(pathActual, actual); // Full golden compare only when layout is off (stext fallback). - if (!PdfExtractor.LayoutAvailable) + if (!MuPDF4LLM.LayoutAvailable) Assert.Equal(expected.Replace("\r\n", "\n"), actual.Replace("\r\n", "\n")); else { @@ -56,7 +56,7 @@ public void test_370() } finally { - PdfExtractor.SetUseLayout(priorLayout); + MuPDF4LLM.SetUseLayout(priorLayout); } } } diff --git a/PDF4LLM.Test/Test376.cs b/MuPDF.NET.PDF4LLM.Test/Test376.cs similarity index 78% rename from PDF4LLM.Test/Test376.cs rename to MuPDF.NET.PDF4LLM.Test/Test376.cs index 0aa71b72..325601a1 100644 --- a/PDF4LLM.Test/Test376.cs +++ b/MuPDF.NET.PDF4LLM.Test/Test376.cs @@ -1,12 +1,12 @@ using System.IO; using MuPDF.NET; -using PDF4LLM; -using PDF4LLM.Llama; +using MuPDF.NET.PDF4LLM; +using MuPDF.NET.PDF4LLM.Llama; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class Test376 { private const string TestClassName = nameof(Test376); @@ -24,7 +24,7 @@ public void test_376() } // reader = LlamaMarkdownReader() - PDFMarkdownReader reader = PdfExtractor.LlamaMarkdownReader(); + PDFMarkdownReader reader = MuPDF4LLM.LlamaMarkdownReader(); // documents = reader.load_data(path) var documents = reader.LoadData(path); Assert.NotNull(documents); diff --git a/PDF4LLM.Test/TestLayout.cs b/MuPDF.NET.PDF4LLM.Test/TestLayout.cs similarity index 56% rename from PDF4LLM.Test/TestLayout.cs rename to MuPDF.NET.PDF4LLM.Test/TestLayout.cs index 5eff7d60..3a22ae59 100644 --- a/PDF4LLM.Test/TestLayout.cs +++ b/MuPDF.NET.PDF4LLM.Test/TestLayout.cs @@ -1,10 +1,10 @@ -using PDF4LLM; -using PDF4LLM.Layout; +using MuPDF.NET.PDF4LLM; +using MuPDF.NET.PDF4LLM.Layout; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class TestLayout { [Fact] @@ -12,26 +12,26 @@ public void test_layout_switch() { // Check that we can activate/deactivate use of layout. - bool prior = PdfExtractor.UseLayout; + bool prior = MuPDF4LLM.UseLayout; try { - PdfExtractor.SetUseLayout(true); - Assert.True(PdfExtractor.UseLayout); + MuPDF4LLM.SetUseLayout(true); + Assert.True(MuPDF4LLM.UseLayout); if (PyMuPdfLayout.IsAvailable) - Assert.True(PdfExtractor.LayoutAvailable); + Assert.True(MuPDF4LLM.LayoutAvailable); - PdfExtractor.SetUseLayout(false); - Assert.False(PdfExtractor.UseLayout); - Assert.False(PdfExtractor.LayoutAvailable); + MuPDF4LLM.SetUseLayout(false); + Assert.False(MuPDF4LLM.UseLayout); + Assert.False(MuPDF4LLM.LayoutAvailable); - PdfExtractor.SetUseLayout(true); - Assert.True(PdfExtractor.UseLayout); + MuPDF4LLM.SetUseLayout(true); + Assert.True(MuPDF4LLM.UseLayout); if (PyMuPdfLayout.IsAvailable) - Assert.True(PdfExtractor.LayoutAvailable); + Assert.True(MuPDF4LLM.LayoutAvailable); } finally { - PdfExtractor.SetUseLayout(prior); + MuPDF4LLM.SetUseLayout(prior); } } @@ -42,16 +42,16 @@ public void test_layout_default() if (!PyMuPdfLayout.IsAvailable) return; - bool prior = PdfExtractor.UseLayout; + bool prior = MuPDF4LLM.UseLayout; try { - PdfExtractor.SetUseLayout(true); - Assert.True(PdfExtractor.UseLayout); - Assert.True(PdfExtractor.LayoutAvailable); + MuPDF4LLM.SetUseLayout(true); + Assert.True(MuPDF4LLM.UseLayout); + Assert.True(MuPDF4LLM.LayoutAvailable); } finally { - PdfExtractor.SetUseLayout(prior); + MuPDF4LLM.SetUseLayout(prior); } } @@ -61,11 +61,11 @@ public void test_layout_provider_returns_boxes() if (!PyMuPdfLayout.IsAvailable) return; - bool prior = PdfExtractor.UseLayout; + bool prior = MuPDF4LLM.UseLayout; try { - PdfExtractor.SetUseLayout(true); - Assert.True(PdfExtractor.LayoutAvailable); + MuPDF4LLM.SetUseLayout(true); + Assert.True(MuPDF4LLM.LayoutAvailable); string path = _Path.ForTestClass("test_370.pdf", nameof(Test370)); using (var doc = new MuPDF.NET.Document(path)) @@ -80,7 +80,7 @@ public void test_layout_provider_returns_boxes() } finally { - PdfExtractor.SetUseLayout(prior); + MuPDF4LLM.SetUseLayout(prior); } } } diff --git a/PDF4LLM.Test/TestMarkdownToPdf.cs b/MuPDF.NET.PDF4LLM.Test/TestMarkdownToPdf.cs similarity index 83% rename from PDF4LLM.Test/TestMarkdownToPdf.cs rename to MuPDF.NET.PDF4LLM.Test/TestMarkdownToPdf.cs index 3bd2a472..f8acfd84 100644 --- a/PDF4LLM.Test/TestMarkdownToPdf.cs +++ b/MuPDF.NET.PDF4LLM.Test/TestMarkdownToPdf.cs @@ -1,10 +1,10 @@ using System.IO; -using PDF4LLM; +using MuPDF.NET.PDF4LLM; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class TestMarkdownToPdf { private const string TestClassName = nameof(TestMarkdownToPdf); @@ -24,11 +24,11 @@ public void test_markdown_to_pdf() // new_pdf = layout package.markdown_to_pdf(old_md_path, output_path=...) string pdfPath = Out("test_markdown_to_pdf.pdf"); - PdfExtractor.MarkdownToPdf(oldMdPath, outputPath: pdfPath); + MuPDF4LLM.MarkdownToPdf(oldMdPath, outputPath: pdfPath); Assert.True(File.Exists(pdfPath)); // new_md = layout package.to_markdown(pdf_path, use_ocr=False) - string newMd = PdfExtractor.ToMarkdown(pdfPath, useOcr: false); + string newMd = MuPDF4LLM.ToMarkdown(pdfPath, useOcr: false); Assert.Equal(NormalizeMarkdown(oldMd), NormalizeMarkdown(newMd)); } diff --git a/PDF4LLM.Test/TestOcr.cs b/MuPDF.NET.PDF4LLM.Test/TestOcr.cs similarity index 85% rename from PDF4LLM.Test/TestOcr.cs rename to MuPDF.NET.PDF4LLM.Test/TestOcr.cs index d3fc210c..549ee748 100644 --- a/PDF4LLM.Test/TestOcr.cs +++ b/MuPDF.NET.PDF4LLM.Test/TestOcr.cs @@ -1,12 +1,12 @@ using System.IO; using MuPDF.NET; -using PDF4LLM; -using PDF4LLM.Ocr; +using MuPDF.NET.PDF4LLM; +using MuPDF.NET.PDF4LLM.Ocr; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class TestOcr { private const string TestClassName = nameof(TestOcr); @@ -41,7 +41,7 @@ public void test_ocr_1() return; string path = Doc("test_ocr_loremipsum_FFFD.pdf"); - string md = PdfExtractor.ToMarkdown(path); + string md = MuPDF4LLM.ToMarkdown(path); // f.write(md) File.WriteAllText(Out("out_test_ocr_1.md"), md); string replacement = TesseractApi.ReplacementUnicode.ToString(); @@ -55,7 +55,7 @@ public void test_ocr_1() public void test_ocr_2() { string path = Doc("test_ocr_loremipsum_FFFD.pdf"); - string md = PdfExtractor.ToMarkdown(path, useOcr: false); + string md = MuPDF4LLM.ToMarkdown(path, useOcr: false); File.WriteAllText(Out("out_test_ocr_2.md"), md); Assert.Contains(TesseractApi.ReplacementUnicode.ToString(), md); @@ -69,8 +69,8 @@ public void test_ocr_3() return; string path = Doc("test_ocr_loremipsum_svg.pdf"); - string md = PdfExtractor.ToMarkdown(path, useOcr: true, forceOcr: true); - string mdNoOcr = PdfExtractor.ToMarkdown(path, useOcr: false); + string md = MuPDF4LLM.ToMarkdown(path, useOcr: true, forceOcr: true); + string mdNoOcr = MuPDF4LLM.ToMarkdown(path, useOcr: false); File.WriteAllText(Out("out_test_ocr_3.md"), md); File.WriteAllText(Out("out_test_ocr_3_no_ocr.md"), mdNoOcr); if (_ocr_tesseract_available()) diff --git a/MuPDF.NET.PDF4LLM.Test/TestOffice.cs b/MuPDF.NET.PDF4LLM.Test/TestOffice.cs new file mode 100644 index 00000000..f178cec7 --- /dev/null +++ b/MuPDF.NET.PDF4LLM.Test/TestOffice.cs @@ -0,0 +1,120 @@ +using System; +using System.Runtime.InteropServices; +using MuPDF.NET.Office; +using MuPDF.NET.PDF4LLM; +using MuPDF.NET.PDF4LLM.Layout; +using MuPDF.NET.PDF4LLM.Llama; +using Xunit; + +namespace MuPDF.NET.PDF4LLM.Test +{ + /// + /// Integration coverage for the optional MuPDF.NET.Office document handler. + /// Mirrors the PyMuPDFPro test_4496 direct pymupdf4llm path scenario. + /// + [Collection("MuPDF.NET.PDF4LLM")] + public class TestOffice + { + private static readonly object UnlockLock = new object(); + private static bool _unlocked; + + [Theory] + [InlineData("test_4496.hwpx")] + [InlineData("test_4159.doc")] + [InlineData("pages.docx")] + [InlineData("pages.odt")] + public void ToMarkdown_accepts_Office_paths(string fileName) + { + if (!TryUnlockOffice()) + return; + + bool priorLayout = MuPDF4LLM.UseLayout; + try + { + MuPDF4LLM.SetUseLayout(false); + string markdown = MuPDF4LLM.ToMarkdown( + _Path.Office(fileName), + showProgress: false); + + Assert.NotNull(markdown); + } + finally + { + MuPDF4LLM.SetUseLayout(priorLayout); + } + } + + [Fact] + public void Layout_pipeline_accepts_HWPX_path() + { + if (!TryUnlockOffice() || !PyMuPdfLayout.IsAvailable) + return; + + string path = _Path.Office("test_4496.hwpx"); + bool priorLayout = MuPDF4LLM.UseLayout; + try + { + MuPDF4LLM.SetUseLayout(true); + + string markdown = MuPDF4LLM.ToMarkdown(path, showProgress: false); + string json = MuPDF4LLM.ToJson(path, useOcr: false); + string text = MuPDF4LLM.ToText(path, useOcr: false); + + Assert.NotNull(markdown); + Assert.False(string.IsNullOrWhiteSpace(json)); + Assert.NotNull(text); + } + finally + { + MuPDF4LLM.SetUseLayout(priorLayout); + } + } + + [Fact] + public void Llama_reader_accepts_DOCX_path() + { + if (!TryUnlockOffice()) + return; + + var reader = new PDFMarkdownReader(); + var documents = reader.LoadData(_Path.Office("pages.docx")); + + Assert.NotEmpty(documents); + Assert.All(documents, document => Assert.NotNull(document.Text)); + } + + [Fact] + public void GetKeyValues_is_safe_for_non_PDF_Office_document() + { + if (!TryUnlockOffice()) + return; + + var fields = MuPDF4LLM.GetKeyValues(_Path.Office("pages.odt")); + + Assert.Empty(fields); + } + + private static bool TryUnlockOffice() + { + // NativeAssets currently ships the SmartOffice bridge for Windows x64. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + || RuntimeInformation.ProcessArchitecture != Architecture.X64) + { + return false; + } + + lock (UnlockLock) + { + if (!_unlocked) + { + // No key is stored in MuPDF.NET.PDF4LLM.Test. Restricted mode is + // sufficient because all integration fixtures have <= 3 pages. + MuPDFOffice.Unlock(fontPathAuto: true); + _unlocked = true; + } + } + + return true; + } + } +} diff --git a/PDF4LLM.Test/TestPdfMarkdownReader.cs b/MuPDF.NET.PDF4LLM.Test/TestPdfMarkdownReader.cs similarity index 79% rename from PDF4LLM.Test/TestPdfMarkdownReader.cs rename to MuPDF.NET.PDF4LLM.Test/TestPdfMarkdownReader.cs index 948f465c..24240f3e 100644 --- a/PDF4LLM.Test/TestPdfMarkdownReader.cs +++ b/MuPDF.NET.PDF4LLM.Test/TestPdfMarkdownReader.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.IO; -using PDF4LLM; -using PDF4LLM.Llama; +using MuPDF.NET.PDF4LLM; +using MuPDF.NET.PDF4LLM.Llama; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class TestPdfMarkdownReader { // PDF = "input.pdf" @@ -24,7 +24,7 @@ public void test_load_data() if (path == null || !File.Exists(path)) return; - var pdfReader = PdfExtractor.LlamaMarkdownReader(); + var pdfReader = MuPDF4LLM.LlamaMarkdownReader(); var extraInfo = new Dictionary { ["test_key"] = "test_value" }; var documents = pdfReader.LoadData(path, extraInfo); @@ -35,17 +35,17 @@ public void test_load_data() [Fact] public void test_load_data_with_invalid_file_path() { - bool prior = PdfExtractor.UseLayout; + bool prior = MuPDF4LLM.UseLayout; try { - PdfExtractor.SetUseLayout(false); - var pdfReader = PdfExtractor.LlamaMarkdownReader(); + MuPDF4LLM.SetUseLayout(false); + var pdfReader = MuPDF4LLM.LlamaMarkdownReader(); var extraInfo = new Dictionary { ["test_key"] = "test_value" }; Assert.ThrowsAny(() => pdfReader.LoadData("fake/path", extraInfo)); } finally { - PdfExtractor.SetUseLayout(prior); + MuPDF4LLM.SetUseLayout(prior); } } @@ -53,7 +53,7 @@ public void test_load_data_with_invalid_file_path() public void test_load_data_with_invalid_extra_info() { // pdf_reader = PDFMarkdownReader() - var pdfReader = PdfExtractor.LlamaMarkdownReader(); + var pdfReader = MuPDF4LLM.LlamaMarkdownReader(); // path = _get_test_file_path(PDF) string? path = _get_test_file_path(PDF); if (path == null || !File.Exists(path)) diff --git a/PDF4LLM.Test/TestSce150.cs b/MuPDF.NET.PDF4LLM.Test/TestSce150.cs similarity index 85% rename from PDF4LLM.Test/TestSce150.cs rename to MuPDF.NET.PDF4LLM.Test/TestSce150.cs index 4ecb2b77..b8579dce 100644 --- a/PDF4LLM.Test/TestSce150.cs +++ b/MuPDF.NET.PDF4LLM.Test/TestSce150.cs @@ -1,10 +1,10 @@ using System.IO; -using PDF4LLM; +using MuPDF.NET.PDF4LLM; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class TestSce150 { private const string TestClassName = nameof(TestSce150); @@ -24,11 +24,11 @@ private static void RunGoldenCompare(string pdfFileName, string expectedFileName string expected = NormalizeExpected(File.ReadAllText(Expected(expectedFileName))); - bool prior = PdfExtractor.UseLayout; + bool prior = MuPDF4LLM.UseLayout; try { - PdfExtractor.SetUseLayout(true); - string md = PdfExtractor.ToMarkdown( + MuPDF4LLM.SetUseLayout(true); + string md = MuPDF4LLM.ToMarkdown( pdfPath, writeImages: false, embedImages: false, @@ -39,7 +39,7 @@ private static void RunGoldenCompare(string pdfFileName, string expectedFileName } finally { - PdfExtractor.SetUseLayout(prior); + MuPDF4LLM.SetUseLayout(prior); } } diff --git a/PDF4LLM.Test/TestSce156.cs b/MuPDF.NET.PDF4LLM.Test/TestSce156.cs similarity index 65% rename from PDF4LLM.Test/TestSce156.cs rename to MuPDF.NET.PDF4LLM.Test/TestSce156.cs index a6f00263..01bc627c 100644 --- a/PDF4LLM.Test/TestSce156.cs +++ b/MuPDF.NET.PDF4LLM.Test/TestSce156.cs @@ -1,9 +1,9 @@ -using PDF4LLM; +using MuPDF.NET.PDF4LLM; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class TestSce156 { private const string TestClassName = nameof(TestSce156); @@ -12,16 +12,16 @@ public class TestSce156 public void test_sce_156() { // Python installs rapidocr then runs to_markdown with page_chunks and OCR. - // PDF4LLM: smoke-test the same API surface (must not throw). + // MuPDF.NET.PDF4LLM: smoke-test the same API surface (must not throw). string? path = _Path.TryForTestClass("test_sce_156.pdf", TestClassName); if (path == null) return; - bool prior = PdfExtractor.UseLayout; + bool prior = MuPDF4LLM.UseLayout; try { - PdfExtractor.SetUseLayout(true); - string result = PdfExtractor.ToMarkdown( + MuPDF4LLM.SetUseLayout(true); + string result = MuPDF4LLM.ToMarkdown( path, pageChunks: true, showProgress: false, @@ -30,7 +30,7 @@ public void test_sce_156() } finally { - PdfExtractor.SetUseLayout(prior); + MuPDF4LLM.SetUseLayout(prior); } } } diff --git a/PDF4LLM.Test/TestTablulate.cs b/MuPDF.NET.PDF4LLM.Test/TestTablulate.cs similarity index 76% rename from PDF4LLM.Test/TestTablulate.cs rename to MuPDF.NET.PDF4LLM.Test/TestTablulate.cs index 270677f9..823fd130 100644 --- a/PDF4LLM.Test/TestTablulate.cs +++ b/MuPDF.NET.PDF4LLM.Test/TestTablulate.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; -using PDF4LLM; +using MuPDF.NET.PDF4LLM; using Xunit; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - [Collection("PDF4LLM")] + [Collection("MuPDF.NET.PDF4LLM")] public class TestTablulate { private const string TestClassName = nameof(TestTablulate); @@ -14,14 +14,14 @@ public class TestTablulate [Fact] public void test_tablulate_bug() { - bool prior = PdfExtractor.UseLayout; + bool prior = MuPDF4LLM.UseLayout; try { // layout package.use_layout(True) # default in Python when layout bridge is available - PdfExtractor.SetUseLayout(true); + MuPDF4LLM.SetUseLayout(true); string path = Doc("test_tablulate_bug.pdf"); - string pageList = PdfExtractor.ToText( + string pageList = MuPDF4LLM.ToText( path, pageChunks: true, useOcr: false, @@ -35,7 +35,7 @@ public void test_tablulate_bug() } finally { - PdfExtractor.SetUseLayout(prior); + MuPDF4LLM.SetUseLayout(prior); } } } diff --git a/PDF4LLM.Test/_Path.cs b/MuPDF.NET.PDF4LLM.Test/_Path.cs similarity index 75% rename from PDF4LLM.Test/_Path.cs rename to MuPDF.NET.PDF4LLM.Test/_Path.cs index 04d04ea1..cc32f3ae 100644 --- a/PDF4LLM.Test/_Path.cs +++ b/MuPDF.NET.PDF4LLM.Test/_Path.cs @@ -1,9 +1,9 @@ using System; using System.IO; -namespace PDF4LLM.Test +namespace MuPDF.NET.PDF4LLM.Test { - /// Test document paths (mirrors MuPDF.NET.Test/_GenUtils.cs _Path). + /// Test document paths relative to the MuPDF.NET.PDF4LLM solution root. internal static class _Path { public const string SharedFolderName = "Shared"; @@ -28,8 +28,8 @@ public static string ResolveTestDocument(string fileName, string? subFolder = nu { string root = ResolveSolutionRoot(); return string.IsNullOrWhiteSpace(subFolder) - ? Path.Combine(root, "TestDocuments", "PDF4LLM.Test", fileName) - : Path.Combine(root, "TestDocuments", "PDF4LLM.Test", subFolder, fileName); + ? Path.Combine(root, "TestDocuments", "MuPDF.NET.PDF4LLM.Test", fileName) + : Path.Combine(root, "TestDocuments", "MuPDF.NET.PDF4LLM.Test", subFolder, fileName); } public static string RequireTestDocument(string fileName, string? subFolder = null) @@ -43,6 +43,9 @@ public static string RequireTestDocument(string fileName, string? subFolder = nu public static string Shared(string fileName) => RequireTestDocument(fileName, SharedFolderName); + public static string Office(string fileName) => + RequireTestDocument(fileName, "TestOffice"); + public static string ForTestClass(string fileName, string testClassName) => RequireTestDocument(fileName, testClassName); @@ -54,30 +57,30 @@ public static string ForOutput(string fileName, string testClassName) throw new ArgumentException("testClassName must not be empty.", nameof(testClassName)); string path = Path.Combine( - ResolveSolutionRoot(), "TestDocuments", "PDF4LLM.Test", OutputFolderName, testClassName, fileName); + ResolveSolutionRoot(), "TestDocuments", "MuPDF.NET.PDF4LLM.Test", OutputFolderName, testClassName, fileName); Directory.CreateDirectory(Path.GetDirectoryName(path)!); return path; } - /// Test class fixture, or upstream layout package*/tests when absent. + /// Test class fixture, or empty when absent. public static string ForTestClassOrUpstream(string fileName, string testClassName) { string local = Path.Combine( - ResolveSolutionRoot(), "TestDocuments", "PDF4LLM.Test", testClassName, fileName); + ResolveSolutionRoot(), "TestDocuments", "MuPDF.NET.PDF4LLM.Test", testClassName, fileName); if (File.Exists(local)) return local; return ""; } - /// Returns null when the fixture is not present locally or upstream. + /// Returns null when the fixture is not present. public static string? TryForTestClass(string fileName, string testClassName) { string local = Path.Combine( - ResolveSolutionRoot(), "TestDocuments", "PDF4LLM.Test", testClassName, fileName); + ResolveSolutionRoot(), "TestDocuments", "MuPDF.NET.PDF4LLM.Test", testClassName, fileName); if (File.Exists(local)) return local; return null; } } -} \ No newline at end of file +} diff --git a/PDF4LLM.Test/resources/140.pdf b/MuPDF.NET.PDF4LLM.Test/resources/140.pdf similarity index 100% rename from PDF4LLM.Test/resources/140.pdf rename to MuPDF.NET.PDF4LLM.Test/resources/140.pdf diff --git a/PDF4LLM.Test/resources/Magazine.pdf b/MuPDF.NET.PDF4LLM.Test/resources/Magazine.pdf similarity index 100% rename from PDF4LLM.Test/resources/Magazine.pdf rename to MuPDF.NET.PDF4LLM.Test/resources/Magazine.pdf diff --git a/PDF4LLM.Test/resources/columns.pdf b/MuPDF.NET.PDF4LLM.Test/resources/columns.pdf similarity index 100% rename from PDF4LLM.Test/resources/columns.pdf rename to MuPDF.NET.PDF4LLM.Test/resources/columns.pdf diff --git a/PDF4LLM.Test/resources/national-capitals.pdf b/MuPDF.NET.PDF4LLM.Test/resources/national-capitals.pdf similarity index 100% rename from PDF4LLM.Test/resources/national-capitals.pdf rename to MuPDF.NET.PDF4LLM.Test/resources/national-capitals.pdf diff --git a/PDF4LLM.Test/resources/nato-members.pdf b/MuPDF.NET.PDF4LLM.Test/resources/nato-members.pdf similarity index 100% rename from PDF4LLM.Test/resources/nato-members.pdf rename to MuPDF.NET.PDF4LLM.Test/resources/nato-members.pdf diff --git a/PDF4LLM.Test/resources/test_370_expected.md b/MuPDF.NET.PDF4LLM.Test/resources/test_370_expected.md similarity index 100% rename from PDF4LLM.Test/resources/test_370_expected.md rename to MuPDF.NET.PDF4LLM.Test/resources/test_370_expected.md diff --git a/PDF4LLM.Test/resources/test_markdown_to_pdf-expected.md b/MuPDF.NET.PDF4LLM.Test/resources/test_markdown_to_pdf-expected.md similarity index 100% rename from PDF4LLM.Test/resources/test_markdown_to_pdf-expected.md rename to MuPDF.NET.PDF4LLM.Test/resources/test_markdown_to_pdf-expected.md diff --git a/PDF4LLM/.gitattributes b/MuPDF.NET.PDF4LLM/.gitattributes similarity index 100% rename from PDF4LLM/.gitattributes rename to MuPDF.NET.PDF4LLM/.gitattributes diff --git a/PDF4LLM/AI/Abstractions/IChunkingService.cs b/MuPDF.NET.PDF4LLM/AI/Abstractions/IChunkingService.cs similarity index 78% rename from PDF4LLM/AI/Abstractions/IChunkingService.cs rename to MuPDF.NET.PDF4LLM/AI/Abstractions/IChunkingService.cs index a1719a3a..afc467ac 100644 --- a/PDF4LLM/AI/Abstractions/IChunkingService.cs +++ b/MuPDF.NET.PDF4LLM/AI/Abstractions/IChunkingService.cs @@ -1,6 +1,6 @@ -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Abstractions; +namespace MuPDF.NET.PDF4LLM.AI.Abstractions; /// Splits extracted pages into RAG-sized chunks (Phase 2 — Kernel Memory). public interface IChunkingService diff --git a/PDF4LLM/AI/Abstractions/IPdfExtractor.cs b/MuPDF.NET.PDF4LLM/AI/Abstractions/IPdfExtractor.cs similarity index 82% rename from PDF4LLM/AI/Abstractions/IPdfExtractor.cs rename to MuPDF.NET.PDF4LLM/AI/Abstractions/IPdfExtractor.cs index 84f83681..2b174966 100644 --- a/PDF4LLM/AI/Abstractions/IPdfExtractor.cs +++ b/MuPDF.NET.PDF4LLM/AI/Abstractions/IPdfExtractor.cs @@ -1,6 +1,6 @@ -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Abstractions; +namespace MuPDF.NET.PDF4LLM.AI.Abstractions; /// Extracts structured text from PDF files (Phase 1). public interface IPdfExtractor diff --git a/PDF4LLM/AI/Abstractions/IRagCompletionService.cs b/MuPDF.NET.PDF4LLM/AI/Abstractions/IRagCompletionService.cs similarity index 92% rename from PDF4LLM/AI/Abstractions/IRagCompletionService.cs rename to MuPDF.NET.PDF4LLM/AI/Abstractions/IRagCompletionService.cs index a4dfa0b3..03038f93 100644 --- a/PDF4LLM/AI/Abstractions/IRagCompletionService.cs +++ b/MuPDF.NET.PDF4LLM/AI/Abstractions/IRagCompletionService.cs @@ -1,6 +1,6 @@ -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Abstractions; +namespace MuPDF.NET.PDF4LLM.AI.Abstractions; /// LLM reasoning for Q&A and summaries (Phase 3 — Azure OpenAI / Microsoft.Extensions.AI). public interface IRagCompletionService diff --git a/PDF4LLM/AI/Abstractions/IVectorIndex.cs b/MuPDF.NET.PDF4LLM/AI/Abstractions/IVectorIndex.cs similarity index 92% rename from PDF4LLM/AI/Abstractions/IVectorIndex.cs rename to MuPDF.NET.PDF4LLM/AI/Abstractions/IVectorIndex.cs index 8de8e0d7..28b23da4 100644 --- a/PDF4LLM/AI/Abstractions/IVectorIndex.cs +++ b/MuPDF.NET.PDF4LLM/AI/Abstractions/IVectorIndex.cs @@ -1,6 +1,6 @@ -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Abstractions; +namespace MuPDF.NET.PDF4LLM.AI.Abstractions; /// Vector store for semantic search (Phase 2 — Azure AI Search or in-memory). public interface IVectorIndex diff --git a/PDF4LLM/AI/AiDocumentCollection.cs b/MuPDF.NET.PDF4LLM/AI/AiDocumentCollection.cs similarity index 96% rename from PDF4LLM/AI/AiDocumentCollection.cs rename to MuPDF.NET.PDF4LLM/AI/AiDocumentCollection.cs index ba9046e2..8481ec1b 100644 --- a/PDF4LLM/AI/AiDocumentCollection.cs +++ b/MuPDF.NET.PDF4LLM/AI/AiDocumentCollection.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.AI; -using PDF4LLM.AI.Abstractions; -using PDF4LLM.AI.Models; -using PDF4LLM.AI.Services; +using MuPDF.NET.PDF4LLM.AI.Abstractions; +using MuPDF.NET.PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Services; -namespace PDF4LLM.AI; +namespace MuPDF.NET.PDF4LLM.AI; /// /// Indexed PDF corpus returned by . diff --git a/PDF4LLM/AI/GlobalUsings.cs b/MuPDF.NET.PDF4LLM/AI/GlobalUsings.cs similarity index 100% rename from PDF4LLM/AI/GlobalUsings.cs rename to MuPDF.NET.PDF4LLM/AI/GlobalUsings.cs diff --git a/PDF4LLM/AI/MicrosoftAIConnector.cs b/MuPDF.NET.PDF4LLM/AI/MicrosoftAIConnector.cs similarity index 91% rename from PDF4LLM/AI/MicrosoftAIConnector.cs rename to MuPDF.NET.PDF4LLM/AI/MicrosoftAIConnector.cs index 90d7525a..91a978b7 100644 --- a/PDF4LLM/AI/MicrosoftAIConnector.cs +++ b/MuPDF.NET.PDF4LLM/AI/MicrosoftAIConnector.cs @@ -1,13 +1,13 @@ using Microsoft.Extensions.AI; -using PDF4LLM.AI.Abstractions; -using PDF4LLM.AI.Models; -using PDF4LLM.AI.Options; -using PDF4LLM.AI.Services; +using MuPDF.NET.PDF4LLM.AI.Abstractions; +using MuPDF.NET.PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Options; +using MuPDF.NET.PDF4LLM.AI.Services; -namespace PDF4LLM.AI; +namespace MuPDF.NET.PDF4LLM.AI; /// -/// Unified connector linking PDF4LLM extraction with Microsoft's AI ecosystem +/// Unified connector linking MuPDF.NET.PDF4LLM extraction with Microsoft's AI ecosystem /// (Kernel Memory chunking, Microsoft.Extensions.AI embeddings, Azure AI Search, Azure OpenAI). /// public static class MsAIConnector @@ -34,7 +34,7 @@ public static async Task LoadAsync( options ??= MicrosoftAIConnectorOptions.CreateForDevelopment(); - IPdfExtractor extractor = options.PdfExtractor + IPdfExtractor extractor = options.MuPDF4LLM ?? new Pdf4LlmExtractor(options.UseLayout); IChunkingService chunking = options.ChunkingService diff --git a/PDF4LLM/AI/Models/AiChunk.cs b/MuPDF.NET.PDF4LLM/AI/Models/AiChunk.cs similarity index 93% rename from PDF4LLM/AI/Models/AiChunk.cs rename to MuPDF.NET.PDF4LLM/AI/Models/AiChunk.cs index 6f99fbe9..28a4f301 100644 --- a/PDF4LLM/AI/Models/AiChunk.cs +++ b/MuPDF.NET.PDF4LLM/AI/Models/AiChunk.cs @@ -1,4 +1,4 @@ -namespace PDF4LLM.AI.Models; +namespace MuPDF.NET.PDF4LLM.AI.Models; /// A text segment indexed for retrieval (RAG chunk). public sealed class AiChunk diff --git a/PDF4LLM/AI/Models/ExtractedPage.cs b/MuPDF.NET.PDF4LLM/AI/Models/ExtractedPage.cs similarity index 72% rename from PDF4LLM/AI/Models/ExtractedPage.cs rename to MuPDF.NET.PDF4LLM/AI/Models/ExtractedPage.cs index d39e4e42..895ee062 100644 --- a/PDF4LLM/AI/Models/ExtractedPage.cs +++ b/MuPDF.NET.PDF4LLM/AI/Models/ExtractedPage.cs @@ -1,6 +1,6 @@ -namespace PDF4LLM.AI.Models; +namespace MuPDF.NET.PDF4LLM.AI.Models; -/// Structured page content extracted from a PDF via PDF4LLM. +/// Structured page content extracted from a PDF via MuPDF.NET.PDF4LLM. public sealed class ExtractedPage { public required string SourceFilePath { get; init; } diff --git a/PDF4LLM/AI/Models/SearchResult.cs b/MuPDF.NET.PDF4LLM/AI/Models/SearchResult.cs similarity index 84% rename from PDF4LLM/AI/Models/SearchResult.cs rename to MuPDF.NET.PDF4LLM/AI/Models/SearchResult.cs index 3dfec244..645ea9ff 100644 --- a/PDF4LLM/AI/Models/SearchResult.cs +++ b/MuPDF.NET.PDF4LLM/AI/Models/SearchResult.cs @@ -1,4 +1,4 @@ -namespace PDF4LLM.AI.Models; +namespace MuPDF.NET.PDF4LLM.AI.Models; /// Chunk returned by semantic search with relevance score. public sealed class SearchResult diff --git a/PDF4LLM/AI/Options/AzureAiOptions.cs b/MuPDF.NET.PDF4LLM/AI/Options/AzureAiOptions.cs similarity index 91% rename from PDF4LLM/AI/Options/AzureAiOptions.cs rename to MuPDF.NET.PDF4LLM/AI/Options/AzureAiOptions.cs index cc9e108b..1076f4fd 100644 --- a/PDF4LLM/AI/Options/AzureAiOptions.cs +++ b/MuPDF.NET.PDF4LLM/AI/Options/AzureAiOptions.cs @@ -2,9 +2,9 @@ using Microsoft.Extensions.AI; using OpenAI.Chat; -namespace PDF4LLM.AI.Options; +namespace MuPDF.NET.PDF4LLM.AI.Options; -/// Azure OpenAI + Azure AI Search settings from the PDF4LLM AI development plan. +/// Azure OpenAI + Azure AI Search settings from the MuPDF.NET.PDF4LLM AI development plan. public sealed class AzureAiOptions { public required string OpenAiEndpoint { get; init; } diff --git a/PDF4LLM/AI/Options/MicrosoftAIConnectorOptions.cs b/MuPDF.NET.PDF4LLM/AI/Options/MicrosoftAIConnectorOptions.cs similarity index 91% rename from PDF4LLM/AI/Options/MicrosoftAIConnectorOptions.cs rename to MuPDF.NET.PDF4LLM/AI/Options/MicrosoftAIConnectorOptions.cs index 968656fb..d97beb55 100644 --- a/PDF4LLM/AI/Options/MicrosoftAIConnectorOptions.cs +++ b/MuPDF.NET.PDF4LLM/AI/Options/MicrosoftAIConnectorOptions.cs @@ -1,13 +1,13 @@ using Microsoft.Extensions.AI; -using PDF4LLM.AI.Abstractions; -using PDF4LLM.AI.Services; +using MuPDF.NET.PDF4LLM.AI.Abstractions; +using MuPDF.NET.PDF4LLM.AI.Services; -namespace PDF4LLM.AI.Options; +namespace MuPDF.NET.PDF4LLM.AI.Options; /// Configuration for . public sealed class MicrosoftAIConnectorOptions { - /// Use PDF4LLM layout pipeline when a layout provider is registered. + /// Use MuPDF.NET.PDF4LLM layout pipeline when a layout provider is registered. public bool UseLayout { get; set; } /// Maximum characters per RAG chunk (Kernel Memory text partition size). @@ -20,7 +20,7 @@ public sealed class MicrosoftAIConnectorOptions public int DefaultTopK { get; set; } = 8; /// Optional custom PDF extractor (defaults to ). - public IPdfExtractor? PdfExtractor { get; set; } + public IPdfExtractor? MuPDF4LLM { get; set; } /// Optional custom chunking service (defaults to overlap text partitioning). public IChunkingService? ChunkingService { get; set; } diff --git a/PDF4LLM/AI/Services/AzureSearchVectorIndex.cs b/MuPDF.NET.PDF4LLM/AI/Services/AzureSearchVectorIndex.cs similarity index 98% rename from PDF4LLM/AI/Services/AzureSearchVectorIndex.cs rename to MuPDF.NET.PDF4LLM/AI/Services/AzureSearchVectorIndex.cs index cdf11b89..0efbb0a2 100644 --- a/PDF4LLM/AI/Services/AzureSearchVectorIndex.cs +++ b/MuPDF.NET.PDF4LLM/AI/Services/AzureSearchVectorIndex.cs @@ -3,10 +3,10 @@ using Azure.Search.Documents.Indexes; using Azure.Search.Documents.Indexes.Models; using Azure.Search.Documents.Models; -using PDF4LLM.AI.Abstractions; -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Abstractions; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Services; +namespace MuPDF.NET.PDF4LLM.AI.Services; /// Phase 2 vector indexing via Azure AI Search. public sealed class AzureSearchVectorIndex : IVectorIndex diff --git a/PDF4LLM/AI/Services/EmbeddingIndexer.cs b/MuPDF.NET.PDF4LLM/AI/Services/EmbeddingIndexer.cs similarity index 93% rename from PDF4LLM/AI/Services/EmbeddingIndexer.cs rename to MuPDF.NET.PDF4LLM/AI/Services/EmbeddingIndexer.cs index 8c97d9d8..73d751b6 100644 --- a/PDF4LLM/AI/Services/EmbeddingIndexer.cs +++ b/MuPDF.NET.PDF4LLM/AI/Services/EmbeddingIndexer.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.AI; -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Services; +namespace MuPDF.NET.PDF4LLM.AI.Services; /// Generates embeddings for chunks via Microsoft.Extensions.AI. internal static class EmbeddingIndexer diff --git a/PDF4LLM/AI/Services/ExtensionsAiRagCompletionService.cs b/MuPDF.NET.PDF4LLM/AI/Services/ExtensionsAiRagCompletionService.cs similarity index 95% rename from PDF4LLM/AI/Services/ExtensionsAiRagCompletionService.cs rename to MuPDF.NET.PDF4LLM/AI/Services/ExtensionsAiRagCompletionService.cs index 22c0e4be..124d2157 100644 --- a/PDF4LLM/AI/Services/ExtensionsAiRagCompletionService.cs +++ b/MuPDF.NET.PDF4LLM/AI/Services/ExtensionsAiRagCompletionService.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.AI; -using PDF4LLM.AI.Abstractions; -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Abstractions; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Services; +namespace MuPDF.NET.PDF4LLM.AI.Services; /// Phase 3 Q&A and summarization via (Azure OpenAI / GPT-4o). public sealed class ExtensionsAiRagCompletionService : IRagCompletionService diff --git a/PDF4LLM/AI/Services/HashEmbeddingGenerator.cs b/MuPDF.NET.PDF4LLM/AI/Services/HashEmbeddingGenerator.cs similarity index 97% rename from PDF4LLM/AI/Services/HashEmbeddingGenerator.cs rename to MuPDF.NET.PDF4LLM/AI/Services/HashEmbeddingGenerator.cs index 1a3ff4de..60cd8f37 100644 --- a/PDF4LLM/AI/Services/HashEmbeddingGenerator.cs +++ b/MuPDF.NET.PDF4LLM/AI/Services/HashEmbeddingGenerator.cs @@ -2,7 +2,7 @@ using System.Text; using Microsoft.Extensions.AI; -namespace PDF4LLM.AI.Services; +namespace MuPDF.NET.PDF4LLM.AI.Services; /// Deterministic embeddings for development/tests (no cloud credentials). public sealed class HashEmbeddingGenerator : IEmbeddingGenerator> diff --git a/PDF4LLM/AI/Services/InMemoryVectorIndex.cs b/MuPDF.NET.PDF4LLM/AI/Services/InMemoryVectorIndex.cs similarity index 95% rename from PDF4LLM/AI/Services/InMemoryVectorIndex.cs rename to MuPDF.NET.PDF4LLM/AI/Services/InMemoryVectorIndex.cs index 18b6083a..6be33db0 100644 --- a/PDF4LLM/AI/Services/InMemoryVectorIndex.cs +++ b/MuPDF.NET.PDF4LLM/AI/Services/InMemoryVectorIndex.cs @@ -1,7 +1,7 @@ -using PDF4LLM.AI.Abstractions; -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Abstractions; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Services; +namespace MuPDF.NET.PDF4LLM.AI.Services; /// In-memory cosine-similarity index for development and tests. public sealed class InMemoryVectorIndex : IVectorIndex diff --git a/PDF4LLM/AI/Services/Pdf4LlmExtractor.cs b/MuPDF.NET.PDF4LLM/AI/Services/Pdf4LlmExtractor.cs similarity index 86% rename from PDF4LLM/AI/Services/Pdf4LlmExtractor.cs rename to MuPDF.NET.PDF4LLM/AI/Services/Pdf4LlmExtractor.cs index 4f579377..33a4024b 100644 --- a/PDF4LLM/AI/Services/Pdf4LlmExtractor.cs +++ b/MuPDF.NET.PDF4LLM/AI/Services/Pdf4LlmExtractor.cs @@ -1,15 +1,15 @@ using Newtonsoft.Json.Linq; -using PDF4LLM.AI.Abstractions; -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Abstractions; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Services; +namespace MuPDF.NET.PDF4LLM.AI.Services; -/// Phase 1 extraction via PDF4LLM to_text(page_chunks=true). +/// Phase 1 extraction via MuPDF.NET.PDF4LLM to_text(page_chunks=true). public sealed class Pdf4LlmExtractor : IPdfExtractor { private readonly bool _useLayout; - /// When , extract using the PDF4LLM layout pipeline. + /// When , extract using the MuPDF.NET.PDF4LLM layout pipeline. public Pdf4LlmExtractor(bool useLayout = false) => _useLayout = useLayout; /// @@ -23,12 +23,12 @@ public Task> ExtractAsync( if (!File.Exists(pdfPath)) throw new FileNotFoundException("PDF not found.", pdfPath); - bool priorLayout = PdfExtractor.UseLayout; + bool priorLayout = MuPDF4LLM.UseLayout; try { - PdfExtractor.SetUseLayout(_useLayout); + MuPDF4LLM.SetUseLayout(_useLayout); - string json = PdfExtractor.ToText( + string json = MuPDF4LLM.ToText( pdfPath, pageChunks: true, header: true, @@ -45,7 +45,7 @@ public Task> ExtractAsync( } finally { - PdfExtractor.SetUseLayout(priorLayout); + MuPDF4LLM.SetUseLayout(priorLayout); } } @@ -86,7 +86,7 @@ private static List ExtractLegacyPages(string pdfPath) using var doc = new MuPDF.NET.Document(pdfPath); for (int i = 0; i < doc.PageCount; i++) { - string text = PdfExtractor.ToMarkdown( + string text = MuPDF4LLM.ToMarkdown( doc, pages: new List { i }, filename: pdfPath, diff --git a/PDF4LLM/AI/Services/TextChunkingService.cs b/MuPDF.NET.PDF4LLM/AI/Services/TextChunkingService.cs similarity index 95% rename from PDF4LLM/AI/Services/TextChunkingService.cs rename to MuPDF.NET.PDF4LLM/AI/Services/TextChunkingService.cs index f5c528bc..e06a11f8 100644 --- a/PDF4LLM/AI/Services/TextChunkingService.cs +++ b/MuPDF.NET.PDF4LLM/AI/Services/TextChunkingService.cs @@ -1,7 +1,7 @@ -using PDF4LLM.AI.Abstractions; -using PDF4LLM.AI.Models; +using MuPDF.NET.PDF4LLM.AI.Abstractions; +using MuPDF.NET.PDF4LLM.AI.Models; -namespace PDF4LLM.AI.Services; +namespace MuPDF.NET.PDF4LLM.AI.Services; /// /// Phase 2 text chunking with overlap (Kernel Memory–compatible partition sizes). diff --git a/PDF4LLM/CHANGELOG.md b/MuPDF.NET.PDF4LLM/CHANGELOG.md similarity index 66% rename from PDF4LLM/CHANGELOG.md rename to MuPDF.NET.PDF4LLM/CHANGELOG.md index 49a89cad..8478166f 100644 --- a/PDF4LLM/CHANGELOG.md +++ b/MuPDF.NET.PDF4LLM/CHANGELOG.md @@ -1,12 +1,12 @@ # Changelog -All notable changes for `PDF4LLM` are documented in this file. +All notable changes for `MuPDF.NET.PDF4LLM` are documented in this file. ## [1.28.0.1] - Synced with **pymupdf4llm** 1.28.0 and **MuPDF.NET 3.2.28.0**. - Renamed `OcrMode` values to match Python `OCRMode`: `SelectDropOld`, `SelectKeepOld`, `ForceDropOld`, `ForceKeepOld` (obsolete aliases kept for the old names). Default OCR behavior now preserves existing text (`SelectKeepOld`). - Layout pipeline uses raw layout results from the pymupdf-layout bridge (`ReadPageLayoutRaw`) and grid-based `GetTableDetails`; removed `table-fallback` / `TableFinder` table path from markdown/text output. Layout worker serializes grid predictions and table cells for JSON IPC. -- Requires **pymupdf-layout 1.28.0** (run `dotnet msbuild -t:PDF4LLMSetupLayoutPython` or `python PDF4LLM/scripts/setup_layout_python.py` to refresh the layout venv). +- Requires **pymupdf-layout 1.28.0** (run `dotnet msbuild -t:MuPDFNetPDF4LLMSetupLayoutPython` or `python MuPDF.NET.PDF4LLM/scripts/setup_layout_python.py` to refresh the layout venv). - Added `MakeOcrDecision`, `UpdateHeaderTags`, and improved `GetStyledText` (``, ``, ``, named font flags). - Added `Utils.Iou()` and simplified `TableToMarkdown(cells)` for layout grid tables. - `GetRawLines`: Type3 font check uses `StartsWith`; superscript text no longer wrapped in `[...]`. @@ -16,13 +16,13 @@ All notable changes for `PDF4LLM` are documented in this file. - Requires **MuPDF.NET 3.2.17.9** (pymupdf4llm **1.27.2.3** bind). - Ported pymupdf4llm OCR decision pipeline: `AnalyzePage`, `ComputeOcrFeatures`, and `PredictOcrProbability()` using the bundled `ocr_decision_model.onnx` (shipped as a separate NuGet file and copied to output via MSBuild targets). - Hardened the pymupdf-layout Python worker bridge: prefixed `RESULT` responses, redirect stray library stdout to stderr, skip non-JSON lines, and return `null` on parse failure so `Page.FindTables()` and layout parsing degrade gracefully instead of throwing. -- Improved pipeline status messages: print **Using pymupdf-layout** when the layout worker is active; show setup instructions when pymupdf-layout is missing (`dotnet msbuild -t:PDF4LLMSetupLayoutPython` or `python PDF4LLM/scripts/setup_layout_python.py`); show Tesseract install guidance when OCR is requested but tessdata is unavailable; print **Using Tesseract for OCR processing** when OCR is available. +- Improved pipeline status messages: print **Using pymupdf-layout** when the layout worker is active; show setup instructions when pymupdf-layout is missing (`dotnet msbuild -t:MuPDFNetPDF4LLMSetupLayoutPython` or `python MuPDF.NET.PDF4LLM/scripts/setup_layout_python.py`); show Tesseract install guidance when OCR is requested but tessdata is unavailable; print **Using Tesseract for OCR processing** when OCR is available. - Improved Tesseract discovery (`TESSDATA_PREFIX`, `tesseract --list-langs`) and removed the misleading static `TesseractApi` warning at class load. ## [1.27.2.17] -- Synced with **pymupdf4llm** 1.27.2.3: added `MarkdownToPdf()`, `GetKeyValues()`, and automatic OCR plugin selection (`SelectOcrFunction`) so `forceOcr=True` works without an explicit `ocrFunction` when Tesseract or RapidOCR is available. -- Added `PyMuPdfLayout` and the Python layout bridge (`PDF4LLMSetupLayoutPython` MSBuild target) for optional [pymupdf-layout](https://pypi.org/project/pymupdf-layout/) integration. -- Added `PdfExtractor.LoadAiAsync` and the `PDF4LLM.AI` pipeline (**net8.0**): chunk PDFs, generate embeddings, index vectors, and run Ask / Summarize / Search workflows via `Microsoft.Extensions.AI` (optional Azure OpenAI and Azure AI Search). +- Synced with **pymupdf4llm** 1.27.2.3: added `MuPDF4LLM.MarkdownToPdf()`, `MuPDF4LLM.GetKeyValues()`, and automatic OCR plugin selection (`SelectOcrFunction`) so `forceOcr=True` works without an explicit `ocrFunction` when Tesseract or RapidOCR is available. +- Added `PyMuPdfLayout` and the Python layout bridge (`MuPDFNetPDF4LLMSetupLayoutPython` MSBuild target) for optional [pymupdf-layout](https://pypi.org/project/pymupdf-layout/) integration. +- Added `MuPDF4LLM.LoadAiAsync` and the `MuPDF.NET.PDF4LLM.AI` pipeline (**net8.0**): chunk PDFs, generate embeddings, index vectors, and run Ask / Summarize / Search workflows via `Microsoft.Extensions.AI` (optional Azure OpenAI and Azure AI Search). - Raised the minimum **MuPDF.NET** dependency to **3.2.17** (improved table detection on rotated pages). - Centralized package version metadata in `VersionInfo` (sourced from `Versions.props` / `Artifex.Versions`). diff --git a/MuPDF.NET.PDF4LLM/Description.md b/MuPDF.NET.PDF4LLM/Description.md new file mode 100644 index 00000000..f4a605ff --- /dev/null +++ b/MuPDF.NET.PDF4LLM/Description.md @@ -0,0 +1,43 @@ +## About + +**MuPDF.NET.PDF4LLM** provides LLM/RAG helpers for [MuPDF.NET](https://www.nuget.org/packages/MuPDF.NET): PDF-to-Markdown conversion, layout parsing, and document structure analysis. It is designed for use with RAG (Retrieval-Augmented Generation) pipelines and integration with LLMs. + +**Documentation:** [docs.pdf4llm.com](https://docs.pdf4llm.com/) · [.NET installation](https://docs.pdf4llm.com/dotnet/getting-started/installation) + +This package extends MuPDF.NET with: + +- **PDF-to-Markdown** — Convert PDF pages to Markdown with layout awareness (tables, headers, images) +- **Layout parsing** — Extract document structure (pages, boxes, tables, images) as JSON or structured objects +- **LlamaIndex integration** — `PDFMarkdownReader` for compatibility with LlamaIndex document loading +- **OCR support** — Optional OCR for scanned or image-heavy pages +- **Form fields** — Extract key/value pairs from interactive PDF forms + +Install with `dotnet add package MuPDF.NET.PDF4LLM`. MuPDF.NET is installed automatically as a dependency. + +For Office/HWP documents (DOC/DOCX, XLS/XLSX, PPT/PPTX, HWP/HWPX), also install +[MuPDF.NET.Office](https://www.nuget.org/packages/MuPDF.NET.Office), call `MuPDFOffice.Unlock(...)`, +then pass those paths to `MuPDF4LLM.ToMarkdown` / `MuPDF4LLM.ToJson` / `MuPDF4LLM.ToText`. + +## PyMuPDF Layout (optional) + +AI-based page layout uses the Python package [pymupdf-layout](https://pypi.org/project/pymupdf-layout/) via an external worker. Install it once per machine: + +```bash +dotnet msbuild -t:MuPDFNetPDF4LLMSetupLayoutPython +``` + +This creates a per-user Python venv (Windows: `%LOCALAPPDATA%\MuPDF.NET.PDF4LLM\.venv-layout`, Linux/macOS: `~/.local/share/mupdf4llm.net/.venv-layout`) and installs pinned `pymupdf` / `pymupdf-layout` wheels. MuPDF.NET.PDF4LLM discovers that venv automatically. + +On Debian/Ubuntu, install system packages first: + +```bash +sudo apt install python3-venv python3-pip +``` + +Alternatively, install `pymupdf-layout` into any Python 3.10+ environment and set `MuPDF4LLM_NET_PYTHON` to that interpreter. If layout is unavailable, MuPDF.NET.PDF4LLM falls back to classic text extraction. + +Project-local venv: run `python path/to/setup_layout_python.py --venv .mupdf4llm-net-venv` in your project directory (also auto-discovered). + +## License and Copyright + +**MuPDF.NET.PDF4LLM** is part of MuPDF.NET and is available under the [Artifex Community License](https://github.com/ArtifexSoftware/MuPDF.NET/blob/main/LICENSE.md) and commercial license agreements. If you determine you cannot meet the requirements of the Artifex Community License, please [contact Artifex](https://artifex.com/contact/mupdf-net-inquiry.php) for more information regarding a commercial license. diff --git a/PDF4LLM/LICENSE.md b/MuPDF.NET.PDF4LLM/LICENSE.md similarity index 94% rename from PDF4LLM/LICENSE.md rename to MuPDF.NET.PDF4LLM/LICENSE.md index 51a51179..c217c9de 100644 --- a/PDF4LLM/LICENSE.md +++ b/MuPDF.NET.PDF4LLM/LICENSE.md @@ -2,7 +2,7 @@ **Version 1, 11th December 2024** -This **Artifex Community License** ("License") governs the use, reproduction, and distribution of **PDF4LLM** ("Software") for non-commercial use. By using, modifying, or distributing this Software, you accept the terms of this License. +This **Artifex Community License** ("License") governs the use, reproduction, and distribution of **MuPDF.NET.PDF4LLM** ("Software") for non-commercial use. By using, modifying, or distributing this Software, you accept the terms of this License. You may not use the software without an appropriate license, so if you cannot abide by all the terms of this license, you must [contact the Licensor](https://artifex.com/contact) to discuss obtaining a **Commercial Use License**. diff --git a/PDF4LLM/Layout/LayoutPythonPaths.cs b/MuPDF.NET.PDF4LLM/Layout/LayoutPythonPaths.cs similarity index 76% rename from PDF4LLM/Layout/LayoutPythonPaths.cs rename to MuPDF.NET.PDF4LLM/Layout/LayoutPythonPaths.cs index 3887301a..6a769694 100644 --- a/PDF4LLM/Layout/LayoutPythonPaths.cs +++ b/MuPDF.NET.PDF4LLM/Layout/LayoutPythonPaths.cs @@ -2,17 +2,17 @@ using System.Collections.Generic; using System.IO; -namespace PDF4LLM.Layout +namespace MuPDF.NET.PDF4LLM.Layout { /// Shared venv locations for the layout bridge Python bridge. internal static class LayoutPythonPaths { public const string VenvDirName = ".venv-layout"; - public const string ProjectVenvDirName = ".pdf4llm-venv"; + public const string ProjectVenvDirName = ".mupdf4llm-net-venv"; /// /// Per-user venv created by setup_layout_python.py - /// (%LOCALAPPDATA%\PDF4LLM\.venv-layout or ~/.local/share/pdf4llm/.venv-layout). + /// (%LOCALAPPDATA%\MuPDF.NET.PDF4LLM\.venv-layout or ~/.local/share/mupdf4llm.net/.venv-layout). /// public static string UserLocalVenvRoot() { @@ -20,11 +20,11 @@ public static string UserLocalVenvRoot() { string localAppData = Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData); - return Path.Combine(localAppData, "PDF4LLM", VenvDirName); + return Path.Combine(localAppData, "MuPDF.NET.PDF4LLM", VenvDirName); } string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - return Path.Combine(home, ".local", "share", "pdf4llm", VenvDirName); + return Path.Combine(home, ".local", "share", "mupdf4llm.net", VenvDirName); } public static string VenvPython(string venvRoot) => @@ -51,7 +51,7 @@ public static IEnumerable CandidateVenvRoots() foreach (string root in EnumerateSearchRoots()) { yield return Path.Combine(root, ProjectVenvDirName); - yield return Path.Combine(root, "PDF4LLM", VenvDirName); + yield return Path.Combine(root, "MuPDF.NET.PDF4LLM", VenvDirName); } } @@ -101,19 +101,19 @@ internal static void PrintSetupHelp() string venv = UserLocalVenvRoot(); Console.Error.WriteLine( - "PDF4LLM: pymupdf-layout is not installed; layout analysis is unavailable.\n" + + "MuPDF.NET.PDF4LLM: pymupdf-layout is not installed; layout analysis is unavailable.\n" + "\n" + "Install once per machine:\n" + - " dotnet msbuild -t:PDF4LLMSetupLayoutPython\n" + + " dotnet msbuild -t:MuPDFNetPDF4LLMSetupLayoutPython\n" + "\n" + - "Or run the setup script from the PDF4LLM package / repo:\n" + - " python PDF4LLM/scripts/setup_layout_python.py\n" + + "Or run the setup script from the MuPDF.NET.PDF4LLM package / repo:\n" + + " python MuPDF.NET.PDF4LLM/scripts/setup_layout_python.py\n" + "\n" + "On Debian/Ubuntu, install system packages first:\n" + " sudo apt install python3-venv python3-pip\n" + "\n" + $"The setup script creates a venv at: {venv}\n" + - "Or set PDF4LLM_PYTHON to a Python 3.10+ interpreter with pymupdf-layout installed."); + "Or set MuPDF4LLM_NET_PYTHON to a Python 3.10+ interpreter with pymupdf-layout installed."); } /// Warn once when the installed pymupdf-layout is older than expected. @@ -124,11 +124,11 @@ internal static void PrintVersionTooLowWarning(string requiredVersion, string in _versionMismatchWarningPrinted = true; Console.Error.WriteLine( - $"PDF4LLM warning: pymupdf-layout {installedVersion} is installed; " + - $"PDF4LLM {VersionInfo.Version} expects pymupdf-layout {requiredVersion} or newer. " + + $"MuPDF.NET.PDF4LLM warning: pymupdf-layout {installedVersion} is installed; " + + $"MuPDF.NET.PDF4LLM {VersionInfo.Version} expects pymupdf-layout {requiredVersion} or newer. " + "Continuing with the installed version.\n" + "To install the expected version:\n" + - " dotnet msbuild -t:PDF4LLMSetupLayoutPython"); + " dotnet msbuild -t:MuPDFNetPDF4LLMSetupLayoutPython"); } } } \ No newline at end of file diff --git a/PDF4LLM/Layout/PyMuPdfLayout.cs b/MuPDF.NET.PDF4LLM/Layout/PyMuPdfLayout.cs similarity index 87% rename from PDF4LLM/Layout/PyMuPdfLayout.cs rename to MuPDF.NET.PDF4LLM/Layout/PyMuPdfLayout.cs index e66c7dea..5412485f 100644 --- a/PDF4LLM/Layout/PyMuPdfLayout.cs +++ b/MuPDF.NET.PDF4LLM/Layout/PyMuPdfLayout.cs @@ -1,8 +1,8 @@ -namespace PDF4LLM.Layout +namespace MuPDF.NET.PDF4LLM.Layout { /// /// ONNX layout analysis via an external Python worker. - /// Run dotnet msbuild -t:PDF4LLMSetupLayoutPython once to install layout bridge. + /// Run dotnet msbuild -t:MuPDFNetPDF4LLMSetupLayoutPython once to install layout bridge. /// public static class PyMuPdfLayout { diff --git a/PDF4LLM/Layout/PyMuPdfLayoutBridge.cs b/MuPDF.NET.PDF4LLM/Layout/PyMuPdfLayoutBridge.cs similarity index 92% rename from PDF4LLM/Layout/PyMuPdfLayoutBridge.cs rename to MuPDF.NET.PDF4LLM/Layout/PyMuPdfLayoutBridge.cs index 5c2b8e4a..89f0c44d 100644 --- a/PDF4LLM/Layout/PyMuPdfLayoutBridge.cs +++ b/MuPDF.NET.PDF4LLM/Layout/PyMuPdfLayoutBridge.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace PDF4LLM.Layout +namespace MuPDF.NET.PDF4LLM.Layout { /// Python worker that runs layout ONNX inference per page. internal static class PyMuPdfLayoutBridge @@ -171,7 +171,7 @@ internal static void RefreshLayoutSnapshot(Document doc) lock (Gate) { string path = GetOrCreateLayoutSnapshotPath(doc); - doc.Save(path); + WriteLayoutSnapshotTo(doc, path); ClearLayoutCache(doc); } } @@ -191,10 +191,27 @@ static string GetOrCreateLayoutSnapshotPath(Document doc) => string tmp = Path.Combine( Path.GetTempPath(), "pdf4llm_layout_" + Guid.NewGuid().ToString("N") + ".pdf"); - d.Save(tmp); + WriteLayoutSnapshotTo(d, tmp); return tmp; }); + /// + /// Materialize a PDF path the external Python layout worker can reopen. + /// Non-PDF Office/HWP inputs are converted because the worker cannot inherit + /// MuPDF.NET.Office MuPDFOffice.Unlock() / SmartOffice handlers. + /// + static void WriteLayoutSnapshotTo(Document doc, string path) + { + if (doc.IsPDF) + { + doc.Save(path); + return; + } + + byte[] pdf = doc.ConvertToPdf(); + File.WriteAllBytes(path, pdf); + } + static bool ProbePythonLayout() { string version = TryQueryLayoutVersion(); @@ -414,7 +431,7 @@ static object GetLayoutForPage(Page page) catch (JsonException ex) { Console.Error.WriteLine( - "PDF4LLM: pymupdf.layout worker returned invalid JSON: " + "MuPDF.NET.PDF4LLM: pymupdf.layout worker returned invalid JSON: " + ex.Message); return null; } @@ -432,6 +449,12 @@ static string ResolveDocumentPath(Document doc) return snapshotPath; } + // The layout worker is a separate Python process and cannot inherit + // MuPDF.NET.Office `MuPDFOffice.Unlock()` / SmartOffice handlers. Named Office + // and HWP paths must be snapshotted to PDF before the worker reopens them. + if (!doc.IsPDF) + return GetOrCreateLayoutSnapshotPath(doc); + if (!string.IsNullOrEmpty(doc.Name) && !string.Equals(doc.Name, "", StringComparison.Ordinal) && File.Exists(doc.Name)) @@ -443,7 +466,7 @@ static string ResolveDocumentPath(Document doc) } static string PythonExecutable => - Environment.GetEnvironmentVariable("PDF4LLM_PYTHON") + Environment.GetEnvironmentVariable("MuPDF4LLM_NET_PYTHON") ?? Environment.GetEnvironmentVariable("PYTHON") ?? LayoutPythonPaths.TryResolveVenvPython() ?? "python"; diff --git a/PDF4LLM/LayoutActivation.cs b/MuPDF.NET.PDF4LLM/LayoutActivation.cs similarity index 79% rename from PDF4LLM/LayoutActivation.cs rename to MuPDF.NET.PDF4LLM/LayoutActivation.cs index 17ed14c6..9b4be105 100644 --- a/PDF4LLM/LayoutActivation.cs +++ b/MuPDF.NET.PDF4LLM/LayoutActivation.cs @@ -1,7 +1,7 @@ using MuPDF.NET; -using PDF4LLM.Layout; +using MuPDF.NET.PDF4LLM.Layout; -namespace PDF4LLM +namespace MuPDF.NET.PDF4LLM { internal static class LayoutActivation { diff --git a/PDF4LLM/PDF4LLM.csproj b/MuPDF.NET.PDF4LLM/MuPDF.NET.PDF4LLM.csproj similarity index 77% rename from PDF4LLM/PDF4LLM.csproj rename to MuPDF.NET.PDF4LLM/MuPDF.NET.PDF4LLM.csproj index 4b628c21..cf0a0538 100644 --- a/PDF4LLM/PDF4LLM.csproj +++ b/MuPDF.NET.PDF4LLM/MuPDF.NET.PDF4LLM.csproj @@ -1,18 +1,20 @@ - + - PDF4LLM + MuPDF.NET.PDF4LLM + MuPDF.NET.PDF4LLM netstandard2.0;net461;net472;net48;net5.0;net6.0;net7.0;net8.0 AnyCPU;x64;x86 True $(Platform) - . - $(ArtifexPDF4LLMVersion) - $(MSBuildProjectDirectory)\PDF4LLM.nuspec - Configuration=$(Configuration);version=$(ArtifexPDF4LLMVersion);PlatformFolder=$(PlatformFolder);mupdfNetMinVersion=$(ArtifexMuPDFNetVersion) + . + $(ArtifexMuPDFNetPDF4LLMVersion) + $(MSBuildProjectDirectory)\MuPDF.NET.PDF4LLM.nuspec + Configuration=$(Configuration);version=$(ArtifexMuPDFNetPDF4LLMVersion);PlatformFolder=$(PlatformFolder);mupdfNetMinVersion=$(ArtifexMuPDFNetVersion) + $(MSBuildThisFileDirectory)packages $(NoWarn);NU5110;NU5111 - + WINDOWS @@ -50,7 +52,7 @@ - + @@ -61,11 +63,6 @@ - - - - - <_LayoutRequirementsFile>$(MSBuildProjectDirectory)\scripts\requirements-layout.txt @@ -73,6 +70,6 @@ - + diff --git a/PDF4LLM/PDF4LLM.nuspec b/MuPDF.NET.PDF4LLM/MuPDF.NET.PDF4LLM.nuspec similarity index 84% rename from PDF4LLM/PDF4LLM.nuspec rename to MuPDF.NET.PDF4LLM/MuPDF.NET.PDF4LLM.nuspec index 39940ab3..ed1dd59f 100644 --- a/PDF4LLM/PDF4LLM.nuspec +++ b/MuPDF.NET.PDF4LLM/MuPDF.NET.PDF4LLM.nuspec @@ -1,7 +1,7 @@ - PDF4LLM + MuPDF.NET.PDF4LLM $version$ Artifex Software Inc. Artifex Software Inc. @@ -9,7 +9,7 @@ LICENSE.md logo.png Description.md - https://github.com/ArtifexSoftware/MuPDF.NET + https://docs.pdf4llm.com/ LLM/RAG helpers for MuPDF.NET: PDF-to-Markdown conversion, layout parsing, document structure analysis. Designed for use with RAG pipelines and integration with LLMs. LLM/RAG helpers for PDF processing: Markdown conversion, layout analysis, multi-column detection. MuPDF PDF LLM RAG Markdown LlamaIndex document-processing PDF-to-text @@ -64,19 +64,19 @@ - - - - - - - - + + + + + + + + - + diff --git a/PDF4LLM/PdfExtractor.Ai.cs b/MuPDF.NET.PDF4LLM/MuPDF4LLM.Ai.cs similarity index 88% rename from PDF4LLM/PdfExtractor.Ai.cs rename to MuPDF.NET.PDF4LLM/MuPDF4LLM.Ai.cs index 43bc2d5f..3d321715 100644 --- a/PDF4LLM/PdfExtractor.Ai.cs +++ b/MuPDF.NET.PDF4LLM/MuPDF4LLM.Ai.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using PDF4LLM.AI; -using PDF4LLM.AI.Options; +using MuPDF.NET.PDF4LLM.AI; +using MuPDF.NET.PDF4LLM.AI.Options; -namespace PDF4LLM +namespace MuPDF.NET.PDF4LLM { - public static partial class PdfExtractor + public static partial class MuPDF4LLM { /// /// Load, chunk, embed, and index PDFs for AI/RAG workflows diff --git a/PDF4LLM/PdfExtractor.MarkdownToPdf.cs b/MuPDF.NET.PDF4LLM/MuPDF4LLM.MarkdownToPdf.cs similarity index 98% rename from PDF4LLM/PdfExtractor.MarkdownToPdf.cs rename to MuPDF.NET.PDF4LLM/MuPDF4LLM.MarkdownToPdf.cs index 981d3c92..ab871755 100644 --- a/PDF4LLM/PdfExtractor.MarkdownToPdf.cs +++ b/MuPDF.NET.PDF4LLM/MuPDF4LLM.MarkdownToPdf.cs @@ -3,9 +3,9 @@ using Markdig; using MuPDF.NET; -namespace PDF4LLM +namespace MuPDF.NET.PDF4LLM { - public static partial class PdfExtractor + public static partial class MuPDF4LLM { /// /// Return a PDF document for a Markdown source string. diff --git a/PDF4LLM/PdfExtractor.UseLayout.cs b/MuPDF.NET.PDF4LLM/MuPDF4LLM.UseLayout.cs similarity index 93% rename from PDF4LLM/PdfExtractor.UseLayout.cs rename to MuPDF.NET.PDF4LLM/MuPDF4LLM.UseLayout.cs index d5ad20e2..22213272 100644 --- a/PDF4LLM/PdfExtractor.UseLayout.cs +++ b/MuPDF.NET.PDF4LLM/MuPDF4LLM.UseLayout.cs @@ -1,10 +1,10 @@ using System; using MuPDF.NET; -using PDF4LLM.Helpers; +using MuPDF.NET.PDF4LLM.Helpers; -namespace PDF4LLM +namespace MuPDF.NET.PDF4LLM { - public static partial class PdfExtractor + public static partial class MuPDF4LLM { /// Whether a layout provider is registered. public static bool LayoutAvailable => Page.GetLayoutProvider != null; diff --git a/PDF4LLM/PdfExtractor.cs b/MuPDF.NET.PDF4LLM/MuPDF4LLM.cs similarity index 96% rename from PDF4LLM/PdfExtractor.cs rename to MuPDF.NET.PDF4LLM/MuPDF4LLM.cs index 5aed7c66..60e25b59 100644 --- a/PDF4LLM/PdfExtractor.cs +++ b/MuPDF.NET.PDF4LLM/MuPDF4LLM.cs @@ -1,23 +1,23 @@ using System; using System.Collections.Generic; using MuPDF.NET; -using PDF4LLM.Helpers; -using PDF4LLM.Layout; -using PDF4LLM.Llama; +using MuPDF.NET.PDF4LLM.Helpers; +using MuPDF.NET.PDF4LLM.Layout; +using MuPDF.NET.PDF4LLM.Llama; -namespace PDF4LLM +namespace MuPDF.NET.PDF4LLM { /// PDF extraction and layout-to-markdown API. - public static partial class PdfExtractor + public static partial class MuPDF4LLM { - static PdfExtractor() + static MuPDF4LLM() { string actual = Artifex.Versions.MuPDF.Split('-')[0]; string required = VersionInfo.RequiredMuPdf.Split('-')[0]; if (!string.Equals(actual, required, StringComparison.Ordinal)) { throw new InvalidOperationException( - $"PDF4LLM {VersionInfo.Version} requires MuPDF {VersionInfo.RequiredMuPdf}, " + + $"MuPDF.NET.PDF4LLM {VersionInfo.Version} requires MuPDF {VersionInfo.RequiredMuPdf}, " + $"but MuPDF.NET reports {Artifex.Versions.MuPDF}."); } @@ -56,7 +56,7 @@ public static PDFMarkdownReader LlamaMarkdownReader( } /// Convert a document to Markdown. - /// PDF document to convert (required for in-memory or non-path sources). + /// Document to convert (required for in-memory or non-path sources). /// When , include page-header regions in the output. /// When , include page-footer regions in the output. /// 0-based page indices to process; processes all pages. @@ -165,8 +165,8 @@ public static string ToMarkdown( pageChunks: pageChunks); } - /// Convert a PDF file path to Markdown. - /// Path to the PDF file. + /// Convert a supported document path to Markdown. + /// Path to the document. /// When , include page-header regions in the output. /// When , include page-footer regions in the output. /// 0-based page indices to process; processes all pages. @@ -241,7 +241,7 @@ public static string ToMarkdown( } /// Convert a document to layout JSON. - /// PDF document to convert. + /// Document to convert. /// Resolution in dots per inch for extracted images. /// Image file extension/format (for example png). /// Folder for written images when is . @@ -295,8 +295,8 @@ public static string ToJson( return parsedDoc.ToJson(showProgress: showProgress); } - /// Convert a PDF file path to layout JSON. - /// Path to the PDF file. + /// Convert a supported document path to layout JSON. + /// Path to the document. /// Resolution in dots per inch for extracted images. /// Image file extension/format (for example png). /// Folder for written images when is . @@ -347,7 +347,7 @@ public static string ToJson( } /// Convert a document to plain text. - /// PDF document to convert. + /// Document to convert. /// Logical file name stored in the parsed model metadata. /// When , include page-header regions in the output. /// When , include page-footer regions in the output. @@ -412,8 +412,8 @@ public static string ToText( tableMinColWidth: tableMinColWidth); } - /// Convert a PDF file path to plain text. - /// Path to the PDF file. + /// Convert a supported document path to plain text. + /// Path to the document. /// Logical file name stored in the parsed model metadata. /// When , include page-header regions in the output. /// When , include page-footer regions in the output. @@ -473,7 +473,7 @@ public static string ToText( } /// Parse a document into a structured layout model. - /// PDF document to parse. + /// Document to parse. /// Logical file name stored in the parsed model metadata. /// Resolution in dots per inch for extracted images. /// Image file extension/format (for example png). @@ -541,6 +541,10 @@ public static Dictionary> GetKeyValues( Document doc, bool includeXrefs = false) { + if (doc == null) + throw new ArgumentNullException(nameof(doc)); + if (doc.IsClosed || !doc.IsPDF) + return new Dictionary>(); if (doc.IsFormPDF != 0) return Helpers.Utils.ExtractFormFieldsWithPages(doc, includeXrefs); return new Dictionary>(); diff --git a/MuPDF.NET.PDF4LLM/README.md b/MuPDF.NET.PDF4LLM/README.md new file mode 100644 index 00000000..48e34cbd --- /dev/null +++ b/MuPDF.NET.PDF4LLM/README.md @@ -0,0 +1,213 @@ +# MuPDF.NET.PDF4LLM + +LLM/RAG helpers for [MuPDF.NET](https://www.nuget.org/packages/MuPDF.NET): convert PDFs to Markdown or plain text, analyze page layout, export structure as JSON, and load documents for retrieval pipelines. + +The public API lives in the **`MuPDF.NET.PDF4LLM`** namespace. The main entry point is the static class **`MuPDF4LLM`**. + +**API naming:** Public members use C# conventions — PascalCase methods and properties, camelCase parameters (for example `writeImages`, `includeXrefs`, `useLayout`). Python-style names from [pymupdf4llm](https://pypi.org/project/pymupdf4llm/) appear only in internal port alignment and in the optional Python layout worker; customer-facing docs and IntelliSense use the C# names below. + +## Documentation + +| Resource | URL | +|----------|-----| +| 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/ | + +## Installation + +```bash +dotnet add package MuPDF.NET.PDF4LLM +``` + +[MuPDF.NET](https://www.nuget.org/packages/MuPDF.NET) is installed automatically as a dependency — you do not need to add it separately. If your project already references MuPDF.NET, add `MuPDF.NET.PDF4LLM` anyway; NuGet will resolve a compatible MuPDF.NET version. + +## PyMuPDF Layout (recommended) + +AI-based layout analysis uses the Python package [pymupdf-layout](https://pypi.org/project/pymupdf-layout/) through a small external worker process. When layout is available, `MuPDF4LLM` enables it automatically on first use. + +### One-time setup (NuGet consumers) + +Requires **Python 3.10+** on `PATH`. From your project directory: + +```bash +dotnet msbuild -t:MuPDFNetPDF4LLMSetupLayoutPython +``` + +This creates a per-user venv and installs pinned `pymupdf` / `pymupdf-layout` wheels: + +| OS | Venv location | +|----|---------------| +| Windows | `%LOCALAPPDATA%\MuPDF.NET.PDF4LLM\.venv-layout` | +| Linux / macOS | `~/.local/share/mupdf4llm.net/.venv-layout` | + +MuPDF.NET.PDF4LLM discovers that venv automatically. No environment variables are required. + +### Alternatives + +- Set **`MuPDF4LLM_NET_PYTHON`** to any Python interpreter that has `pymupdf-layout` installed. +- Project-local venv (also auto-discovered): + + ```bash + python path/to/setup_layout_python.py --venv .mupdf4llm-net-venv + ``` + +- Monorepo / source checkout: + + ```bash + python MuPDF.NET.PDF4LLM/scripts/setup_layout_python.py + ``` + +If layout is not installed, MuPDF.NET.PDF4LLM falls back to classic MuPDF text extraction. Check availability at runtime: + +```csharp +using MuPDF.NET.PDF4LLM; +using MuPDF.NET.PDF4LLM.Layout; + +bool layoutReady = PyMuPdfLayout.IsAvailable; // Python import probe +bool layoutActive = MuPDF4LLM.LayoutAvailable; // provider registered +``` + +## Quick start + +```csharp +using MuPDF.NET; +using MuPDF.NET.PDF4LLM; + +// Path or open Document — both work +string markdown = MuPDF4LLM.ToMarkdown(@"C:\docs\report.pdf"); + +using Document doc = new Document("report.pdf"); +string text = MuPDF4LLM.ToText(doc); +string json = MuPDF4LLM.ToJson(doc); +var parsed = MuPDF4LLM.ParseDocument(doc); +var formData = MuPDF4LLM.GetKeyValues(doc); +``` + +### Selected pages and images + +```csharp +string md = MuPDF4LLM.ToMarkdown( + doc, + pages: new List { 0, 1, 2 }, + writeImages: true, + imagePath: @"C:\output\images", + imageFormat: "png"); +``` + +### Interactive form fields + +```csharp +// includeXrefs: true adds each widget's PDF xref (for Page.LoadWidget) +var fields = MuPDF4LLM.GetKeyValues(doc, includeXrefs: true); +foreach (var kv in fields) + Console.WriteLine($"{kv.Key}: {kv.Value["value"]}"); +``` + +### LlamaIndex-style loading + +```csharp +var reader = MuPDF4LLM.LlamaMarkdownReader(); +var docs = reader.LoadData("report.pdf", extraInfo: new Dictionary()); +foreach (var d in docs) + Console.WriteLine($"Page {d.ExtraInfo["page"]}: {d.Text}"); +``` + +### Markdown to PDF + +```csharp +using Document pdf = MuPDF4LLM.MarkdownToPdf(@"C:\docs\readme.md"); +pdf.Save("readme.pdf"); +``` + +### Layout on / off + +```csharp +MuPDF4LLM.SetUseLayout(useLayout: true); // default when pymupdf-layout is installed +MuPDF4LLM.SetUseLayout(useLayout: false); // legacy header detection (IdentifyHeaders, TocHeaders) +``` + +### Optional Office / HWP support + +`MuPDF.NET.PDF4LLM` does not bundle commercial Office natives. Install the sibling package +[MuPDF.NET.Office](https://www.nuget.org/packages/MuPDF.NET.Office), unlock once, +then pass Office/HWP paths to the same extractors: + +```csharp +using MuPDF.NET.Office; +using MuPDF.NET.PDF4LLM; + +MuPDFOffice.Unlock("YOUR-LICENSE-OR-TRIAL-KEY", fontPathAuto: true); + +string markdown = MuPDF4LLM.ToMarkdown(@"C:\docs\report.docx"); +string json = MuPDF4LLM.ToJson(@"C:\docs\report.hwpx"); +string text = MuPDF4LLM.ToText(@"C:\docs\report.pptx"); +``` + +Supported Office formats follow MuPDF.NET.Office (DOC/DOCX, XLS/XLSX, PPT/PPTX, +HWP/HWPX, and related SmartOffice inputs). Layout mode still works: MuPDF.NET.PDF4LLM +snapshots non-PDF pages to a temporary PDF for the external layout worker while +keeping the original document for extraction metadata. + +### OCR + +When layout mode is active, OCR is selected automatically via `LayoutParseHelpers.SelectOcrFunction()` when Tesseract or RapidOCR is available. Control behavior with `useOcr`, `forceOcr`, `ocrLanguage`, and optional `ocrFunction`: + +```csharp +using MuPDF.NET.PDF4LLM.Ocr; + +string md = MuPDF4LLM.ToMarkdown( + doc, + useOcr: true, + forceOcr: false, + ocrLanguage: "eng"); +``` + +`OcrMode` values (layout pipeline): `Never`, `SelectDropOld`, `SelectKeepOld` (default), `ForceDropOld`, `ForceKeepOld`. + +## API overview + +| Member | Description | +|--------|-------------| +| `MuPDF4LLM.ToMarkdown()` | Document → Markdown (tables, headers, images) | +| `MuPDF4LLM.ToText()` | Document → plain text with the same layout pipeline | +| `MuPDF4LLM.ToJson()` | Layout structure as JSON | +| `MuPDF4LLM.ParseDocument()` | `ParsedDocument` with pages, boxes, tables, images | +| `MuPDF4LLM.GetKeyValues()` | Interactive form field names, values, and locations | +| `MuPDF4LLM.MarkdownToPdf()` | Markdown file → `Document` via MuPDF Story | +| `MuPDF4LLM.LlamaMarkdownReader()` | LlamaIndex-compatible page loader | +| `MuPDF4LLM.SetUseLayout()` | Enable or disable the layout pipeline | +| `MuPDF4LLM.SetLayoutProvider()` | Plug in a custom `Func` layout source | +| `MuPDF4LLM.LoadAiAsync()` | **net8.0 only** — chunk, embed, and index PDFs for RAG (`MuPDF.NET.PDF4LLM.AI`) | + +Lower-level layout control: **`MuPDF.NET.PDF4LLM.Layout.PyMuPdfLayout`** (`Activate`, `Deactivate`, `IsAvailable`, `Version`). + +Additional public helpers: `LayoutParseHelpers.ReadPageLayoutRaw`, `LayoutParseHelpers.SelectOcrFunction`, `GetTextLines.GetRawLines`, `Utils.Iou`, `Utils.TableToMarkdown`. + +## Common options + +`MuPDF4LLM.ToMarkdown`, `MuPDF4LLM.ToText`, and `MuPDF4LLM.ToJson` accept optional parameters including: + +| Parameter | Purpose | +|-----------|---------| +| `pages` | Restrict to specific pages (0-based) | +| `writeImages` / `embedImages` | Save image files or embed as base64 | +| `imagePath`, `imageFormat`, `filename` | Image output location and naming | +| `useOcr`, `ocrLanguage`, `forceOcr`, `ocrFunction` | OCR for scanned pages | +| `forceText` | Extract text even from picture regions (layout mode) | +| `pageChunks`, `pageSeparators` | Chunked or separated page output | +| `showProgress` | Log processing progress | +| `header`, `footer` | Include page header/footer text (layout mode) | + +`MuPDF4LLM.GetKeyValues` also accepts `includeXrefs` to include widget xref numbers in the result. + +## Requirements + +- **.NET:** netstandard2.0, net461, net472, net48, net5.0–net8.0 +- **MuPDF.NET:** 3.28.0 or newer (MuPDF bind **1.28.0** must match `MuPDF4LLM` at runtime) +- **Layout (optional):** Python 3.10+ with [pymupdf-layout](https://pypi.org/project/pymupdf-layout/) 1.28.0 +- **AI/RAG helpers:** net8.0 + `Microsoft.Extensions.AI` (included in the net8.0 package build) + +## License + +MuPDF.NET.PDF4LLM is part of MuPDF.NET and is available under the [Artifex Community License](https://github.com/ArtifexSoftware/MuPDF.NET/blob/main/LICENSE.md) and commercial license agreements. For commercial licensing, [contact Artifex](https://artifex.com/contact/mupdf-net-inquiry.php). diff --git a/MuPDF.NET.PDF4LLM/VersionInfo.cs b/MuPDF.NET.PDF4LLM/VersionInfo.cs new file mode 100644 index 00000000..a96acf0f --- /dev/null +++ b/MuPDF.NET.PDF4LLM/VersionInfo.cs @@ -0,0 +1,15 @@ +namespace MuPDF.NET.PDF4LLM +{ + /// MuPDF.NET.PDF4LLM package version metadata. + public static class VersionInfo + { + /// MuPDF.NET.PDF4LLM NuGet package version. + public const string Version = BuildVersions.Package; + + /// Required pymupdf-layout PyPI package version for the layout bridge. + public const string RequiredPyMuPDFLayout = BuildVersions.RequiredPyMuPDFLayout; + + /// Native MuPDF version expected from MuPDF.NET. + public const string RequiredMuPdf = BuildVersions.RequiredMuPdf; + } +} diff --git a/MuPDF.NET.PDF4LLM/build/MuPDF.NET.PDF4LLM.targets b/MuPDF.NET.PDF4LLM/build/MuPDF.NET.PDF4LLM.targets new file mode 100644 index 00000000..71a886a3 --- /dev/null +++ b/MuPDF.NET.PDF4LLM/build/MuPDF.NET.PDF4LLM.targets @@ -0,0 +1,62 @@ + + + $(MSBuildThisFileDirectory)layout\ + $(MSBuildThisFileDirectory)..\scripts\ + $(MSBuildThisFileDirectory)..\ocr\ocr_decision_model.onnx + $(MSBuildThisFileDirectory)ocr\ocr_decision_model.onnx + + + + + + + + + + <_MuPDFNetPDF4LLMNetCoreVersion>$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)')) + <_MuPDFNetPDF4LLMSupportsRapidOcr>$([MSBuild]::VersionGreaterThanOrEquals($(_MuPDFNetPDF4LLMNetCoreVersion), 8.0)) + + + <_MuPDFNetPDF4LLMSupportsRapidOcr>false + + + <_MuPDFNetPDF4LLMRapidOcrModelsSource Condition="'$(PkgRapidOcrNet)' != ''">$(PkgRapidOcrNet)\models\v5 + <_MuPDFNetPDF4LLMRapidOcrModelsSource Condition="'$(_MuPDFNetPDF4LLMRapidOcrModelsSource)' == '' and '$(NuGetPackageRoot)' != ''">$(NuGetPackageRoot)rapidocrnet\2.0.0\models\v5 + + + + + + + + + + + + + + + + + + + + diff --git a/PDF4LLM/helpers/DocumentLayout.cs b/MuPDF.NET.PDF4LLM/helpers/DocumentLayout.cs similarity index 99% rename from PDF4LLM/helpers/DocumentLayout.cs rename to MuPDF.NET.PDF4LLM/helpers/DocumentLayout.cs index fbe601bd..c9d2d8b5 100644 --- a/PDF4LLM/helpers/DocumentLayout.cs +++ b/MuPDF.NET.PDF4LLM/helpers/DocumentLayout.cs @@ -4,13 +4,13 @@ using System.Linq; using System.Text; using MuPDF.NET; -using PDF4LLM.Layout; -using PDF4LLM.Ocr; +using MuPDF.NET.PDF4LLM.Layout; +using MuPDF.NET.PDF4LLM.Ocr; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using mupdf; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { /// Optional per-page OCR hook; mutates the page text layer in place. /// Page to OCR. @@ -1290,7 +1290,7 @@ public static List NormalizePageIndices(int pageCount, List pages) } /// - /// Parse a PDF document into a with per-page layout boxes. + /// Parse a supported document into a with per-page layout boxes. /// /// /// Runs optional OCR, extracts text and layout information, orders layout boxes for reading, @@ -1298,7 +1298,7 @@ public static List NormalizePageIndices(int pageCount, List pages) /// Uses native when available; otherwise layout regions /// are derived from text blocks plus table detection. /// - /// PDF document to parse. + /// Document to parse. /// Logical file name stored in the parsed model metadata. /// Resolution in dots per inch for extracted images. /// Image file extension/format (for example png). diff --git a/PDF4LLM/helpers/ExtendedSpan.cs b/MuPDF.NET.PDF4LLM/helpers/ExtendedSpan.cs similarity index 96% rename from PDF4LLM/helpers/ExtendedSpan.cs rename to MuPDF.NET.PDF4LLM/helpers/ExtendedSpan.cs index d129eb52..1ecce3cf 100644 --- a/PDF4LLM/helpers/ExtendedSpan.cs +++ b/MuPDF.NET.PDF4LLM/helpers/ExtendedSpan.cs @@ -2,7 +2,7 @@ using MuPDF.NET; using Newtonsoft.Json; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { /// Extended text span with layout metadata. public class ExtendedSpan diff --git a/PDF4LLM/helpers/GetTextLines.cs b/MuPDF.NET.PDF4LLM/helpers/GetTextLines.cs similarity index 99% rename from PDF4LLM/helpers/GetTextLines.cs rename to MuPDF.NET.PDF4LLM/helpers/GetTextLines.cs index 7bef24db..bc8ae48e 100644 --- a/PDF4LLM/helpers/GetTextLines.cs +++ b/MuPDF.NET.PDF4LLM/helpers/GetTextLines.cs @@ -3,7 +3,7 @@ using System.Linq; using MuPDF.NET; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { /// /// Represents a line with its rectangle and spans diff --git a/PDF4LLM/helpers/LayoutParseHelpers.cs b/MuPDF.NET.PDF4LLM/helpers/LayoutParseHelpers.cs similarity index 98% rename from PDF4LLM/helpers/LayoutParseHelpers.cs rename to MuPDF.NET.PDF4LLM/helpers/LayoutParseHelpers.cs index 6fc2e7be..6b4e05ec 100644 --- a/PDF4LLM/helpers/LayoutParseHelpers.cs +++ b/MuPDF.NET.PDF4LLM/helpers/LayoutParseHelpers.cs @@ -3,10 +3,10 @@ using System.Globalization; using System.Linq; using MuPDF.NET; -using PDF4LLM.Ocr; +using MuPDF.NET.PDF4LLM.Ocr; using Newtonsoft.Json.Linq; using mupdf; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { /// /// Layout tuples (x0, y0, x1, y1, "class"), mutable during clean_* passes. @@ -121,10 +121,10 @@ public static void TryRemovePdfStructTreeRoot(Document doc) /// public static void LogLayoutStatus() { - if (global::PDF4LLM.Layout.PyMuPdfLayout.IsActivated + if (global::MuPDF.NET.PDF4LLM.Layout.PyMuPdfLayout.IsActivated && Page.GetLayoutProvider != null) { - string version = global::PDF4LLM.Layout.PyMuPdfLayout.Version; + string version = global::MuPDF.NET.PDF4LLM.Layout.PyMuPdfLayout.Version; if (!string.IsNullOrEmpty(version)) Console.WriteLine($"Using pymupdf-layout ({version}) for document processing."); else @@ -132,7 +132,7 @@ public static void LogLayoutStatus() return; } - global::PDF4LLM.Layout.LayoutPythonPaths.PrintSetupHelp(); + global::MuPDF.NET.PDF4LLM.Layout.LayoutPythonPaths.PrintSetupHelp(); } /// Print once how to install Tesseract OCR and tessdata. @@ -143,7 +143,7 @@ public static void PrintTesseractSetupHelp() _tesseractSetupHelpPrinted = true; Console.Error.WriteLine( - "PDF4LLM: Tesseract OCR is not available; OCR will be disabled.\n" + + "MuPDF.NET.PDF4LLM: Tesseract OCR is not available; OCR will be disabled.\n" + "\n" + "Install Tesseract OCR and language data (tessdata), then ensure one of:\n" + " - Tesseract is on PATH (verify with: tesseract --list-langs)\n" + @@ -221,7 +221,7 @@ public static OcrPageFunction SelectOcrFunction() { Console.WriteLine("Using RapidOCR and Tesseract for OCR processing."); return (page, ocrDpi, ocrLanguage, keepOcrText) => - global::PDF4LLM.Ocr.RapidTessApi.ExecOcr( + global::MuPDF.NET.PDF4LLM.Ocr.RapidTessApi.ExecOcr( page, dpi: ocrDpi, language: ocrLanguage, keepOcrText: keepOcrText); } @@ -229,13 +229,13 @@ public static OcrPageFunction SelectOcrFunction() { Console.WriteLine("Using PaddleOCR and Tesseract for OCR processing."); return (page, ocrDpi, ocrLanguage, keepOcrText) => - global::PDF4LLM.Ocr.PaddleTessApi.ExecOcr( + global::MuPDF.NET.PDF4LLM.Ocr.PaddleTessApi.ExecOcr( page, dpi: ocrDpi, language: ocrLanguage, keepOcrText: keepOcrText); } Console.WriteLine("Using Tesseract for OCR processing."); return (page, ocrDpi, ocrLanguage, keepOcrText) => - global::PDF4LLM.Ocr.TesseractApi.ExecOcr( + global::MuPDF.NET.PDF4LLM.Ocr.TesseractApi.ExecOcr( page, dpi: ocrDpi, language: ocrLanguage, keepOcrText: keepOcrText); } @@ -243,7 +243,7 @@ public static OcrPageFunction SelectOcrFunction() { Console.WriteLine("Using RapidOCR for OCR processing."); return (page, ocrDpi, ocrLanguage, keepOcrText) => - global::PDF4LLM.Ocr.RapidOcrApi.ExecOcr( + global::MuPDF.NET.PDF4LLM.Ocr.RapidOcrApi.ExecOcr( page, dpi: ocrDpi, language: ocrLanguage, keepOcrText: keepOcrText); } @@ -251,7 +251,7 @@ public static OcrPageFunction SelectOcrFunction() { Console.WriteLine("Using PaddleOCR for OCR processing."); return (page, ocrDpi, ocrLanguage, keepOcrText) => - global::PDF4LLM.Ocr.PaddleOcrApi.ExecOcr( + global::MuPDF.NET.PDF4LLM.Ocr.PaddleOcrApi.ExecOcr( page, dpi: ocrDpi, language: ocrLanguage, keepOcrText: keepOcrText); } diff --git a/PDF4LLM/helpers/LayoutTabulate.cs b/MuPDF.NET.PDF4LLM/helpers/LayoutTabulate.cs similarity index 99% rename from PDF4LLM/helpers/LayoutTabulate.cs rename to MuPDF.NET.PDF4LLM/helpers/LayoutTabulate.cs index 675f6295..d1b72fee 100644 --- a/PDF4LLM/helpers/LayoutTabulate.cs +++ b/MuPDF.NET.PDF4LLM/helpers/LayoutTabulate.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { /// Plain-text table formatting for document export. internal static class LayoutTabulate diff --git a/PDF4LLM/helpers/MuPdfRag.cs b/MuPDF.NET.PDF4LLM/helpers/MuPdfRag.cs similarity index 99% rename from PDF4LLM/helpers/MuPdfRag.cs rename to MuPDF.NET.PDF4LLM/helpers/MuPdfRag.cs index ace4586d..9c1850b5 100644 --- a/PDF4LLM/helpers/MuPdfRag.cs +++ b/MuPDF.NET.PDF4LLM/helpers/MuPdfRag.cs @@ -6,7 +6,7 @@ using System.Text.RegularExpressions; using MuPDF.NET; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { /// /// Header identification based on font sizes @@ -225,7 +225,7 @@ public class Parameters /// /// Main markdown conversion utilities (legacy RAG path). - /// delegates here when UseLayout is false. + /// delegates here when UseLayout is false. /// Kept internal until a supported public surface is defined; not part of the public API contract. /// internal static class MuPdfRag @@ -396,7 +396,7 @@ public static string ToMarkdown( filename = doc.Name; // Handle form PDFs and documents with annotations - if (doc.IsFormPDF > 0 || (doc.IsPDF && doc.HasAnnots())) + if (doc.IsPDF && (doc.IsFormPDF > 0 || doc.HasAnnots())) { doc.Bake(); } diff --git a/PDF4LLM/helpers/MultiColumn.cs b/MuPDF.NET.PDF4LLM/helpers/MultiColumn.cs similarity index 99% rename from PDF4LLM/helpers/MultiColumn.cs rename to MuPDF.NET.PDF4LLM/helpers/MultiColumn.cs index 8a8951e0..d2317819 100644 --- a/PDF4LLM/helpers/MultiColumn.cs +++ b/MuPDF.NET.PDF4LLM/helpers/MultiColumn.cs @@ -3,7 +3,7 @@ using System.Linq; using MuPDF.NET; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { /// Multi-column text layout helpers. public static class MultiColumn diff --git a/PDF4LLM/helpers/Progress.cs b/MuPDF.NET.PDF4LLM/helpers/Progress.cs similarity index 98% rename from PDF4LLM/helpers/Progress.cs rename to MuPDF.NET.PDF4LLM/helpers/Progress.cs index f0afeab3..66a49f39 100644 --- a/PDF4LLM/helpers/Progress.cs +++ b/MuPDF.NET.PDF4LLM/helpers/Progress.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { public class _ProgressBar : IEnumerator { diff --git a/PDF4LLM/helpers/Utils.cs b/MuPDF.NET.PDF4LLM/helpers/Utils.cs similarity index 99% rename from PDF4LLM/helpers/Utils.cs rename to MuPDF.NET.PDF4LLM/helpers/Utils.cs index fc40c969..e5b6cf25 100644 --- a/PDF4LLM/helpers/Utils.cs +++ b/MuPDF.NET.PDF4LLM/helpers/Utils.cs @@ -7,7 +7,7 @@ using MuPDF.NET; using mupdf; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { /// Utility functions for PDF processing and layout analysis. public static partial class Utils @@ -86,7 +86,7 @@ public static partial class Utils public static Dictionary> ExtractFormFieldsWithPages(Document doc, bool includeXrefs = false) { var result = new Dictionary>(); - if (doc == null || doc.IsClosed) + if (doc == null || doc.IsClosed || !doc.IsPDF) return result; PdfDocument pdf = null; @@ -765,7 +765,7 @@ public static Dictionary AnalyzePage( Page page, List blocks = null, bool replaceOcr = false) => - global::PDF4LLM.Ocr.AnalyzePage.Analyze(page, blocks, replaceOcr: replaceOcr); + global::MuPDF.NET.PDF4LLM.Ocr.AnalyzePage.Analyze(page, blocks, replaceOcr: replaceOcr); /// Compute intersection over union of two rectangles. public static float Iou(Rect r1, Rect r2) => diff --git a/PDF4LLM/helpers/UtilsTableCells.cs b/MuPDF.NET.PDF4LLM/helpers/UtilsTableCells.cs similarity index 99% rename from PDF4LLM/helpers/UtilsTableCells.cs rename to MuPDF.NET.PDF4LLM/helpers/UtilsTableCells.cs index 15943ab0..fdc5bc7c 100644 --- a/PDF4LLM/helpers/UtilsTableCells.cs +++ b/MuPDF.NET.PDF4LLM/helpers/UtilsTableCells.cs @@ -5,7 +5,7 @@ using MuPDF.NET; using mupdf; -namespace PDF4LLM.Helpers +namespace MuPDF.NET.PDF4LLM.Helpers { /// Table cell extraction and markdown formatting. public static partial class Utils diff --git a/PDF4LLM/llama/PDFMarkdownReader.cs b/MuPDF.NET.PDF4LLM/llama/PDFMarkdownReader.cs similarity index 94% rename from PDF4LLM/llama/PDFMarkdownReader.cs rename to MuPDF.NET.PDF4LLM/llama/PDFMarkdownReader.cs index 85772346..00f91000 100644 --- a/PDF4LLM/llama/PDFMarkdownReader.cs +++ b/MuPDF.NET.PDF4LLM/llama/PDFMarkdownReader.cs @@ -3,9 +3,9 @@ using System.IO; using MuPDF.NET; -namespace PDF4LLM.Llama +namespace MuPDF.NET.PDF4LLM.Llama { - /// Read PDF files and emit LlamaIndex documents. + /// Read supported documents and emit LlamaIndex documents. public class PDFMarkdownReader { public Func, Dictionary> MetaFilter { get; set; } @@ -18,9 +18,9 @@ public PDFMarkdownReader(Func, Dictionary - /// Loads list of documents from PDF file and also accepts extra information in dict format. + /// Loads documents from a supported file and also accepts extra information in dict format. /// - /// Path to the PDF file (string or path-like object). + /// Path to the document (string or path-like object). /// Additional metadata merged into each page document's . /// Optional keyword arguments forwarded to the Markdown extractor. public List LoadData( diff --git a/PDF4LLM/logo.png b/MuPDF.NET.PDF4LLM/logo.png similarity index 100% rename from PDF4LLM/logo.png rename to MuPDF.NET.PDF4LLM/logo.png diff --git a/PDF4LLM/ocr/__init__.cs b/MuPDF.NET.PDF4LLM/ocr/__init__.cs similarity index 98% rename from PDF4LLM/ocr/__init__.cs rename to MuPDF.NET.PDF4LLM/ocr/__init__.cs index e4db0306..08e7895e 100644 --- a/PDF4LLM/ocr/__init__.cs +++ b/MuPDF.NET.PDF4LLM/ocr/__init__.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// OCR execution modes for page analysis. public enum OcrMode diff --git a/PDF4LLM/ocr/analyze_page.cs b/MuPDF.NET.PDF4LLM/ocr/analyze_page.cs similarity index 99% rename from PDF4LLM/ocr/analyze_page.cs rename to MuPDF.NET.PDF4LLM/ocr/analyze_page.cs index bef24cac..d004ab9d 100644 --- a/PDF4LLM/ocr/analyze_page.cs +++ b/MuPDF.NET.PDF4LLM/ocr/analyze_page.cs @@ -9,7 +9,7 @@ using mupdf; using Newtonsoft.Json; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// Analyze a page and decide whether OCR is needed. public static class AnalyzePage diff --git a/PDF4LLM/ocr/compute_ocr_features.cs b/MuPDF.NET.PDF4LLM/ocr/compute_ocr_features.cs similarity index 99% rename from PDF4LLM/ocr/compute_ocr_features.cs rename to MuPDF.NET.PDF4LLM/ocr/compute_ocr_features.cs index cfdfd14c..2f38a810 100644 --- a/PDF4LLM/ocr/compute_ocr_features.cs +++ b/MuPDF.NET.PDF4LLM/ocr/compute_ocr_features.cs @@ -4,7 +4,7 @@ using MuPDF.NET; using mupdf; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// OCR decision model feature extraction. public static class ComputeOcrFeatures diff --git a/PDF4LLM/ocr/get_culled_pixmap.cs b/MuPDF.NET.PDF4LLM/ocr/get_culled_pixmap.cs similarity index 98% rename from PDF4LLM/ocr/get_culled_pixmap.cs rename to MuPDF.NET.PDF4LLM/ocr/get_culled_pixmap.cs index 3448207c..d6d67006 100644 --- a/PDF4LLM/ocr/get_culled_pixmap.cs +++ b/MuPDF.NET.PDF4LLM/ocr/get_culled_pixmap.cs @@ -3,7 +3,7 @@ using MuPDF.NET; using mupdf; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// Pixmap from a page with text culled from given rectangles. public static class GetCulledPixmap diff --git a/PDF4LLM/ocr/ocr_decision_model.cs b/MuPDF.NET.PDF4LLM/ocr/ocr_decision_model.cs similarity index 96% rename from PDF4LLM/ocr/ocr_decision_model.cs rename to MuPDF.NET.PDF4LLM/ocr/ocr_decision_model.cs index 186861d0..0bd140b5 100644 --- a/PDF4LLM/ocr/ocr_decision_model.cs +++ b/MuPDF.NET.PDF4LLM/ocr/ocr_decision_model.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// /// ONNX model path resolution for ocr_decision_model.onnx. diff --git a/PDF4LLM/ocr/ocr_decision_model.onnx b/MuPDF.NET.PDF4LLM/ocr/ocr_decision_model.onnx similarity index 100% rename from PDF4LLM/ocr/ocr_decision_model.onnx rename to MuPDF.NET.PDF4LLM/ocr/ocr_decision_model.onnx diff --git a/PDF4LLM/ocr/ocr_page_helpers.cs b/MuPDF.NET.PDF4LLM/ocr/ocr_page_helpers.cs similarity index 99% rename from PDF4LLM/ocr/ocr_page_helpers.cs rename to MuPDF.NET.PDF4LLM/ocr/ocr_page_helpers.cs index c8d7292e..8d7736d4 100644 --- a/PDF4LLM/ocr/ocr_page_helpers.cs +++ b/MuPDF.NET.PDF4LLM/ocr/ocr_page_helpers.cs @@ -4,7 +4,7 @@ using MuPDF.NET; using mupdf; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// Shared helpers for OCR page execution callbacks. internal static class OcrPageHelpers diff --git a/PDF4LLM/ocr/paddleocr_api.cs b/MuPDF.NET.PDF4LLM/ocr/paddleocr_api.cs similarity index 94% rename from PDF4LLM/ocr/paddleocr_api.cs rename to MuPDF.NET.PDF4LLM/ocr/paddleocr_api.cs index f5adf6ce..41aa38e0 100644 --- a/PDF4LLM/ocr/paddleocr_api.cs +++ b/MuPDF.NET.PDF4LLM/ocr/paddleocr_api.cs @@ -1,6 +1,6 @@ using MuPDF.NET; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// PaddleOCR ONNX pipeline (alias of in upstream). public static class PaddleOcrApi diff --git a/PDF4LLM/ocr/paddletess_api.cs b/MuPDF.NET.PDF4LLM/ocr/paddletess_api.cs similarity index 96% rename from PDF4LLM/ocr/paddletess_api.cs rename to MuPDF.NET.PDF4LLM/ocr/paddletess_api.cs index 1fa12654..132da6f3 100644 --- a/PDF4LLM/ocr/paddletess_api.cs +++ b/MuPDF.NET.PDF4LLM/ocr/paddletess_api.cs @@ -1,6 +1,6 @@ using MuPDF.NET; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// PaddleOCR plus Tesseract pipeline (alias of in upstream). public static class PaddleTessApi diff --git a/PDF4LLM/ocr/rapid_ocr_engine.cs b/MuPDF.NET.PDF4LLM/ocr/rapid_ocr_engine.cs similarity index 99% rename from PDF4LLM/ocr/rapid_ocr_engine.cs rename to MuPDF.NET.PDF4LLM/ocr/rapid_ocr_engine.cs index 985117c1..6400a9c3 100644 --- a/PDF4LLM/ocr/rapid_ocr_engine.cs +++ b/MuPDF.NET.PDF4LLM/ocr/rapid_ocr_engine.cs @@ -7,7 +7,7 @@ using RapidOcrNet; using SkiaSharp; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// Lazy RapidOcrNet engine used by and . internal static class RapidOcrEngine @@ -190,7 +190,7 @@ void Add(string dir) if (!string.IsNullOrEmpty(rapidOcrDir)) Add(Path.Combine(rapidOcrDir, subdir)); - string envDir = Environment.GetEnvironmentVariable("PDF4LLM_RAPIDOCR_MODELS"); + string envDir = Environment.GetEnvironmentVariable("MuPDF4LLM_NET_RAPIDOCR_MODELS"); if (!string.IsNullOrWhiteSpace(envDir)) Add(envDir); diff --git a/PDF4LLM/ocr/rapid_ocr_support.cs b/MuPDF.NET.PDF4LLM/ocr/rapid_ocr_support.cs similarity index 93% rename from PDF4LLM/ocr/rapid_ocr_support.cs rename to MuPDF.NET.PDF4LLM/ocr/rapid_ocr_support.cs index 7fa293ae..006ecacb 100644 --- a/PDF4LLM/ocr/rapid_ocr_support.cs +++ b/MuPDF.NET.PDF4LLM/ocr/rapid_ocr_support.cs @@ -1,4 +1,4 @@ -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// Runtime probe for RapidOCR (RapidOcrNet on .NET 8+). public static class RapidOcrSupport diff --git a/PDF4LLM/ocr/rapidocr_api.cs b/MuPDF.NET.PDF4LLM/ocr/rapidocr_api.cs similarity index 99% rename from PDF4LLM/ocr/rapidocr_api.cs rename to MuPDF.NET.PDF4LLM/ocr/rapidocr_api.cs index 1cc8c348..db2e64a0 100644 --- a/PDF4LLM/ocr/rapidocr_api.cs +++ b/MuPDF.NET.PDF4LLM/ocr/rapidocr_api.cs @@ -2,7 +2,7 @@ using System.Linq; using MuPDF.NET; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// /// RapidOCR-only pipeline (reference OCR module contract). diff --git a/PDF4LLM/ocr/rapidtess_api.cs b/MuPDF.NET.PDF4LLM/ocr/rapidtess_api.cs similarity index 99% rename from PDF4LLM/ocr/rapidtess_api.cs rename to MuPDF.NET.PDF4LLM/ocr/rapidtess_api.cs index 26eec900..034f7397 100644 --- a/PDF4LLM/ocr/rapidtess_api.cs +++ b/MuPDF.NET.PDF4LLM/ocr/rapidtess_api.cs @@ -1,7 +1,7 @@ using System.Linq; using MuPDF.NET; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// RapidOCR plus Tesseract combined pipeline. public static class RapidTessApi diff --git a/PDF4LLM/ocr/tesseract_api.cs b/MuPDF.NET.PDF4LLM/ocr/tesseract_api.cs similarity index 97% rename from PDF4LLM/ocr/tesseract_api.cs rename to MuPDF.NET.PDF4LLM/ocr/tesseract_api.cs index df382f31..f16d3474 100644 --- a/PDF4LLM/ocr/tesseract_api.cs +++ b/MuPDF.NET.PDF4LLM/ocr/tesseract_api.cs @@ -4,14 +4,14 @@ using MuPDF.NET; using mupdf; -namespace PDF4LLM.Ocr +namespace MuPDF.NET.PDF4LLM.Ocr { /// Tesseract OCR integration. public static class TesseractApi { public const char ReplacementUnicode = '\uFFFD'; - public static string Tessdata => global::PDF4LLM.Helpers.LayoutParseHelpers.TryGetTessdata(); + public static string Tessdata => global::MuPDF.NET.PDF4LLM.Helpers.LayoutParseHelpers.TryGetTessdata(); /// Whether span text came from OCR. /// Text span to inspect. diff --git a/PDF4LLM/scripts/requirements-layout.txt b/MuPDF.NET.PDF4LLM/scripts/requirements-layout.txt similarity index 100% rename from PDF4LLM/scripts/requirements-layout.txt rename to MuPDF.NET.PDF4LLM/scripts/requirements-layout.txt diff --git a/PDF4LLM/scripts/setup_layout_python.ps1 b/MuPDF.NET.PDF4LLM/scripts/setup_layout_python.ps1 similarity index 100% rename from PDF4LLM/scripts/setup_layout_python.ps1 rename to MuPDF.NET.PDF4LLM/scripts/setup_layout_python.ps1 diff --git a/PDF4LLM/scripts/setup_layout_python.py b/MuPDF.NET.PDF4LLM/scripts/setup_layout_python.py similarity index 87% rename from PDF4LLM/scripts/setup_layout_python.py rename to MuPDF.NET.PDF4LLM/scripts/setup_layout_python.py index e5cf49e1..3a9090d0 100644 --- a/PDF4LLM/scripts/setup_layout_python.py +++ b/MuPDF.NET.PDF4LLM/scripts/setup_layout_python.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 -"""Create the local Python venv used by PDF4LLM's pymupdf.layout bridge. +"""Create the local Python venv used by MuPDF4LLM.NET's pymupdf.layout bridge. -Run once per machine (or after upgrading PDF4LLM). +Run once per machine (or after upgrading MuPDF4LLM.NET). From a NuGet consumer project: - dotnet msbuild -t:PDF4LLMSetupLayoutPython + dotnet msbuild -t:MuPDF4LLMNetSetupLayoutPython From the MuPDF.NET repo: - python PDF4LLM/scripts/setup_layout_python.py + python MuPDF4LLM.NET/scripts/setup_layout_python.py Windows: @@ -31,12 +31,12 @@ def default_venv_path() -> Path: - """Must match PDF4LLM.Layout.LayoutPythonPaths.UserLocalVenvRoot().""" + """Must match MuPDF4LLM.NET.Layout.LayoutPythonPaths.UserLocalVenvRoot().""" if sys.platform == "win32": local = os.environ.get("LOCALAPPDATA") base = Path(local) if local else Path.home() / "AppData" / "Local" - return base / "PDF4LLM" / ".venv-layout" - return Path.home() / ".local" / "share" / "pdf4llm" / ".venv-layout" + return base / "MuPDF4LLM.NET" / ".venv-layout" + return Path.home() / ".local" / "share" / "mupdf4llm.net" / ".venv-layout" def venv_python(venv_root: Path) -> Path: @@ -129,7 +129,7 @@ def normalize_version(version: str) -> str: def resolve_layout_python(venv_root: Path | None) -> Path | None: - env = os.environ.get("PDF4LLM_PYTHON") or os.environ.get("PYTHON") + env = os.environ.get("MuPDF4LLM_NET_PYTHON") or os.environ.get("PYTHON") if env: candidate = Path(env) if candidate.is_file(): @@ -159,19 +159,19 @@ def verify_layout_python(py: Path, required: str) -> int: print("pymupdf-layout is not installed.", file=sys.stderr) print( "Install with:\n" - " dotnet msbuild -t:PDF4LLMSetupLayoutPython", + " dotnet msbuild -t:MuPDF4LLMNetSetupLayoutPython", file=sys.stderr, ) return 1 if normalize_version(out) != normalize_version(required): print( - f"pymupdf-layout {out} is installed; PDF4LLM requires pymupdf-layout {required}.", + f"pymupdf-layout {out} is installed; MuPDF4LLM.NET requires pymupdf-layout {required}.", file=sys.stderr, ) print( "Refresh with:\n" - " dotnet msbuild -t:PDF4LLMSetupLayoutPython", + " dotnet msbuild -t:MuPDF4LLMNetSetupLayoutPython", file=sys.stderr, ) return 1 @@ -211,7 +211,7 @@ def main() -> int: print("No layout Python interpreter found.", file=sys.stderr) print( "Install with:\n" - " dotnet msbuild -t:PDF4LLMSetupLayoutPython", + " dotnet msbuild -t:MuPDF4LLMNetSetupLayoutPython", file=sys.stderr, ) return 1 @@ -254,8 +254,8 @@ def main() -> int: {venv_root} interpreter: {py} - PDF4LLM discovers this venv automatically when present. - To use another interpreter, set PDF4LLM_PYTHON: + MuPDF4LLM.NET discovers this venv automatically when present. + To use another interpreter, set MuPDF4LLM_NET_PYTHON: {py} """ ).strip() diff --git a/PDF4LLM/scripts/setup_layout_python.sh b/MuPDF.NET.PDF4LLM/scripts/setup_layout_python.sh similarity index 100% rename from PDF4LLM/scripts/setup_layout_python.sh rename to MuPDF.NET.PDF4LLM/scripts/setup_layout_python.sh diff --git a/MuPDF.NET.Test/TestCrypting.cs b/MuPDF.NET.Test/TestCrypting.cs index 71d4acef..42d151d9 100644 --- a/MuPDF.NET.Test/TestCrypting.cs +++ b/MuPDF.NET.Test/TestCrypting.cs @@ -37,8 +37,8 @@ public void test_encryption() ms, encryption: encrypt_meth, // set the encryption method permissions: perm, // set permissions - owner_pw: owner_pass, // set the owner password - user_pw: user_pass); // set the user password + ownerPW: owner_pass, // set the owner password + userPW: user_pass); // set the user password tobytes = ms.ToArray(); doc.Save(outDocPath); } diff --git a/MuPDF.NET.Test/TestGeneral.cs b/MuPDF.NET.Test/TestGeneral.cs index b4e710b9..02b2fb90 100644 --- a/MuPDF.NET.Test/TestGeneral.cs +++ b/MuPDF.NET.Test/TestGeneral.cs @@ -323,7 +323,7 @@ public void test_add_ink_annot() line2.Add(new Point(x, s)); } page.AddInkAnnot(new[] { line1.ToArray(), line2.ToArray() }); - page.insert_text(new Point(100, 72), "Hello world"); + page.InsertText(new Point(100, 72), "Hello world"); page.AddTextAnnot(new Point(200, 200), "Some Text"); page.GetBboxlog(); document.Save(Out("test_add_ink_annot.pdf")); @@ -531,12 +531,12 @@ public void test_2506() // write text with different angles foreach (int angle in angles) { - // page.insert_text( + // page.InsertText( // ) - page.insert_text( + page.InsertText( point, text, - fontsize: fontsize, + fontSize: fontsize, morphFix: point, morphMat: new Matrix(angle)); } @@ -773,7 +773,7 @@ public void test_2182() /// /// Test / confirm identical text positions generated by - /// * page.insert_text() + /// * page.InsertText() /// versus /// * TextWriter.WriteText() /// @@ -783,7 +783,7 @@ public void test_2182() /// 2. CropBox origin is different from that of MediaBox /// 3. Check for all 4 possible page rotations /// - /// The test writes the same text at the same positions using page.insert_text(), + /// The test writes the same text at the same positions using page.InsertText(), /// respectively TextWriter.WriteText(). /// Then extracts the text spans and confirms that they all occupy the same bbox. /// This ensures coincidence of text positions of page.of insert_text() @@ -827,7 +827,7 @@ int BboxCount(int rot) "[{0:g} {1:g} {2:g} {3:g}]", cbox.X0, cbox.Y0, cbox.X1, cbox.Y1)); // set page to desired rotation page.SetRotation(rot); - page.insert_text(new Point(50, 50), "Text inserted at (50,50)"); + page.InsertText(new Point(50, 50), "Text inserted at (50,50)"); var tw = new TextWriter(page.Rect); tw.Append(new Point(50, 50), "Text inserted at (50,50)"); tw.WriteText(page); @@ -1512,7 +1512,7 @@ public void test_scientific_numbers() { using var doc = new Document(); var page = doc.NewPage(width: 595, height: 842); - page.insert_text(new Point(1e-11f, -1e-10f), "Test"); + page.InsertText(new Point(1e-11f, -1e-10f), "Test"); byte[] contents = page.ReadContents(); Assert.DoesNotContain(" 1e-"u8, contents.AsSpan()); doc.Save(Out("test_scientific_numbers.pdf")); @@ -1796,7 +1796,7 @@ public void test_4319() using (var doc = new Document()) { var page = doc.NewPage(); - page.insert_text(new Point(10, 100), "some text"); + page.InsertText(new Point(10, 100), "some text"); doc.Save(path); } using (var doc = new Document(path)) @@ -1860,7 +1860,7 @@ public void test_4415() Point orig = new Point(100, 100); string text = "Text at Top-Left"; // text = 'Text at Top-Left' Matrix mrot = page.DerotationMatrix; // mrot = page.derotation_matrix # matrix annihilating page rotation - page.insert_text(orig * mrot, text, fontsize: 60, rotate: rot); // page.insert_text(orig * mrot, text, fontsize=60, rotate=rot) + page.InsertText(orig * mrot, text, fontSize: 60, rotate: rot); // page.InsertText(orig * mrot, text, fontSize=60, rotate=rot) using Pixmap pixmap = page.GetPixmap(); // pixmap = page.GetPixmap() pixmap.Save(path_out); // pixmap.Save(path_out) float rms = _Compare.PixmapsRms(path_out_expected, path_out); // rms = gentle_compare.pixmaps_rms(path_out_expected, path_out) @@ -1996,7 +1996,7 @@ public void test_4590() Page page = document.NewPage(); // page = document.NewPage() // Add some text string text = "This PDF contains a file attachment annotation."; // text = 'This PDF contains a file attachment annotation.' - page.insert_text(new Point(72, 72), text, fontsize: 12); // page.insert_text((72, 72), text, fontsize=12) + page.InsertText(new Point(72, 72), text, fontSize: 12); // page.InsertText((72, 72), text, fontSize=12) // Create a sample file. string path_sample = Path.GetFullPath(Doc("test_4590_annotation_sample.txt")); // path_sample = os.path.normpath(f'{__file__}/../../tests/test_4590_annotation_sample.txt') File.WriteAllText(path_sample, "This is a sample attachment file."); // with open(path_sample, 'w') as f: f.write('This is a sample attachment file.') diff --git a/MuPDF.NET.Test/TestInsertimage.cs b/MuPDF.NET.Test/TestInsertimage.cs index 5149a939..539f24bf 100644 --- a/MuPDF.NET.Test/TestInsertimage.cs +++ b/MuPDF.NET.Test/TestInsertimage.cs @@ -70,8 +70,8 @@ public void test_compress() Out("test_compress.pdf"), garbage: 3, deflate: 1, - deflate_images: 1, - deflate_fonts: 1, + deflateImages: 1, + deflateFonts: 1, pretty: 1); } diff --git a/MuPDF.NET.Test/TestObjectstreams.cs b/MuPDF.NET.Test/TestObjectstreams.cs index d5961927..00486c43 100644 --- a/MuPDF.NET.Test/TestObjectstreams.cs +++ b/MuPDF.NET.Test/TestObjectstreams.cs @@ -79,14 +79,14 @@ public void test_objectstream3() garbage: 1, clean: 0, deflate: 1, - deflate_images: 1, - deflate_fonts: 1, + deflateImages: 1, + deflateFonts: 1, pretty: 0, linear: 0, ascii: 0, encryption: 1, noNewId: 1, - use_objstms: 1); + useObjstms: 1); Assert.True(HasObjStm(doc), "No object stream found"); doc.EzSave(Out("test_objectstream3.pdf")); } diff --git a/MuPDF.NET.Test/TestPagedelete.cs b/MuPDF.NET.Test/TestPagedelete.cs index 479d152e..3a3ee5e5 100644 --- a/MuPDF.NET.Test/TestPagedelete.cs +++ b/MuPDF.NET.Test/TestPagedelete.cs @@ -265,7 +265,7 @@ public void test_4790() try { // document.Save(path2, raise_on_repair=True) - document.Save(path2, raise_on_repair: true); + document.Save(path2, raiseOnRepair: true); } catch (Exception e) { @@ -288,7 +288,7 @@ public void test_4790() // document.delete_pages(page_to_delete) document.delete_pages(page_to_delete); // document.Save(path2, raise_on_repair=True) - document.Save(path2, raise_on_repair: true); + document.Save(path2, raiseOnRepair: true); } using (var document = new Document(path2)) { diff --git a/MuPDF.NET.Test/TestPixmap.cs b/MuPDF.NET.Test/TestPixmap.cs index fc76bdfb..06f5c7e5 100644 --- a/MuPDF.NET.Test/TestPixmap.cs +++ b/MuPDF.NET.Test/TestPixmap.cs @@ -414,7 +414,7 @@ public void test_3072() // image_save_path = f'{out}/1.jpg' string image_save_path = Path.Combine(outDir, "test_3072_1.jpg"); // pix.Save(image_save_path, jpg_quality=95) - pix.Save(image_save_path, jpg_quality: 95); + pix.Save(image_save_path, jpgQuality: 95); } using (var doc = new Document(path)) @@ -429,7 +429,7 @@ public void test_3072() // image_save_path = f'{out}/2.jpg' string image_save_path = Path.Combine(outDir, "test_3072_2.jpg"); // pix.Save(image_save_path, jpg_quality=95) - pix.Save(image_save_path, jpg_quality: 95); + pix.Save(image_save_path, jpgQuality: 95); string wt = Tools.MupdfWarnings(); Assert.Equal( "syntax error: cannot find ExtGState resource 'BlendMode0'\n" diff --git a/MuPDF.NET.sln b/MuPDF.NET.sln index 1564d09c..556cef48 100644 --- a/MuPDF.NET.sln +++ b/MuPDF.NET.sln @@ -1,6 +1,7 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36511.14 +VisualStudioVersion = 17.14.37516.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MuPDF.NET", "MuPDF.NET\MuPDF.NET.csproj", "{6E09980C-0293-4637-A374-A2C9DF020B19}" ProjectSection(ProjectDependencies) = postProject @@ -11,11 +12,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MuPDF.NET.Test", "MuPDF.NET EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "Demo\Demo.csproj", "{D1CCB24F-A868-F185-9228-8CC249247C79}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PDF4LLM", "PDF4LLM\PDF4LLM.csproj", "{A34D58F2-3815-19A9-1D4C-8488A12C291C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MuPDF.Fonts", "MuPDF.Fonts\MuPDF.Fonts.csproj", "{7083C4C1-2088-3E05-E1F9-487AB90898D2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PDF4LLM.Test", "PDF4LLM.Test\PDF4LLM.Test.csproj", "{07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MuPDF.NET.PDF4LLM", "MuPDF.NET.PDF4LLM\MuPDF.NET.PDF4LLM.csproj", "{BF917038-B32C-455D-A28C-DE81F6156C0E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MuPDF.Fonts", "MuPDF.Fonts\MuPDF.Fonts.csproj", "{7083C4C1-2088-3E05-E1F9-487AB90898D2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MuPDF.NET.PDF4LLM.Test", "MuPDF.NET.PDF4LLM.Test\MuPDF.NET.PDF4LLM.Test.csproj", "{4A37FECE-91B5-4C29-93AA-99BF5226AF7A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -63,30 +64,6 @@ Global {D1CCB24F-A868-F185-9228-8CC249247C79}.Release|x64.Build.0 = Release|x64 {D1CCB24F-A868-F185-9228-8CC249247C79}.Release|x86.ActiveCfg = Release|x86 {D1CCB24F-A868-F185-9228-8CC249247C79}.Release|x86.Build.0 = Release|x86 - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Debug|x64.ActiveCfg = Debug|x64 - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Debug|x64.Build.0 = Debug|x64 - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Debug|x86.ActiveCfg = Debug|x86 - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Debug|x86.Build.0 = Debug|x86 - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Release|Any CPU.Build.0 = Release|Any CPU - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Release|x64.ActiveCfg = Release|x64 - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Release|x64.Build.0 = Release|x64 - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Release|x86.ActiveCfg = Release|x86 - {A34D58F2-3815-19A9-1D4C-8488A12C291C}.Release|x86.Build.0 = Release|x86 - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Debug|x64.ActiveCfg = Debug|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Debug|x64.Build.0 = Debug|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Debug|x86.ActiveCfg = Debug|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Debug|x86.Build.0 = Debug|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Release|Any CPU.Build.0 = Release|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Release|x64.ActiveCfg = Release|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Release|x64.Build.0 = Release|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Release|x86.ActiveCfg = Release|Any CPU - {07FF113F-B37A-8C0E-CF2D-DE4E47EC6784}.Release|x86.Build.0 = Release|Any CPU {7083C4C1-2088-3E05-E1F9-487AB90898D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7083C4C1-2088-3E05-E1F9-487AB90898D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {7083C4C1-2088-3E05-E1F9-487AB90898D2}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -99,6 +76,30 @@ Global {7083C4C1-2088-3E05-E1F9-487AB90898D2}.Release|x64.Build.0 = Release|Any CPU {7083C4C1-2088-3E05-E1F9-487AB90898D2}.Release|x86.ActiveCfg = Release|Any CPU {7083C4C1-2088-3E05-E1F9-487AB90898D2}.Release|x86.Build.0 = Release|Any CPU + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Debug|x64.ActiveCfg = Debug|x64 + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Debug|x64.Build.0 = Debug|x64 + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Debug|x86.ActiveCfg = Debug|x86 + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Debug|x86.Build.0 = Debug|x86 + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Release|Any CPU.Build.0 = Release|Any CPU + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Release|x64.ActiveCfg = Release|x64 + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Release|x64.Build.0 = Release|x64 + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Release|x86.ActiveCfg = Release|x86 + {BF917038-B32C-455D-A28C-DE81F6156C0E}.Release|x86.Build.0 = Release|x86 + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Debug|x64.Build.0 = Debug|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Debug|x86.Build.0 = Debug|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Release|Any CPU.Build.0 = Release|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Release|x64.ActiveCfg = Release|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Release|x64.Build.0 = Release|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Release|x86.ActiveCfg = Release|Any CPU + {4A37FECE-91B5-4C29-93AA-99BF5226AF7A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MuPDF.NET/CHANGELOG.md b/MuPDF.NET/CHANGELOG.md index 4e773ff9..bac619c8 100644 --- a/MuPDF.NET/CHANGELOG.md +++ b/MuPDF.NET/CHANGELOG.md @@ -8,7 +8,7 @@ ### [3.28.1] - 2026-07-10 -- **`FindTables()` / `Utils.GetTables()`**: layout-guided detection when `Page.GetLayoutProvider` is set (pymupdf-layout / **PDF4LLM**); supports tuple and raw layout `table` boxes. +- **`FindTables()` / `Utils.GetTables()`**: layout-guided detection when `Page.GetLayoutProvider` is set (pymupdf-layout / **MuPDF.NET.PDF4LLM**); supports tuple and raw layout `table` boxes. - **Concurrency**: per-thread scratch buffers and detection settings replace shared `TableModule` state — safe for parallel extraction across separate documents. - **`ToMarkdown()`**: fixed header regression (`Col1`, `Col2`, …) after the thread-safety refactor. @@ -35,7 +35,7 @@ Aligned MuPDF.NET with **PyMuPDF 1.28.0** and **MuPDF 1.28.0**. - Depends on stable **`MuPDF.NativeAssets` 1.28.0**. - Updated PyMuPDF bind to **1.27.2.3** (`VersionBind` / `pymupdf_version`). - **`Matrix`**: added static `Matrix.Concat(one, two)`; renamed the in-place PyMuPDF `concat` equivalent to `ConcatInto(one, two)` (avoids C# static/instance signature clashes); added instance `Inverted()` returning a new matrix or `null` when singular. -- **`Page`**: added `GetLayout()`, `LayoutInformation`, and `GetLayoutProvider` so external layout engines (e.g. pymupdf-layout via **PDF4LLM**) can supply `layout_information` boxes consumed by `Page.find_tables()`. +- **`Page`**: added `GetLayout()`, `LayoutInformation`, and `GetLayoutProvider` so external layout engines (e.g. pymupdf-layout via **MuPDF.NET.PDF4LLM**) can supply `layout_information` boxes consumed by `Page.find_tables()`. - Expanded `MuPDF.NET.Test` matrix/geometry coverage aligned with PyMuPDF `test_geometry.py`. ### [3.2.17] - 2026-06-11 @@ -46,18 +46,18 @@ Aligned MuPDF.NET with **PyMuPDF 1.28.0** and **MuPDF 1.28.0**. ### [3.2.16] - 2026-04-24 - Added global `Utils.MuPDFLock` and synchronized MuPDF native calls for improved thread safety. -- Improved Tesseract OCR stability in the `PDF4LLM` OCR pipeline and hardened OCR helper behavior. +- Improved Tesseract OCR stability in the `MuPDF.NET.PDF4LLM` OCR pipeline and hardened OCR helper behavior. - Fixed a regression in Llama `LoadData` and added a new `TableExtract` demo sample. -- Updated `PDF4LLM` package metadata and NuGet project files. +- Updated `MuPDF.NET.PDF4LLM` package metadata and NuGet project files. ### [3.2.15] - 2026-04-17 -- Migrated the helper package from `MuPDF.NET4LLM` to `PDF4LLM` and refreshed the package layout, demos, and documentation. +- Migrated the helper package from `MuPDF.NET4LLM` to `MuPDF.NET.PDF4LLM` and refreshed the package layout, demos, and documentation. - Added file-path overloads for `ToMarkdown`, `ToJson`, and `ToText` helpers. -- Updated `PDF4LLM` package support for the latest MuPDF bindings and metadata. +- Updated `MuPDF.NET.PDF4LLM` package support for the latest MuPDF bindings and metadata. ### [3.2.14] - 2026-03-23 - Fixed issue #234 in page/text utilities and added a regression test in `UtilsTest`. -- Minor `PDF4LLM` documentation and comment updates. +- Minor `MuPDF.NET.PDF4LLM` documentation and comment updates. ### [3.2.13] - 2026-03-18 - Added **MuPDF.NET4LLM** as a separate NuGet package: LLM/RAG helpers for PDF-to-Markdown conversion, layout parsing, document structure analysis, and LlamaIndex integration. Install via `dotnet add package MuPDF.NET4LLM`; depends on MuPDF.NET. diff --git a/MuPDF.NET/Document.cs b/MuPDF.NET/Document.cs index a71da176..ae5cfa16 100644 --- a/MuPDF.NET/Document.cs +++ b/MuPDF.NET/Document.cs @@ -2313,8 +2313,8 @@ internal void Save1( /// Garbage collection level: 0 none; 1 unused objects; 2 compact xref; 3 merge duplicates; 4 stream dedup. /// Clean and sanitize content streams (mutool clean -sc). /// Deflate (compress) uncompressed streams. - /// Deflate uncompressed image streams. - /// Deflate uncompressed fontfile streams. + /// Deflate uncompressed image streams. + /// Deflate uncompressed fontfile streams. /// Incremental save to the original file only; excludes garbage and linear. /// Convert binary stream data to ASCII. /// Decompress objects: 0 none, 1 images, 2 fonts, 255 all. @@ -2324,20 +2324,20 @@ internal void Save1( /// Prettify PDF object syntax. /// Encryption method when saving. /// Permission flags for encrypted output. - /// Owner password (max 40 characters). - /// User password (max 40 characters). - /// Preserve existing document metadata. - /// Store eligible objects in object streams (size reduction). - /// Compression effort 0 (default) to 100 (maximum). - /// Throw if save repairs the PDF structure. + /// Owner password (max 40 characters). + /// User password (max 40 characters). + /// Preserve existing document metadata when cleaning. + /// Store eligible objects in object streams (size reduction). + /// Compression effort 0 (default) to 100 (maximum). + /// Throw if save repairs the PDF structure. /// Document is closed, encrypted, or arguments are invalid. public void Save( object filename, int garbage = 0, int clean = 0, int deflate = 0, - int deflate_images = 0, - int deflate_fonts = 0, + int deflateImages = 0, + int deflateFonts = 0, int incremental = 0, int ascii = 0, int expand = 0, @@ -2347,14 +2347,14 @@ public void Save( int pretty = 0, int encryption = 1, int permissions = 4095, - string owner_pw = null, - string user_pw = null, - int preserve_metadata = 1, - int use_objstms = 0, - int compression_effort = 0, - bool raise_on_repair = false) + string ownerPW = null, + string userPW = null, + int preserveMetadata = 1, + int useObjstms = 0, + int compressionEffort = 0, + bool raiseOnRepair = false) { - SaveCore(filename, garbage, clean, deflate, deflate_images, deflate_fonts, incremental, ascii, expand, linear, noNewId, appearance, pretty, encryption, permissions, owner_pw, user_pw, preserve_metadata, use_objstms, compression_effort, raise_on_repair); + SaveCore(filename, garbage, clean, deflate, deflateImages, deflateFonts, incremental, ascii, expand, linear, noNewId, appearance, pretty, encryption, permissions, ownerPW, userPW, preserveMetadata, useObjstms, compressionEffort, raiseOnRepair); } /// /// PDF only: save the document @@ -2375,12 +2375,12 @@ public void Save( /// Prettify PDF object syntax. /// Encryption method when saving. /// Permission flags for encrypted output. - /// Owner password (max 40 characters). - /// User password (max 40 characters). - /// Preserve existing document metadata. - /// Store eligible objects in object streams (size reduction). - /// Compression effort 0 (default) to 100 (maximum). - /// Throw if save repairs the PDF structure. + /// Owner password (max 40 characters). + /// User password (max 40 characters). + /// Preserve existing document metadata when cleaning. + /// Store eligible objects in object streams (size reduction). + /// Compression effort 0 (default) to 100 (maximum). + /// Throw if save repairs the PDF structure. /// Document is closed, encrypted, or arguments are invalid. public void Save( Stream output, @@ -2398,14 +2398,14 @@ public void Save( int pretty = 0, int encryption = 1, int permissions = 4095, - string owner_pw = null, - string user_pw = null, - int preserve_metadata = 1, - int use_objstms = 0, - int compression_effort = 0, - bool raise_on_repair = false) + string ownerPW = null, + string userPW = null, + int preserveMetadata = 1, + int useObjstms = 0, + int compressionEffort = 0, + bool raiseOnRepair = false) { - SaveCore(output, garbage, clean, deflate, deflateImages, deflateFonts, incremental, ascii, expand, linear, noNewId, appearance, pretty, encryption, permissions, owner_pw, user_pw, preserve_metadata, use_objstms, compression_effort, raise_on_repair); + SaveCore(output, garbage, clean, deflate, deflateImages, deflateFonts, incremental, ascii, expand, linear, noNewId, appearance, pretty, encryption, permissions, ownerPW, userPW, preserveMetadata, useObjstms, compressionEffort, raiseOnRepair); } private void SaveCore( @@ -2413,8 +2413,8 @@ private void SaveCore( int garbage, int clean, int deflate, - int deflate_images, - int deflate_fonts, + int deflateImages, + int deflateFonts, int incremental, int ascii, int expand, @@ -2424,12 +2424,12 @@ private void SaveCore( int pretty, int encryption, int permissions, - string owner_pw, - string user_pw, - int preserve_metadata, - int use_objstms, - int compression_effort, - bool raise_on_repair) + string ownerPW, + string userPW, + int preserveMetadata, + int useObjstms, + int compressionEffort, + bool raiseOnRepair) { bool is_repaired_pre = IsRepaired; if (IsClosed || IsEncrypted) @@ -2445,7 +2445,7 @@ private void SaveCore( // MuPDF: disallow overwriting the on-disk path unless incremental. Memory opens (StreamData) are ok. if (fname == Name && incremental == 0 && (StreamData == null || StreamData.Length == 0)) throw new ValueErrorException("save to original must be incremental"); - if (linear != 0 && use_objstms != 0) + if (linear != 0 && useObjstms != 0) throw new ValueErrorException("'linear' and 'use_objstms' cannot both be requested"); if (PageCount < 1) throw new ValueErrorException("cannot save with zero pages"); @@ -2454,7 +2454,7 @@ private void SaveCore( if (Name != fname || StreamData != null) throw new ValueErrorException("incremental needs original file"); } - if ((user_pw != null && user_pw.Length > 40) || (owner_pw != null && owner_pw.Length > 40)) + if ((userPW != null && userPW.Length > 40) || (ownerPW != null && ownerPW.Length > 40)) throw new ValueErrorException("password length must not exceed 40"); // Use cached borrowed PdfDocument — do not dispose (owned by FzDocument). @@ -2472,8 +2472,8 @@ private void SaveCore( opts.do_incremental = incremental; opts.do_ascii = ascii; opts.do_compress = deflate; - opts.do_compress_images = deflate_images; - opts.do_compress_fonts = deflate_fonts; + opts.do_compress_images = deflateImages; + opts.do_compress_fonts = deflateFonts; opts.do_decompress = expand; opts.do_garbage = garbage; opts.do_pretty = pretty; @@ -2484,15 +2484,15 @@ private void SaveCore( opts.do_appearance = appearance; opts.do_encrypt = encryption; opts.permissions = permissions; - if (owner_pw != null) - opts.opwd_utf8_set_value(owner_pw); - else if (user_pw != null) - opts.opwd_utf8_set_value(user_pw); - if (user_pw != null) - opts.upwd_utf8_set_value(user_pw); - opts.do_preserve_metadata = preserve_metadata; - opts.do_use_objstms = use_objstms; - opts.compression_effort = compression_effort; + if (ownerPW != null) + opts.opwd_utf8_set_value(ownerPW); + else if (userPW != null) + opts.opwd_utf8_set_value(userPW); + if (userPW != null) + opts.upwd_utf8_set_value(userPW); + opts.do_preserve_metadata = preserveMetadata; + opts.do_use_objstms = useObjstms; + opts.compression_effort = compressionEffort; pdf.m_internal.resynth_required = 0; Helpers.JM_embedded_clean(pdf); @@ -2559,7 +2559,7 @@ private void SaveCore( DropPdfPageTreeIfPdf(); convertedDoc?.Close(); } - if (raise_on_repair) + if (raiseOnRepair) { if (IsRepaired && !is_repaired_pre) throw new Exception("Document save did a repair"); @@ -2617,11 +2617,11 @@ public byte[] Write(bool garbage = false, bool clean = false, bool deflate = fal pretty: pretty ? 1 : 0, encryption: encryption, permissions: permissions, - owner_pw: ownerPassword, - user_pw: userPassword, - preserve_metadata: preserveMetadata ? 1 : 0, - use_objstms: useObjstms ? 1 : 0, - compression_effort: compressionEffort ? 1 : 0); + ownerPW: ownerPassword, + userPW: userPassword, + preserveMetadata: preserveMetadata ? 1 : 0, + useObjstms: useObjstms ? 1 : 0, + compressionEffort: compressionEffort ? 1 : 0); return ms.ToArray(); } /// @@ -2696,9 +2696,9 @@ public void EzSave(string filename, int garbage = 1, int clean = 0, int deflate int deflateImages = 1, int deflateFonts = 1, int pretty = 0, int linear = 0, int ascii = 0, int encryption = 1, int noNewId = 1, int useObjstms = 1) { - Save(filename, garbage: garbage, clean: clean, deflate: deflate, deflate_images: deflateImages, - deflate_fonts: deflateFonts, pretty: pretty, linear: linear, ascii: ascii, encryption: encryption, - noNewId: noNewId, use_objstms: useObjstms); + Save(filename, garbage: garbage, clean: clean, deflate: deflate, deflateImages: deflateImages, + deflateFonts: deflateFonts, pretty: pretty, linear: linear, ascii: ascii, encryption: encryption, + noNewId: noNewId, useObjstms: useObjstms); } // ─── Xref Operations ──────────────────────────────────────────── diff --git a/MuPDF.NET/Helpers.cs b/MuPDF.NET/Helpers.cs index 6a69353c..86fd2c6d 100644 --- a/MuPDF.NET/Helpers.cs +++ b/MuPDF.NET/Helpers.cs @@ -2890,7 +2890,7 @@ internal static mupdf.PdfDocument AsPdfDocument(object doc, bool required = true { if (d.IsClosed) throw new ValueErrorException("document closed"); if (d.IsPdf) - // Fresh non-owning wrapper: callers (e.g. PDF4LLM) may Dispose without breaking the cache. + // Fresh non-owning wrapper: callers (e.g. MuPDF.NET.PDF4LLM) may Dispose without breaking the cache. return PdfDocumentBorrowed(d.NativePdfDocument); var ret = PdfDocumentBorrowedFromFz(d.NativeDocument); if (required && ret.m_internal == null) diff --git a/MuPDF.NET/LegacyApiShims.cs b/MuPDF.NET/LegacyApiShims.cs index b0debcf7..99151471 100644 --- a/MuPDF.NET/LegacyApiShims.cs +++ b/MuPDF.NET/LegacyApiShims.cs @@ -535,28 +535,28 @@ public float InsertTextbox( morphMat = tupleMorph.Item2; } - var modernResult = insert_textbox( + var modernResult = InsertTextbox( rect, text?.ToString() ?? string.Empty, align: align, - border_width: borderWidth, + borderWidth: borderWidth, color: color, encoding: encoding, - expandtabs: expandTabs, - fill_opacity: fillOpacity, + expandTabs: expandTabs, + fillOpacity: fillOpacity, fill: fill, - fontfile: fontFile, - fontname: fontName, - fontsize: fontSize, - lineheight: lineHeight, - morph_fix: morphFix, - morph_mat: morphMat, + fontFile: fontFile, + fontName: fontName, + fontSize: fontSize, + lineHeight: lineHeight, + morphFix: morphFix, + morphMat: morphMat, oc: oc, overlay: overlay, - render_mode: renderMode, + renderMode: renderMode, rotate: rotate, - set_simple: setSimple, - stroke_opacity: strokeOpacity); + setSimple: setSimple, + strokeOpacity: strokeOpacity); return modernResult.Rc; } diff --git a/MuPDF.NET/Page.cs b/MuPDF.NET/Page.cs index 3a7911d7..f7298a5d 100644 --- a/MuPDF.NET/Page.cs +++ b/MuPDF.NET/Page.cs @@ -1092,12 +1092,6 @@ private Annot AddStampAnnotFromPayload(Rect rect, string? builtinName, byte[]? i SetRotation(oldRotation); } } - /// - /// PDF only: Add a file attachment annotation with a "PushPin" icon at the specified location. - /// - public Annot add_file_annot(object point, byte[] buffer_, string filename, string ufilename = null, string desc = null, string icon = null) - => add_file_annot_impl(point, buffer_, filename, uFileName: ufilename, desc: desc, icon: icon); - internal Annot add_file_annot_impl(object point, byte[] buffer_, string filename, string uFileName = null, string desc = null, string icon = null) { int old_rotation = annot_preprocess(); @@ -5630,17 +5624,6 @@ internal void _addAnnot_FromString(object linklist) SyncLinkWrapperCache(); } internal Annot add_text_annot(Point point, string text, string icon = "Note") => AddTextAnnot(point, text, icon); - /// - /// PDF only: Add text in a given rectangle. Optionally, the appearance of a "callout" shape can be requested by specifying two or three point-like objects; see below. - /// - public Annot add_freetext_annot(Rect rect, string text, float fontsize = 12, string fontname = "helv", - float[] text_color = null, float[] fill_color = null, float[] border_color = null, float border_width = 0, - int[] dashes = null, Point[] callout = null, int line_end = (int)mupdf.pdf_line_ending.PDF_ANNOT_LE_OPEN_ARROW, - float opacity = 1, int align = 0, int rotate = 0, bool richtext = false, string style = null) - => AddFreeTextAnnot(rect, text, fontSize: fontsize, fontName: fontname, textColor: text_color, - fillColor: fill_color, borderColor: border_color, borderWidth: border_width, dashes: dashes, - callout: callout, lineEnd: (PdfLineEnding)line_end, opacity: opacity, align: align, rotate: rotate, - richtext: richtext, style: style); internal Annot add_line_annot(Point p1, Point p2) => AddLineAnnot(p1, p2); internal Annot add_rect_annot(Rect rect) => AddRectAnnot(rect); internal Annot add_circle_annot(Rect rect) => AddCircleAnnot(rect); @@ -5652,18 +5635,6 @@ internal Annot add_highlight_annot(object quads = null, Point start = null, Poin internal Annot add_strikeout_annot(Quad[] quads = null, Point start = null, Point stop = null, IRect clip = null) => AddStrikeoutAnnot(quads, start, stop, clip); internal Annot add_squiggly_annot(Quad[] quads = null, Point start = null, Point stop = null, IRect clip = null) => AddSquigglyAnnot(quads, start, stop, clip); internal Annot add_stamp_annot(Rect rect, object stamp = null) => _add_stamp_annot(rect, stamp); - /// - /// , text_color=(0, 0, 0), cross_out=True). - /// - public Annot add_redact_annot(Quad quad, string text = null, string fontname = null, float fontsize = 11, - int align = 0, float[] fill = null, float[] text_color = null, bool cross_out = true) - => AddRedactAnnot(quad, text, fontName: fontname, fontSize: fontsize, align: align, fillColor: fill, textColor: text_color, crossOut: cross_out); - /// - /// , text_color=(0, 0, 0), cross_out=True). - /// - public Annot add_redact_annot(Rect rect, string text = null, string fontname = null, float fontsize = 11, - int align = 0, float[] fill = null, float[] text_color = null, bool cross_out = true) - => AddRedactAnnot(rect, text, fontName: fontname, fontSize: fontsize, align: align, fillColor: fill, textColor: text_color, crossOut: cross_out); // Annotation/link operations. internal bool apply_redactions(int images = 2, int graphics = 1, int text = 0) => ApplyRedactions(images, graphics, text); @@ -5718,60 +5689,6 @@ internal List search_for_rects(string needle, Quad clip = null, int max_hi => search_for(needle, clip, max_hits, flags, textpage); // Insertion helpers. - /// - /// PDF only: Insert text lines starting at point_like point. See Shape.insert_text. - /// - public int insert_text(Point point, string text, float fontsize = 11, string fontname = "helv", - float[] color = null, float rotate = 0, int render_mode = 0, float border_width = 0.05f, - float? miter_limit = null, Point morphFix = null, Matrix morphMat = null) - => InsertText(point, text, fontSize: fontsize, fontName: fontname, color: color, rotate: rotate, renderMode: render_mode, borderWidth: border_width, - miterLimit: miter_limit.HasValue ? (float?)miter_limit.Value : null, - morphFix: morphFix, morphMat: morphMat); - /// overload with snake_case parameters. - public InsertTextboxResult insert_textbox( - Rect rect, - string text, - float fontsize = 11, - string fontname = "helv", - float[] color = null, - int align = 0, - float border_width = 0.05f, - float expandtabs = 1, - int render_mode = 0, - int rotate = 0, - int encoding = 0, - float fill_opacity = 1, - float[] fill = null, - string fontfile = null, - float? lineheight = null, - int set_simple = 0, - Point morph_fix = null, - Matrix morph_mat = null, - bool overlay = true, - float stroke_opacity = 1, - int oc = 0) - => InsertTextbox( - rect, - text, - align: align, - borderWidth: border_width, - color: color, - encoding: encoding, - expandTabs: expandtabs, - fillOpacity: fill_opacity, - fill: fill, - fontFile: fontfile, - fontName: fontname, - fontSize: fontsize, - lineHeight: lineheight, - morphFix: morph_fix, - morphMat: morph_mat, - oc: oc, - overlay: overlay, - renderMode: render_mode, - rotate: rotate, - setSimple: set_simple, - strokeOpacity: stroke_opacity); internal (float spare_height, float scale) insert_htmlbox(Rect rect, string text, string css = null, float scale_low = 0, Archive archive = null, int rotate = 0, int oc = 0, float opacity = 1, bool overlay = true, bool scale_word_width = true, bool verbose = false) diff --git a/MuPDF.NET/Pixmap.cs b/MuPDF.NET/Pixmap.cs index c680436a..31d9406e 100644 --- a/MuPDF.NET/Pixmap.cs +++ b/MuPDF.NET/Pixmap.cs @@ -913,8 +913,8 @@ public Pixmap ToColorspace(Colorspace cs, bool alpha = true) /// /// The file to save to. May be provided as a string, as a pathlib.Path or as a Python file object. In the latter two cases, the filename is taken from the resp. object. The filename's extension determines the image format, which can be overruled by the output parameter. /// The desired image format. The default is the filename's extension. If both, this value and the file extension are unsupported, an exception is raised. For possible values see PixmapOutput. - /// The desired image quality, default 95. Only applies to JPEG images, else ignored. This parameter trades quality against file size. A value of 98 is close to lossless. Higher values should not lead to better quality. - public void Save(string filename, string output = null, int jpg_quality = 95) + /// The desired image quality, default 95. Only applies to JPEG images, else ignored. This parameter trades quality against file size. A value of 98 is close to lossless. Higher values should not lead to better quality. + public void Save(string filename, string output = null, int jpgQuality = 95) { if (filename == null) throw new ArgumentNullException(nameof(filename)); string path = filename; @@ -931,7 +931,7 @@ public void Save(string filename, string output = null, int jpg_quality = 95) throw new ValueErrorException($"unsupported colorspace for '{ext}'"); if (idx == 7) SetDpi(XRes, YRes); - WriteImage(path, idx.Value, jpg_quality); + WriteImage(path, idx.Value, jpgQuality); } private static int? FormatIndex(string ext) => ext switch @@ -945,7 +945,7 @@ public void Save(string filename, string output = null, int jpg_quality = 95) _ => null, }; - private void WriteImage(string filename, int format, int jpg_quality) + private void WriteImage(string filename, int format, int jpgQuality) { var pm = NativePixmap; switch (format) @@ -955,12 +955,12 @@ private void WriteImage(string filename, int format, int jpg_quality) case 3: mupdf.mupdf.fz_save_pixmap_as_pam(pm, filename); break; case 5: mupdf.mupdf.fz_save_pixmap_as_psd(pm, filename); break; case 6: mupdf.mupdf.fz_save_pixmap_as_ps(pm, filename, 0); break; - case 7: mupdf.mupdf.fz_save_pixmap_as_jpeg(pm, filename, jpg_quality); break; + case 7: mupdf.mupdf.fz_save_pixmap_as_jpeg(pm, filename, jpgQuality); break; default: mupdf.mupdf.fz_save_pixmap_as_png(pm, filename); break; } } - private byte[] ToBytesInternal(int format, int jpg_quality) + private byte[] ToBytesInternal(int format, int jpgQuality) { var pm = NativePixmap; int size = pm.fz_pixmap_stride() * pm.h(); @@ -973,7 +973,7 @@ private byte[] ToBytesInternal(int format, int jpg_quality) case 3: out_.fz_write_pixmap_as_pam(pm); break; case 5: out_.fz_write_pixmap_as_psd(pm); break; case 6: out_.fz_write_pixmap_as_ps(pm); break; - case 7: out_.fz_write_pixmap_as_jpeg(pm, jpg_quality, 0); break; + case 7: out_.fz_write_pixmap_as_jpeg(pm, jpgQuality, 0); break; default: out_.fz_write_pixmap_as_png(pm); break; } out_.fz_close_output(); @@ -983,8 +983,8 @@ private byte[] ToBytesInternal(int format, int jpg_quality) /// return a memory area in a variety of formats. /// /// The requested image format. The default is "png". For other possible values see PixmapOutput. - /// The desired image quality, default 95. Only applies to JPEG images, else ignored. This parameter trades quality against file size. A value of 98 is close to lossless. Higher values should not lead to better quality. - public byte[] ToBytes(string output = "png", int jpg_quality = 95) + /// The desired image quality, default 95. Only applies to JPEG images, else ignored. This parameter trades quality against file size. A value of 98 is close to lossless. Higher values should not lead to better quality. + public byte[] ToBytes(string output = "png", int jpgQuality = 95) { string ext = (output ?? "png").ToLowerInvariant(); int? idx = FormatIndex(ext); @@ -996,7 +996,7 @@ public byte[] ToBytes(string output = "png", int jpg_quality = 95) throw new ValueErrorException($"unsupported colorspace for '{output}'"); if (idx == 7) SetDpi(XRes, YRes); - return ToBytesInternal(idx.Value, jpg_quality); + return ToBytesInternal(idx.Value, jpgQuality); } /// /// Returns a one-page searchable PDF (OCR text layer) as bytes; alias for . diff --git a/MuPDF.NET/Story.cs b/MuPDF.NET/Story.cs index 475f75a7..af3f92d4 100644 --- a/MuPDF.NET/Story.cs +++ b/MuPDF.NET/Story.cs @@ -445,22 +445,22 @@ public void Write(DocumentWriter writer, RectFunction rectfn, Action p /// Final output writer; null during intermediate passes. /// Returns HTML from prior list (e.g. table of contents). Use for legacy naming. /// Pagination callback. - /// User CSS for each iteration. + /// User CSS for each iteration. /// Base font size. /// Called when layout has stabilized. /// Per-page hook (see ). /// Resource archive for images/fonts. - /// When true, assign ids to header tags without ids. + /// When true, assign ids to header tags without ids. public static void WriteStabilized( DocumentWriter writer, Func, string> contentfn, StoryRectFn rectfn, - string user_css = null, + string userCss = null, float em = 12, Action positionfn = null, Action pagefn = null, Archive archive = null, - bool add_header_ids = true) + bool addHeaderIds = true) { var positions = new List(); // content = None @@ -480,8 +480,8 @@ public static void WriteStabilized( // content2 = content string content2 = content; // story = Story(content2, user_css, em, archive) - using var story = new Story(content2, user_css, em, archive); - if (add_header_ids) + using var story = new Story(content2, userCss, em, archive); + if (addHeaderIds) // story.add_header_ids() story.AddHeaderIds(); @@ -514,22 +514,22 @@ void positionfn2(StoryElementPositionInfo position) /// /// HTML builder from element positions. /// Pagination callback. - /// User CSS. + /// User CSS. /// Base font size. /// Optional position callback on final pass. /// Optional page hook. /// Resource archive. - /// Assign header ids when missing. + /// Assign header ids when missing. /// PDF document with goto links for internal href anchors. public static Document WriteStabilizedWithLinks( Func, string> contentfn, StoryRectFn rectfn, - string user_css = null, + string userCss = null, float em = 12, Action positionfn = null, Action pagefn = null, Archive archive = null, - bool add_header_ids = true) + bool addHeaderIds = true) { // stream = io.BytesIO() var stream = mupdf.mupdf.fz_new_buffer(1024); @@ -546,7 +546,7 @@ void positionfn2(StoryElementPositionInfo position) positionfn(position); } // Story.write_stabilized(writer, contentfn, rectfn, user_css, em, positionfn2, pagefn, archive, add_header_ids) - WriteStabilized(writer, contentfn, rectfn, user_css, em, positionfn2, pagefn, archive, add_header_ids); + WriteStabilized(writer, contentfn, rectfn, userCss, em, positionfn2, pagefn, archive, addHeaderIds); // writer.close() byte[] pdf = writer.Close(); // stream.seek(0) @@ -804,60 +804,60 @@ float opposite(float? p, int direction) } /// - /// Finds the smallest scale in .. so the scaled contains the story. + /// Finds the smallest scale in .. so the scaled contains the story. /// /// Base rectangle (origin and size). - /// Minimum scale (≥ 0). - /// Maximum scale, or null for no upper bound. + /// Minimum scale (≥ 0). + /// Maximum scale, or null for no upper bound. /// Maximum error in returned scale. /// Log search diagnostics. /// Placement flags for internal calls. - public FitResult FitScale(Rect rect, float scale_min = 0, float? scale_max = null, float delta = 0.001f, bool verbose = false, int flags = 0) + public FitResult FitScale(Rect rect, float scaleMin = 0, float? scaleMax = null, float delta = 0.001f, bool verbose = false, int flags = 0) { float x0 = rect.X0; float y0 = rect.Y0; float width = rect.Width; float height = rect.Height; Rect fn(float scale) => new Rect(x0, y0, x0 + scale * width, y0 + scale * height); - return Fit(fn, scale_min, scale_max, delta, verbose, flags); + return Fit(fn, scaleMin, scaleMax, delta, verbose, flags); } /// /// Finds the smallest height for a fixed rectangle that contains the story. /// /// Rectangle width. - /// Minimum height (≥ 0). - /// Maximum height, or null for no upper bound. + /// Minimum height (≥ 0). + /// Maximum height, or null for no upper bound. /// Top-left corner of the rectangle (default 0, 0). /// Maximum error in returned height. /// Log search diagnostics. - public FitResult FitHeight(float width, float height_min = 0, float? height_max = null, Point origin = null, float delta = 0.001f, bool verbose = false) + public FitResult FitHeight(float width, float heightMin = 0, float? heightMax = null, Point origin = null, float delta = 0.001f, bool verbose = false) { origin ??= new Point(0, 0); float x0 = origin.X; float y0 = origin.Y; float x1 = x0 + width; Rect fn(float height) => new Rect(x0, y0, x1, y0 + height); - return Fit(fn, height_min, height_max, delta, verbose, 0); + return Fit(fn, heightMin, heightMax, delta, verbose, 0); } /// /// Finds the smallest width for a fixed rectangle that contains the story. /// /// Rectangle height. - /// Minimum width (≥ 0). - /// Maximum width, or null for no upper bound. + /// Minimum width (≥ 0). + /// Maximum width, or null for no upper bound. /// Top-left corner of the rectangle (default 0, 0). /// Maximum error in returned width. /// Log search diagnostics. - public FitResult FitWidth(float height, float width_min = 0, float? width_max = null, Point origin = null, float delta = 0.001f, bool verbose = false) + public FitResult FitWidth(float height, float widthMin = 0, float? widthMax = null, Point origin = null, float delta = 0.001f, bool verbose = false) { origin ??= new Point(0, 0); float x0 = origin.X; float y0 = origin.Y; float y1 = y0 + height; Rect fn(float width) => new Rect(x0, y0, x0 + width, y1); - return Fit(fn, width_min, width_max, delta, verbose, 0); + return Fit(fn, widthMin, widthMax, delta, verbose, 0); } // ─── MuPDF API names (internal, same assembly) ───────────────── diff --git a/MuPDF.NET/Table.cs b/MuPDF.NET/Table.cs index 082f4730..ea42498f 100644 --- a/MuPDF.NET/Table.cs +++ b/MuPDF.NET/Table.cs @@ -2701,7 +2701,7 @@ internal static Page PageRotationReset(Page page, int xref, int rot, Rect mediab /// /// /// Normalized tuple: - /// [x0, y0, x1, y1, "table"] — written by layout pipelines such as PDF4LLM + /// [x0, y0, x1, y1, "table"] — written by layout pipelines such as MuPDF.NET.PDF4LLM /// WritePageLayout after find_reading_order. /// /// diff --git a/MuPDF.NET/Utils.cs b/MuPDF.NET/Utils.cs index 55480181..13790df1 100644 --- a/MuPDF.NET/Utils.cs +++ b/MuPDF.NET/Utils.cs @@ -828,8 +828,8 @@ public static float GetTextLength( /// /// Basic image metadata : width, height, colorspace, bpc, ext, etc. /// - /// When non-zero, include the native fz_image handle under key image. - public static Dictionary ImageProperties(object img, int keep_image = 0) + /// When non-zero, include the native fz_image handle under key image. + public static Dictionary ImageProperties(object img, int keepImage = 0) { if (img == null) throw new ArgumentException("bad argument 'img'"); @@ -908,7 +908,7 @@ public static Dictionary ImageProperties(object img, int keep_im // result[dictkey_cs_name] = cs_name ["cs-name"] = csName, }; - if (keep_image != 0) + if (keepImage != 0) result["image"] = image; return result; } diff --git a/MuPDF.NET/runtime.json b/MuPDF.NET/runtime.json index 24f52bb3..184f9244 100644 --- a/MuPDF.NET/runtime.json +++ b/MuPDF.NET/runtime.json @@ -1,12 +1,12 @@ { "runtimes": { - "win-x86": { "MuPDF.NativeAssets.Windows.x86": "1.28.0.1" }, - "win-x64": { "MuPDF.NativeAssets.Windows.x64": "1.28.0.1" }, - "win-arm64": { "MuPDF.NativeAssets.Windows.arm64": "1.28.0.1" }, - "linux-x64": { "MuPDF.NativeAssets.Linux.x64": "1.28.0.1" }, - "linux-arm": { "MuPDF.NativeAssets.Linux.arm": "1.28.0.1" }, - "linux-arm64": { "MuPDF.NativeAssets.Linux.arm64": "1.28.0.1" }, - "osx-x64": { "MuPDF.NativeAssets.macOS.x64": "1.28.0.1" }, - "osx-arm64": { "MuPDF.NativeAssets.macOS.arm64": "1.28.0.1" } + "win-x86": { "MuPDF.NativeAssets.Windows.x86": "1.28.0.3" }, + "win-x64": { "MuPDF.NativeAssets.Windows.x64": "1.28.0.3" }, + "win-arm64": { "MuPDF.NativeAssets.Windows.arm64": "1.28.0.3" }, + "linux-x64": { "MuPDF.NativeAssets.Linux.x64": "1.28.0.3" }, + "linux-arm": { "MuPDF.NativeAssets.Linux.arm": "1.28.0.3" }, + "linux-arm64": { "MuPDF.NativeAssets.Linux.arm64": "1.28.0.3" }, + "osx-x64": { "MuPDF.NativeAssets.macOS.x64": "1.28.0.3" }, + "osx-arm64": { "MuPDF.NativeAssets.macOS.arm64": "1.28.0.3" } } } diff --git a/MuPDF/buildTransitive/MuPDF.NativeAssets.targets b/MuPDF/buildTransitive/MuPDF.NativeAssets.targets index b46d6bc2..f34a80b8 100644 --- a/MuPDF/buildTransitive/MuPDF.NativeAssets.targets +++ b/MuPDF/buildTransitive/MuPDF.NativeAssets.targets @@ -16,9 +16,18 @@ true + + + + PreserveNewest + + + - PreserveNewest @@ -26,8 +35,8 @@ - PreserveNewest diff --git a/MuPDF/native/win-arm64/.gitkeep b/MuPDF/native/win-arm64/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/MuPDF/native/win-arm64/mupdfcpp64.dll b/MuPDF/native/win-arm64/mupdfcpp64.dll new file mode 100644 index 00000000..6c90df11 Binary files /dev/null and b/MuPDF/native/win-arm64/mupdfcpp64.dll differ diff --git a/MuPDF/native/win-arm64/mupdfcsharp.dll b/MuPDF/native/win-arm64/mupdfcsharp.dll new file mode 100644 index 00000000..9b24cc93 Binary files /dev/null and b/MuPDF/native/win-arm64/mupdfcsharp.dll differ diff --git a/MuPDF/pack-nativeassets.ps1 b/MuPDF/pack-nativeassets.ps1 index 12cd2767..31b4b0a9 100644 --- a/MuPDF/pack-nativeassets.ps1 +++ b/MuPDF/pack-nativeassets.ps1 @@ -69,12 +69,24 @@ function Test-HasBinaries([string] $sourceDir) { return $files.Count -gt 0 } -function New-PlatformTargetsContent([string] $packageId, [string] $rid) { +function New-PlatformTargetsContent([string] $rid, [string] $nativeRoot) { + # Every installed MuPDF.NativeAssets.* package imports its own copy of this file, + # so the copy must be gated on the RID being built for. Without the gate the + # win-x86 and win-x64 packages both write mupdfcsharp.dll to the output directory + # and Linux/macOS binaries are copied into Windows builds. @" - - + win-x86 + win-x64 + win-arm64 + + win-x86 + win-x64 + + + @@ -112,9 +124,23 @@ foreach ($platform in $manifest.platforms) { New-Item -ItemType Directory -Force -Path $targetsDir | Out-Null $targetsFileName = "$packageId.targets" $targetsPath = Join-Path $targetsDir $targetsFileName - Set-Content -Path $targetsPath -Value (New-PlatformTargetsContent $packageId $rid) -Encoding UTF8 + Set-Content -Path $targetsPath -Value (New-PlatformTargetsContent $rid '..\runtimes') -Encoding UTF8 $fileLines.Add(" ") + # Legacy packages.config determines compatibility from lib/content assets and + # does not import buildTransitive. Add conventional empty reference assets and + # framework-specific build imports for supported .NET Framework targets. + $placeholderPath = Join-Path $stage '_._' + [System.IO.File]::WriteAllBytes($placeholderPath, [byte[]]@()) + foreach ($framework in @('net461', 'net472', 'net48')) { + $legacyTargetsDir = Join-Path $stage "build\$framework" + New-Item -ItemType Directory -Force -Path $legacyTargetsDir | Out-Null + Set-Content -Path (Join-Path $legacyTargetsDir $targetsFileName) ` + -Value (New-PlatformTargetsContent $rid '..\..\runtimes') -Encoding UTF8 + $fileLines.Add(" ") + $fileLines.Add(" ") + } + Copy-Item (Join-Path $root 'LICENSE.md') $stage $fileLines.Add(' ') @@ -131,6 +157,11 @@ foreach ($platform in $manifest.platforms) { Artifex C# MuPDF DotNet PDF nativeassets $rid + + + + + $filesSection diff --git a/NuGet.config b/NuGet.config deleted file mode 100644 index 174884f2..00000000 --- a/NuGet.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/PDF4LLM.Test/Pdf4LlmCollection.cs b/PDF4LLM.Test/Pdf4LlmCollection.cs deleted file mode 100644 index 9b0b5143..00000000 --- a/PDF4LLM.Test/Pdf4LlmCollection.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Xunit; - -namespace PDF4LLM.Test -{ - /// - /// Serializes tests that mutate (mirrors Python module globals). - /// - [CollectionDefinition("PDF4LLM", DisableParallelization = true)] - public class Pdf4LlmCollection - { - } -} diff --git a/PDF4LLM/Description.md b/PDF4LLM/Description.md deleted file mode 100644 index 21179517..00000000 --- a/PDF4LLM/Description.md +++ /dev/null @@ -1,37 +0,0 @@ -## About - -**PDF4LLM** provides LLM/RAG helpers for [MuPDF.NET](https://www.nuget.org/packages/MuPDF.NET): PDF-to-Markdown conversion, layout parsing, and document structure analysis. It is designed for use with RAG (Retrieval-Augmented Generation) pipelines and integration with LLMs. - -This package extends MuPDF.NET with: - -- **PDF-to-Markdown** — Convert PDF pages to Markdown with layout awareness (tables, headers, images) -- **Layout parsing** — Extract document structure (pages, boxes, tables, images) as JSON or structured objects -- **LlamaIndex integration** — `PDFMarkdownReader` for compatibility with LlamaIndex document loading -- **OCR support** — Optional OCR for scanned or image-heavy pages -- **Form fields** — Extract key/value pairs from interactive PDF forms - -Install with `dotnet add package PDF4LLM`. MuPDF.NET is installed automatically as a dependency. - -## PyMuPDF Layout (optional) - -AI-based page layout uses the Python package [pymupdf-layout](https://pypi.org/project/pymupdf-layout/) via an external worker. Install it once per machine: - -```bash -dotnet msbuild -t:PDF4LLMSetupLayoutPython -``` - -This creates a per-user Python venv (Windows: `%LOCALAPPDATA%\PDF4LLM\.venv-layout`, Linux/macOS: `~/.local/share/pdf4llm/.venv-layout`) and installs pinned `pymupdf` / `pymupdf-layout` wheels. PDF4LLM discovers that venv automatically. - -On Debian/Ubuntu, install system packages first: - -```bash -sudo apt install python3-venv python3-pip -``` - -Alternatively, install `pymupdf-layout` into any Python 3.10+ environment and set `PDF4LLM_PYTHON` to that interpreter. If layout is unavailable, PDF4LLM falls back to classic text extraction. - -Project-local venv: run `python path/to/setup_layout_python.py --venv .pdf4llm-venv` in your project directory (also auto-discovered). - -## License and Copyright - -**PDF4LLM** is part of MuPDF.NET and is available under the [Artifex Community License](https://github.com/ArtifexSoftware/MuPDF.NET/blob/main/LICENSE.md) and commercial license agreements. If you determine you cannot meet the requirements of the Artifex Community License, please [contact Artifex](https://artifex.com/contact/mupdf-net-inquiry.php) for more information regarding a commercial license. diff --git a/PDF4LLM/README.md b/PDF4LLM/README.md deleted file mode 100644 index c9962a0d..00000000 --- a/PDF4LLM/README.md +++ /dev/null @@ -1,183 +0,0 @@ -# PDF4LLM - -LLM/RAG helpers for [MuPDF.NET](https://www.nuget.org/packages/MuPDF.NET): convert PDFs to Markdown or plain text, analyze page layout, export structure as JSON, and load documents for retrieval pipelines. - -The public API lives in the **`PDF4LLM`** namespace. The main entry point is the static class **`PdfExtractor`**. - -**API naming:** Public members use C# conventions — PascalCase methods and properties, camelCase parameters (for example `writeImages`, `includeXrefs`, `useLayout`). Python-style names from [pymupdf4llm](https://pypi.org/project/pymupdf4llm/) appear only in internal port alignment and in the optional Python layout worker; customer-facing docs and IntelliSense use the C# names below. - -## Installation - -```bash -dotnet add package PDF4LLM -``` - -[MuPDF.NET](https://www.nuget.org/packages/MuPDF.NET) is installed automatically as a dependency — you do not need to add it separately. If your project already references MuPDF.NET, add `PDF4LLM` anyway; NuGet will resolve a compatible MuPDF.NET version. - -## PyMuPDF Layout (recommended) - -AI-based layout analysis uses the Python package [pymupdf-layout](https://pypi.org/project/pymupdf-layout/) through a small external worker process. When layout is available, `PdfExtractor` enables it automatically on first use. - -### One-time setup (NuGet consumers) - -Requires **Python 3.10+** on `PATH`. From your project directory: - -```bash -dotnet msbuild -t:PDF4LLMSetupLayoutPython -``` - -This creates a per-user venv and installs pinned `pymupdf` / `pymupdf-layout` wheels: - -| OS | Venv location | -|----|---------------| -| Windows | `%LOCALAPPDATA%\PDF4LLM\.venv-layout` | -| Linux / macOS | `~/.local/share/pdf4llm/.venv-layout` | - -PDF4LLM discovers that venv automatically. No environment variables are required. - -### Alternatives - -- Set **`PDF4LLM_PYTHON`** to any Python interpreter that has `pymupdf-layout` installed. -- Project-local venv (also auto-discovered): - - ```bash - python path/to/setup_layout_python.py --venv .pdf4llm-venv - ``` - -- Monorepo / source checkout: - - ```bash - python PDF4LLM/scripts/setup_layout_python.py - ``` - -If layout is not installed, PDF4LLM falls back to classic MuPDF text extraction. Check availability at runtime: - -```csharp -using PDF4LLM; -using PDF4LLM.Layout; - -bool layoutReady = PyMuPdfLayout.IsAvailable; // Python import probe -bool layoutActive = PdfExtractor.LayoutAvailable; // provider registered -``` - -## Quick start - -```csharp -using MuPDF.NET; -using PDF4LLM; - -// Path or open Document — both work -string markdown = PdfExtractor.ToMarkdown(@"C:\docs\report.pdf"); - -using Document doc = new Document("report.pdf"); -string text = PdfExtractor.ToText(doc); -string json = PdfExtractor.ToJson(doc); -var parsed = PdfExtractor.ParseDocument(doc); -var formData = PdfExtractor.GetKeyValues(doc); -``` - -### Selected pages and images - -```csharp -string md = PdfExtractor.ToMarkdown( - doc, - pages: new List { 0, 1, 2 }, - writeImages: true, - imagePath: @"C:\output\images", - imageFormat: "png"); -``` - -### Interactive form fields - -```csharp -// includeXrefs: true adds each widget's PDF xref (for Page.LoadWidget) -var fields = PdfExtractor.GetKeyValues(doc, includeXrefs: true); -foreach (var kv in fields) - Console.WriteLine($"{kv.Key}: {kv.Value["value"]}"); -``` - -### LlamaIndex-style loading - -```csharp -var reader = PdfExtractor.LlamaMarkdownReader(); -var docs = reader.LoadData("report.pdf", extraInfo: new Dictionary()); -foreach (var d in docs) - Console.WriteLine($"Page {d.ExtraInfo["page"]}: {d.Text}"); -``` - -### Markdown to PDF - -```csharp -using Document pdf = PdfExtractor.MarkdownToPdf(@"C:\docs\readme.md"); -pdf.Save("readme.pdf"); -``` - -### Layout on / off - -```csharp -PdfExtractor.SetUseLayout(useLayout: true); // default when pymupdf-layout is installed -PdfExtractor.SetUseLayout(useLayout: false); // legacy header detection (IdentifyHeaders, TocHeaders) -``` - -### OCR - -When layout mode is active, OCR is selected automatically via `LayoutParseHelpers.SelectOcrFunction()` when Tesseract or RapidOCR is available. Control behavior with `useOcr`, `forceOcr`, `ocrLanguage`, and optional `ocrFunction`: - -```csharp -using PDF4LLM.Ocr; - -string md = PdfExtractor.ToMarkdown( - doc, - useOcr: true, - forceOcr: false, - ocrLanguage: "eng"); -``` - -`OcrMode` values (layout pipeline): `Never`, `SelectDropOld`, `SelectKeepOld` (default), `ForceDropOld`, `ForceKeepOld`. - -## API overview - -| Member | Description | -|--------|-------------| -| `ToMarkdown()` | Document → Markdown (tables, headers, images) | -| `ToText()` | Document → plain text with the same layout pipeline | -| `ToJson()` | Layout structure as JSON | -| `ParseDocument()` | `ParsedDocument` with pages, boxes, tables, images | -| `GetKeyValues()` | Interactive form field names, values, and locations | -| `MarkdownToPdf()` | Markdown file → `Document` via MuPDF Story | -| `LlamaMarkdownReader()` | LlamaIndex-compatible page loader | -| `SetUseLayout()` | Enable or disable the layout pipeline | -| `SetLayoutProvider()` | Plug in a custom `Func` layout source | -| `LoadAiAsync()` | **net8.0 only** — chunk, embed, and index PDFs for RAG (`PDF4LLM.AI`) | - -Lower-level layout control: **`PDF4LLM.Layout.PyMuPdfLayout`** (`Activate`, `Deactivate`, `IsAvailable`, `Version`). - -Additional public helpers: `LayoutParseHelpers.ReadPageLayoutRaw`, `LayoutParseHelpers.SelectOcrFunction`, `GetTextLines.GetRawLines`, `Utils.Iou`, `Utils.TableToMarkdown`. - -## Common options - -`ToMarkdown`, `ToText`, and `ToJson` accept optional parameters including: - -| Parameter | Purpose | -|-----------|---------| -| `pages` | Restrict to specific pages (0-based) | -| `writeImages` / `embedImages` | Save image files or embed as base64 | -| `imagePath`, `imageFormat`, `filename` | Image output location and naming | -| `useOcr`, `ocrLanguage`, `forceOcr`, `ocrFunction` | OCR for scanned pages | -| `forceText` | Extract text even from picture regions (layout mode) | -| `pageChunks`, `pageSeparators` | Chunked or separated page output | -| `showProgress` | Log processing progress | -| `header`, `footer` | Include page header/footer text (layout mode) | - -`GetKeyValues` also accepts `includeXrefs` to include widget xref numbers in the result. - -## Requirements - -- **.NET:** netstandard2.0, net461, net472, net48, net5.0–net8.0 -- **MuPDF.NET:** 3.28.0 or newer (MuPDF bind **1.28.0** must match `PdfExtractor` at runtime) -- **Layout (optional):** Python 3.10+ with [pymupdf-layout](https://pypi.org/project/pymupdf-layout/) 1.28.0 -- **AI/RAG helpers:** net8.0 + `Microsoft.Extensions.AI` (included in the net8.0 package build) - -## License - -PDF4LLM is part of MuPDF.NET and is available under the [Artifex Community License](https://github.com/ArtifexSoftware/MuPDF.NET/blob/main/LICENSE.md) and commercial license agreements. For commercial licensing, [contact Artifex](https://artifex.com/contact/mupdf-net-inquiry.php). diff --git a/PDF4LLM/VersionInfo.cs b/PDF4LLM/VersionInfo.cs deleted file mode 100644 index a88ef5f7..00000000 --- a/PDF4LLM/VersionInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace PDF4LLM -{ - /// PDF4LLM package version metadata. - public static class VersionInfo - { - /// PDF4LLM NuGet package version. - public const string Version = Artifex.Versions.PDF4LLM; - - /// Required pymupdf-layout PyPI package version for the layout bridge. - public const string RequiredPyMuPDFLayout = Artifex.Versions.PyMuPDFLayout; - - /// Native MuPDF version bundled with MuPDF.NET. - public const string RequiredMuPdf = Artifex.Versions.MuPDF; - } -} diff --git a/PDF4LLM/build/PDF4LLM.targets b/PDF4LLM/build/PDF4LLM.targets deleted file mode 100644 index 115cc333..00000000 --- a/PDF4LLM/build/PDF4LLM.targets +++ /dev/null @@ -1,62 +0,0 @@ - - - $(MSBuildThisFileDirectory)layout\ - $(MSBuildThisFileDirectory)..\scripts\ - $(MSBuildThisFileDirectory)..\ocr\ocr_decision_model.onnx - $(MSBuildThisFileDirectory)ocr\ocr_decision_model.onnx - - - - - - - - - - <_PDF4LLMNetCoreVersion>$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)')) - <_PDF4LLMSupportsRapidOcr>$([MSBuild]::VersionGreaterThanOrEquals($(_PDF4LLMNetCoreVersion), 8.0)) - - - <_PDF4LLMSupportsRapidOcr>false - - - <_PDF4LLMRapidOcrModelsSource Condition="'$(PkgRapidOcrNet)' != ''">$(PkgRapidOcrNet)\models\v5 - <_PDF4LLMRapidOcrModelsSource Condition="'$(_PDF4LLMRapidOcrModelsSource)' == '' and '$(NuGetPackageRoot)' != ''">$(NuGetPackageRoot)rapidocrnet\2.0.0\models\v5 - - - - - - - - - - - - - - - - - - - - diff --git a/README.md b/README.md index 6751a521..2fee90bf 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ dotnet add package MuPDF.NET ``` -> **LLM / RAG use?** See the companion package [`PDF4LLM`](./PDF4LLM/README.md) for PDF-to-Markdown conversion and LlamaIndex integration, or install it separately: `dotnet add package PDF4LLM` +> **LLM / RAG use?** Install `MuPDF.NET.PDF4LLM` from NuGet, or build the **`MuPDF.NET.PDF4LLM`** project in this solution. + +Open **`MuPDF.NET.sln`** at the repository root. --- @@ -38,7 +40,7 @@ dotnet add package MuPDF.NET - **Versatile** — read, write, annotate, redact, merge, split, convert, and render documents - **Multi-language** — works in C#, F#, and Visual Basic; mirrors the PyMuPDF API so Python experience transfers directly - **Barcode support** — read and generate QR, Code 128, Data Matrix, PDF417, and more -- **LLM-ready** — via the `PDF4LLM` companion for Markdown output, layout parsing, and LlamaIndex loading +- **LLM-ready** — via the `MuPDF.NET.PDF4LLM` companion for Markdown output, layout parsing, and LlamaIndex loading --- @@ -46,7 +48,7 @@ dotnet add package MuPDF.NET | Requirement | Version | |---|---| -| .NET | 8.0 or later (.NET Standard 2.0 via `PDF4LLM`) | +| .NET | 8.0 or later (.NET Standard 2.0 via `MuPDF.NET.PDF4LLM`) | | Visual Studio | 2019 or 2022 (Windows build path) | | OS | Windows, Linux | @@ -136,7 +138,7 @@ doc.Close(); | **OCR** | OCR image-heavy pages via integrated Tesseract support | | **Images** | Extract, insert, and replace embedded images and vector graphics | | **Barcodes** | Scan and generate QR codes, Code 128, Data Matrix, PDF417 | -| **LLM / RAG** | Convert to Markdown with layout awareness via `PDF4LLM` companion package | +| **LLM / RAG** | Convert to Markdown with layout awareness via `MuPDF.NET.PDF4LLM` companion package | --- @@ -347,16 +349,6 @@ Building from source is only required if you want to modify the C# bindings or t See the [Getting Started](https://mupdfnet.readthedocs.io/en/latest/getting-started/index.html) guide for `dotnet` CLI instructions on Ubuntu/Debian. -**Samples** - -The `Demo` project runs curated examples for MuPDF.NET and PDF4LLM. See [Demo/README.md](./Demo/README.md): - -```bash -cd Demo -dotnet run # all samples (see Demo/README.md) -dotnet run -- help # full list (diagnostics marked [diag]) -``` - --- ## Documentation @@ -366,7 +358,7 @@ dotnet run -- help # full list (diagnostics marked [diag]) | Full API reference | https://mupdfnet.readthedocs.io | | 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 (`PDF4LLM`) | https://docs.pdf4llm.com/dotnet/getting-started/installation | +| LLM/RAG companion (`MuPDF.NET.PDF4LLM`) | https://docs.pdf4llm.com/dotnet/getting-started/installation | --- diff --git a/TestDocuments/PDF4LLM.Test/Test137/test_137.pdf b/TestDocuments/MuPDF.NET.PDF4LLM.Test/Test137/test_137.pdf similarity index 100% rename from TestDocuments/PDF4LLM.Test/Test137/test_137.pdf rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/Test137/test_137.pdf diff --git a/TestDocuments/PDF4LLM.Test/Test137/test_to_markdown_link_malicious.pdf b/TestDocuments/MuPDF.NET.PDF4LLM.Test/Test137/test_to_markdown_link_malicious.pdf similarity index 100% rename from TestDocuments/PDF4LLM.Test/Test137/test_to_markdown_link_malicious.pdf rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/Test137/test_to_markdown_link_malicious.pdf diff --git a/TestDocuments/PDF4LLM.Test/Test137/test_to_markdown_link_malicious.pdf.expected.md b/TestDocuments/MuPDF.NET.PDF4LLM.Test/Test137/test_to_markdown_link_malicious.pdf.expected.md similarity index 100% rename from TestDocuments/PDF4LLM.Test/Test137/test_to_markdown_link_malicious.pdf.expected.md rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/Test137/test_to_markdown_link_malicious.pdf.expected.md diff --git a/TestDocuments/PDF4LLM.Test/Test370/test_370.pdf b/TestDocuments/MuPDF.NET.PDF4LLM.Test/Test370/test_370.pdf similarity index 100% rename from TestDocuments/PDF4LLM.Test/Test370/test_370.pdf rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/Test370/test_370.pdf diff --git a/TestDocuments/PDF4LLM.Test/Test370/test_370_expected.md b/TestDocuments/MuPDF.NET.PDF4LLM.Test/Test370/test_370_expected.md similarity index 100% rename from TestDocuments/PDF4LLM.Test/Test370/test_370_expected.md rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/Test370/test_370_expected.md diff --git a/TestDocuments/PDF4LLM.Test/TestMarkdownToPdf/test_markdown_to_pdf-expected.md b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestMarkdownToPdf/test_markdown_to_pdf-expected.md similarity index 100% rename from TestDocuments/PDF4LLM.Test/TestMarkdownToPdf/test_markdown_to_pdf-expected.md rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/TestMarkdownToPdf/test_markdown_to_pdf-expected.md diff --git a/TestDocuments/PDF4LLM.Test/TestOcr/test_ocr_loremipsum_FFFD.pdf b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOcr/test_ocr_loremipsum_FFFD.pdf similarity index 100% rename from TestDocuments/PDF4LLM.Test/TestOcr/test_ocr_loremipsum_FFFD.pdf rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOcr/test_ocr_loremipsum_FFFD.pdf diff --git a/TestDocuments/PDF4LLM.Test/TestOcr/test_ocr_loremipsum_svg.pdf b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOcr/test_ocr_loremipsum_svg.pdf similarity index 100% rename from TestDocuments/PDF4LLM.Test/TestOcr/test_ocr_loremipsum_svg.pdf rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOcr/test_ocr_loremipsum_svg.pdf diff --git a/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/pages.docx b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/pages.docx new file mode 100644 index 00000000..0a460bca Binary files /dev/null and b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/pages.docx differ diff --git a/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/pages.odt b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/pages.odt new file mode 100644 index 00000000..d89cd655 Binary files /dev/null and b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/pages.odt differ diff --git a/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/test_4159.doc b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/test_4159.doc new file mode 100644 index 00000000..fc164523 Binary files /dev/null and b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/test_4159.doc differ diff --git a/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/test_4496.hwpx b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/test_4496.hwpx new file mode 100644 index 00000000..f1e0fb1a Binary files /dev/null and b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestOffice/test_4496.hwpx differ diff --git a/TestDocuments/PDF4LLM.Test/TestSce150/test_sce_150_1.expected.md b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestSce150/test_sce_150_1.expected.md similarity index 100% rename from TestDocuments/PDF4LLM.Test/TestSce150/test_sce_150_1.expected.md rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/TestSce150/test_sce_150_1.expected.md diff --git a/TestDocuments/PDF4LLM.Test/TestSce150/test_sce_150_2.expected.md b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestSce150/test_sce_150_2.expected.md similarity index 100% rename from TestDocuments/PDF4LLM.Test/TestSce150/test_sce_150_2.expected.md rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/TestSce150/test_sce_150_2.expected.md diff --git a/TestDocuments/PDF4LLM.Test/TestSce150/test_sce_150_3.expected.md b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestSce150/test_sce_150_3.expected.md similarity index 100% rename from TestDocuments/PDF4LLM.Test/TestSce150/test_sce_150_3.expected.md rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/TestSce150/test_sce_150_3.expected.md diff --git a/TestDocuments/PDF4LLM.Test/TestTablulate/test_tablulate_bug.pdf b/TestDocuments/MuPDF.NET.PDF4LLM.Test/TestTablulate/test_tablulate_bug.pdf similarity index 100% rename from TestDocuments/PDF4LLM.Test/TestTablulate/test_tablulate_bug.pdf rename to TestDocuments/MuPDF.NET.PDF4LLM.Test/TestTablulate/test_tablulate_bug.pdf diff --git a/Versions.props b/Versions.props index 11aa32f3..f08c3b1b 100644 --- a/Versions.props +++ b/Versions.props @@ -1,16 +1,22 @@ - - - - - 1.28.0.1 - - 3.28.1.3 - - 1.28.0.2 - - 1.28.0 - - + + + + + 1.28.0.3 + + + 3.28.1.6 + + + 1.28.0.3 + + + 1.28.0.3 + + + 1.28.0 + +