From cf9517f8b44cdcc3c0ecb1ad5b7eca72ae67c96d Mon Sep 17 00:00:00 2001 From: Maksym Tkachuk Date: Mon, 23 Mar 2026 15:07:50 +0200 Subject: [PATCH 1/2] added updates to fix issue #234 --- Demo/Program.cs | 17 +++++++++++++ MuPDF.NET.Test/UtilsTest.cs | 50 +++++++++++++++++++++++++++++++++++++ MuPDF.NET/Page.cs | 10 +++++++- MuPDF.NET/Utils.cs | 12 ++++----- 4 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 MuPDF.NET.Test/UtilsTest.cs diff --git a/Demo/Program.cs b/Demo/Program.cs index 19fcedf..08fc6ef 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -85,10 +85,27 @@ static void Main(string[] args) TestGetText(); TestMarkdownReader(); TestRecompressJBIG2(); + TestIssue234(); return; } + static void TestIssue234() + { + Console.WriteLine("\n=== TestIssue234 ======================="); + + var pix = new Pixmap("../../../TestDocuments/Image/boxedpage.jpg"); // 629x1000 image + var scaled = new Pixmap(pix, 943, 1500, null); // scale up + byte[] jpeg = scaled.ToBytes("jpg", 65); + + using var doc = new Document(); + Page page = doc.NewPage(0, 943, 1500); + page.InsertImage(page.Rect, stream: jpeg); + page.Dispose(); + doc.Save("issue_234.pdf"); + doc.Close(); + } + static void TestRecompressJBIG2() { Console.WriteLine("\n=== TestJBIG2 ======================="); diff --git a/MuPDF.NET.Test/UtilsTest.cs b/MuPDF.NET.Test/UtilsTest.cs new file mode 100644 index 0000000..2aeb0e7 --- /dev/null +++ b/MuPDF.NET.Test/UtilsTest.cs @@ -0,0 +1,50 @@ +using NUnit.Framework; +using MuPDF.NET; + +namespace MuPDF.NET.Test +{ + public class UtilsTest + { + [Test] + public void FloatToString_NoScientificNotation() + { + Assert.That(Utils.FloatToString(1.5f), Is.EqualTo("1.5")); + Assert.That(Utils.FloatToString(0f), Is.EqualTo("0")); + Assert.That(Utils.FloatToString(-123.456f), Is.EqualTo("-123.456")); + Assert.That(Utils.FloatToString(1000000f), Is.EqualTo("1000000")); + + // Values that would use scientific notation with default ToString + string small = Utils.FloatToString(0.0000123f); + Assert.That(small.Contains("E") || small.Contains("e"), Is.False, "Should not use scientific notation"); + Assert.That(small.Contains("0.00001") || small.Contains("0.000012"), Is.True); + } + + [Test] + public void FloatToString_InvariantCulture() + { + string result = Utils.FloatToString(1.5f); + Assert.That(result.Contains("."), Is.True); + Assert.That(result.Contains(","), Is.False); + } + + [Test] + public void DoubleToString_NoScientificNotation() + { + Assert.That(Utils.DoubleToString(1.5), Is.EqualTo("1.5")); + Assert.That(Utils.DoubleToString(0), Is.EqualTo("0")); + Assert.That(Utils.DoubleToString(-123.456), Is.EqualTo("-123.456")); + Assert.That(Utils.DoubleToString(1000000), Is.EqualTo("1000000")); + + string small = Utils.DoubleToString(1.23e-10); + Assert.That(small.Contains("E") || small.Contains("e"), Is.False, "Should not use scientific notation"); + } + + [Test] + public void DoubleToString_InvariantCulture() + { + string result = Utils.DoubleToString(1.5); + Assert.That(result.Contains("."), Is.True); + Assert.That(result.Contains(","), Is.False); + } + } +} diff --git a/MuPDF.NET/Page.cs b/MuPDF.NET/Page.cs index b3e5484..4d47a8c 100644 --- a/MuPDF.NET/Page.cs +++ b/MuPDF.NET/Page.cs @@ -2242,7 +2242,15 @@ public int InsertImage( xobject.pdf_dict_puts(imgName, ref_); FzBuffer nres = mupdf.mupdf.fz_new_buffer(50); nres.fz_append_string( - string.Format(System.Globalization.CultureInfo.InvariantCulture, template, mat.a, mat.b, mat.c, mat.d, mat.e, mat.f, imgName) + string.Format(System.Globalization.CultureInfo.InvariantCulture, + template, + Utils.FloatToString(mat.a), + Utils.FloatToString(mat.b), + Utils.FloatToString(mat.c), + Utils.FloatToString(mat.d), + Utils.FloatToString(mat.e), + Utils.FloatToString(mat.f), + imgName) ); Utils.InsertContents(pageDoc, page.obj(), nres, overlay); } diff --git a/MuPDF.NET/Utils.cs b/MuPDF.NET/Utils.cs index bf551ce..a777e08 100644 --- a/MuPDF.NET/Utils.cs +++ b/MuPDF.NET/Utils.cs @@ -7913,19 +7913,19 @@ internal static void SetDotCultureForNumber() } /// - /// Converts a float to string with dot as decimal separator, regardless of culture + /// Converts a float to string with dot as decimal separator, without scientific notation. /// - internal static string FloatToString(float value) + public static string FloatToString(float value) { - return value.ToString(CultureInfo.InvariantCulture); + return value.ToString("0.#######", CultureInfo.InvariantCulture); } /// - /// Converts a double to string with dot as decimal separator, regardless of culture + /// Converts a double to string with dot as decimal separator, without scientific notation. /// - internal static string DoubleToString(double value) + public static string DoubleToString(double value) { - return value.ToString(CultureInfo.InvariantCulture); + return value.ToString("0.#################", CultureInfo.InvariantCulture); } /// From a1ed555a12fd19fc7af2d3768e047eae459cb120 Mon Sep 17 00:00:00 2001 From: Maksym Tkachuk Date: Wed, 1 Apr 2026 22:16:28 +0300 Subject: [PATCH 2/2] added updates for NET4LLM new version 0.3.4 --- .gitignore | 3 + Demo/Demo.csproj | 3 +- Demo/GlobalUsings.cs | 19 + Demo/Program.cs | 1863 +---------------- Demo/SampleMenu.cs | 171 ++ .../Annotations/NewAnnots.cs} | 0 .../Program.Annotations.FreeText.cs | 75 + Demo/Samples/Barcodes/Program.Barcodes.cs | 341 +++ Demo/Samples/Document/Program.Document.cs | 125 ++ .../ImageFilters/Program.ImageFilters.cs | 86 + .../Program.Llm.PdfMarkdownReader.Fixtures.cs | 49 + .../Llm/Program.Llm.ToMarkdown.Fixtures.cs | 211 ++ Demo/Samples/Llm/Program.Llm.cs | 437 ++++ .../PageContent/Program.PageContent.cs | 291 +++ Demo/Samples/Regression/Program.Regression.cs | 168 ++ .../TextDrawing/Program.TextDrawing.cs | 366 ++++ Demo/{_Constants.cs => Support/Constants.cs} | 0 Demo/Support/Units.cs | 14 + Demo/annotations-freetext1.cs | 42 - Demo/annotations-freetext2.cs | 65 - MuPDF.NET.sln | 1 - MuPDF.NET/MuPDF.NET.csproj | 9 + MuPDF.NET/MuPDF.NET.nuspec | 34 +- MuPDF.NET4LLM/Description.md | 17 + MuPDF.NET4LLM/LICENSE.md | 60 + MuPDF.NET4LLM/MuPDF.NET4LLM.csproj | 5 +- MuPDF.NET4LLM/MuPDF.NET4LLM.nuspec | 58 + MuPDF.NET4LLM/README.md | 95 + MuPDF.NET4LLM/VersionInfo.cs | 2 +- MuPDF.NET4LLM/helpers/DocumentLayout.cs | 1 + MuPDF.NET4LLM/logo.png | Bin 0 -> 59275 bytes MuPDF.NET4LLM/ocr/__init__.cs | 14 + MuPDF.NET4LLM/ocr/paddleocr_api.cs | 23 + MuPDF.NET4LLM/ocr/paddletess_api.cs | 26 + MuPDF.NET4LLM/ocr/rapidocr_api.cs | 29 + MuPDF.NET4LLM/ocr/rapidtess_api.cs | 33 + .../CheckOcr.cs => ocr/tesseract_api.cs} | 126 +- 37 files changed, 2800 insertions(+), 2062 deletions(-) create mode 100644 Demo/GlobalUsings.cs create mode 100644 Demo/SampleMenu.cs rename Demo/{new-annots.cs => Samples/Annotations/NewAnnots.cs} (100%) create mode 100644 Demo/Samples/Annotations/Program.Annotations.FreeText.cs create mode 100644 Demo/Samples/Barcodes/Program.Barcodes.cs create mode 100644 Demo/Samples/Document/Program.Document.cs create mode 100644 Demo/Samples/ImageFilters/Program.ImageFilters.cs create mode 100644 Demo/Samples/Llm/Program.Llm.PdfMarkdownReader.Fixtures.cs create mode 100644 Demo/Samples/Llm/Program.Llm.ToMarkdown.Fixtures.cs create mode 100644 Demo/Samples/Llm/Program.Llm.cs create mode 100644 Demo/Samples/PageContent/Program.PageContent.cs create mode 100644 Demo/Samples/Regression/Program.Regression.cs create mode 100644 Demo/Samples/TextDrawing/Program.TextDrawing.cs rename Demo/{_Constants.cs => Support/Constants.cs} (100%) create mode 100644 Demo/Support/Units.cs delete mode 100644 Demo/annotations-freetext1.cs delete mode 100644 Demo/annotations-freetext2.cs create mode 100644 MuPDF.NET4LLM/Description.md create mode 100644 MuPDF.NET4LLM/LICENSE.md create mode 100644 MuPDF.NET4LLM/MuPDF.NET4LLM.nuspec create mode 100644 MuPDF.NET4LLM/README.md create mode 100644 MuPDF.NET4LLM/logo.png create mode 100644 MuPDF.NET4LLM/ocr/__init__.cs create mode 100644 MuPDF.NET4LLM/ocr/paddleocr_api.cs create mode 100644 MuPDF.NET4LLM/ocr/paddletess_api.cs create mode 100644 MuPDF.NET4LLM/ocr/rapidocr_api.cs create mode 100644 MuPDF.NET4LLM/ocr/rapidtess_api.cs rename MuPDF.NET4LLM/{helpers/CheckOcr.cs => ocr/tesseract_api.cs} (69%) diff --git a/.gitignore b/.gitignore index 6990476..f659cee 100644 --- a/.gitignore +++ b/.gitignore @@ -395,5 +395,8 @@ FodyWeavers.xsd *.msm *.msp +# Local clone of https://github.com/pymupdf/pymupdf4llm for porting / diff (optional) +pymupdf4llm/ + # JetBrains Rider *.sln.iml diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj index 4d71245..1590b31 100644 --- a/Demo/Demo.csproj +++ b/Demo/Demo.csproj @@ -1,4 +1,5 @@ + Exe @@ -7,8 +8,8 @@ - + diff --git a/Demo/GlobalUsings.cs b/Demo/GlobalUsings.cs new file mode 100644 index 0000000..9541be4 --- /dev/null +++ b/Demo/GlobalUsings.cs @@ -0,0 +1,19 @@ +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Text; +global using System.Threading; +global using mupdf; +global using MuPDF.NET; +global using MuPDF.NET4LLM; +global using MuPDF.NET4LLM.Helpers; +global using MuPDF.NET4LLM.Llama; +global using SkiaSharp; +global using Box = MuPDF.NET.Box; +global using Encoding = System.Text.Encoding; +global using File = System.IO.File; +global using Font = MuPDF.NET.Font; +global using Morph = MuPDF.NET.Morph; +global using TextWriter = MuPDF.NET.TextWriter; +global using Utils = MuPDF.NET.Utils; diff --git a/Demo/Program.cs b/Demo/Program.cs index 08fc6ef..e72dce8 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -1,1864 +1,13 @@ -using mupdf; -using MuPDF.NET; -using MuPDF.NET4LLM; -using MuPDF.NET4LLM.Helpers; -using MuPDF.NET4LLM.Llama; -using SkiaSharp; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using static ICSharpCode.SharpZipLib.Zip.ExtendedUnixData; -using static System.Net.Mime.MediaTypeNames; -using static System.Net.WebRequestMethods; -using Box = MuPDF.NET.Box; -using Encoding = System.Text.Encoding; -using File = System.IO.File; -using Font = MuPDF.NET.Font; -using Morph = MuPDF.NET.Morph; -using TextWriter = MuPDF.NET.TextWriter; -using Utils = MuPDF.NET.Utils; - namespace Demo { - public static class Units + /// + /// GitHub samples entry point. With no arguments, all samples run; see . + /// + internal partial class Program { - // Constants - public const float InchesPerMm = 1.0f / 25.4f; - public const float PointsPerInch = 72.0f; - - // --- mm <-> points (PostScript points: 1 pt = 1/72 in) --- - public static float MmToPoints(float mm) => mm * InchesPerMm * PointsPerInch; // = mm * 72 / 25.4 - public static float PointsToMm(float points) => points / PointsPerInch / InchesPerMm; // = points * 25.4 / 72 - - // --- mm <-> pixels (requires device DPI) --- - public static float MmToPixels(float mm, float dpi) => mm * InchesPerMm * dpi; - public static float PixelsToMm(float px, float dpi) => px / dpi / InchesPerMm; - } - class Program - { - static void Main(string[] args) - { - TestInsertHtmlbox(); - TestLineAnnot(); - AnnotationsFreeText1.Run(args); - AnnotationsFreeText2.Run(args); - NewAnnots.Run(args); - TestHelloWorldToNewDocument(args); - TestHelloWorldToExistingDocument(args); - TestReadBarcode(args); - TestReadDataMatrix(); - TestWriteBarcode(args); - TestExtractTextWithLayout(args); - TestWidget(args); - TestColor(args); - TestCMYKRecolor(args); - TestSVGRecolor(args); - TestReplaceImage(args); - TestInsertImage(args); - TestGetImageInfo(args); - TestGetTextPageOcr(args); - TestCreateImagePage(args); - TestJoinPdfPages(args); - TestFreeTextAnnot(args); - TestTextFont(args); - TestMemoryLeak(); - TestDrawLine(); - TestWriteBarcode1(); - TestUnicodeDocument(); - TestMorph(); - TestMetadata(); - TestMoveFile(); - TestImageFilter(); - TestImageFilterOcr(); - CreateAnnotDocument(); - TestDrawShape(); - TestIssue213(); - TestIssue1880(); - TestLLM(); - TestPyMuPdfRagToMarkdown(); - TestTable(); - TestGetText(); - TestMarkdownReader(); - TestRecompressJBIG2(); - TestIssue234(); - - return; - } - - static void TestIssue234() - { - Console.WriteLine("\n=== TestIssue234 ======================="); - - var pix = new Pixmap("../../../TestDocuments/Image/boxedpage.jpg"); // 629x1000 image - var scaled = new Pixmap(pix, 943, 1500, null); // scale up - byte[] jpeg = scaled.ToBytes("jpg", 65); - - using var doc = new Document(); - Page page = doc.NewPage(0, 943, 1500); - page.InsertImage(page.Rect, stream: jpeg); - page.Dispose(); - doc.Save("issue_234.pdf"); - doc.Close(); - } - - static void TestRecompressJBIG2() - { - Console.WriteLine("\n=== TestJBIG2 ======================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/Jbig2.pdf"); - - Document doc = new Document(testFilePath); - - PdfImageRewriterOptions opts = new PdfImageRewriterOptions(); - - opts.bitonal_image_recompress_method = mupdf.mupdf.FZ_RECOMPRESS_FAX; - opts.recompress_when = mupdf.mupdf.FZ_RECOMPRESS_WHEN_ALWAYS; - - doc.RewriteImage(options: opts); - - doc.Save(@"e:\TestRecompressJBIG2.pdf"); - doc.Close(); - } - - static void TestMarkdownReader() - { - Console.WriteLine("\n=== TestMarkdownReader ======================="); - - var reader = new PDFMarkdownReader(); - string testFilePath = Path.GetFullPath("../../../TestDocuments/columns.pdf"); - - var docs = reader.LoadData(testFilePath); - - foreach (var doc in docs) - { - Console.WriteLine(doc.Text); - } - } - - static void TestGetText() - { - Console.WriteLine("\n=== TestGetText ======================="); - - var reader = new PDFMarkdownReader(); - string testFilePath = Path.GetFullPath("../../../TestDocuments/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(); - } - - static void TestTable() - { - Console.WriteLine("\n=== TestTable ======================="); - - try - { - string testFilePath = Path.GetFullPath("../../../TestDocuments/err_table.pdf"); - - if (!File.Exists(testFilePath)) - { - Console.WriteLine($"Error: Test file not found: {testFilePath}"); - return; - } - - Console.WriteLine($"Loading PDF: {testFilePath}"); - 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, - vertical_strategy: "lines_strict", - horizontal_strategy: "lines_strict"); - - Console.WriteLine($"Found {tables.Count} table(s) on page 0"); - - if (tables.Count > 0) - { - for (int i = 0; i < tables.Count; i++) - { - 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..."); - 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)}..."); - } - } - catch (Exception ex) - { - Console.WriteLine($" Error converting to markdown: {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, - vertical_strategy: "lines_strict", - horizontal_strategy: "lines_strict"); - - if (pageTables.Count > 0) - { - Console.WriteLine($" Page {pageNum}: {pageTables.Count} table(s)"); - totalTables += pageTables.Count; - } - currentPage.Dispose(); - } - Console.WriteLine($"Total tables found across all pages: {totalTables}"); - - page.Dispose(); - doc.Close(); - - Console.WriteLine("\n=== TestTable completed successfully ==="); - } - catch (Exception ex) - { - Console.WriteLine($"Error in TestTable: {ex.Message}"); - Console.WriteLine($"Stack trace: {ex.StackTrace}"); - throw; - } - } - - static void TestPyMuPdfRagToMarkdown() - { - Console.WriteLine("\n=== TestPyMuPdfRagToMarkdown ======================="); - - try - { - // Find a test PDF file - //string testFilePath = Path.GetFullPath("../../../TestDocuments/national-capitals.pdf"); - string testFilePath = Path.GetFullPath("../../../TestDocuments/Magazine.pdf"); - - Document doc = new Document(testFilePath); - Console.WriteLine($"Document loaded: {doc.PageCount} page(s)"); - Console.WriteLine($"Document name: {doc.Name}"); - - // Test 1: Basic ToMarkdown with default settings - Console.WriteLine("\n--- Test 1: Basic ToMarkdown (default settings) ---"); - try - { - List pages = new List(); - pages.Add(0); - string markdown = MuPdfRag.ToMarkdown( - doc, - pages: pages, // All pages - hdrInfo: null, // Auto-detect headers - writeImages: false, - embedImages: false, - ignoreImages: false, - ignoreGraphics: false, - detectBgColor: true, - imagePath: "", - imageFormat: "png", - imageSizeLimit: 0.05f, - filename: testFilePath, - forceText: true, - pageChunks: false, - pageSeparators: false, - margins: null, - dpi: 150, - pageWidth: 612, - pageHeight: null, - tableStrategy: "lines_strict", - graphicsLimit: null, - fontsizeLimit: 3.0f, - ignoreCode: false, - extractWords: false, - showProgress: false, - useGlyphs: false, - ignoreAlpha: false - ); - - string markdownFile = "TestPyMuPdfRag_Output.md"; - File.WriteAllText(markdownFile, markdown, Encoding.UTF8); - Console.WriteLine($"Markdown output saved to: {markdownFile}"); - Console.WriteLine($"Markdown length: {markdown.Length} characters"); - if (markdown.Length > 0) - { - int previewLength = Math.Min(300, markdown.Length); - Console.WriteLine($"Preview (first {previewLength} chars):\n{markdown.Substring(0, previewLength)}..."); - } - } - catch (Exception ex) - { - 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 = "TestPyMuPdfRag_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 = "TestPyMuPdfRag_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 = "TestPyMuPdfRag_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 = "TestPyMuPdfRag_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) - { - Console.WriteLine($"An unexpected error occurred during PyMuPdfRag test: {ex.Message}"); - Console.WriteLine($"Stack trace: {ex.StackTrace}"); - } - - Console.WriteLine("\n=== TestPyMuPdfRagToMarkdown Completed ======================="); - } - - static void TestLLM() - { - Console.WriteLine("\n=== TestLLM ======================="); - - try - { - // Display version information - Console.WriteLine($"MuPDF.NET4LLM Version: {MuPDF4LLM.Version}"); - var versionTuple = MuPDF4LLM.VersionTuple; - Console.WriteLine($"Version Tuple: ({versionTuple.major}, {versionTuple.minor}, {versionTuple.patch})"); - - // Test with a sample PDF file - string testFilePath = Path.GetFullPath("../../../TestDocuments/national-capitals.pdf"); - //string testFilePath = Path.GetFullPath("../../../TestDocuments/Magazine.pdf"); - - // Try to find a PDF with actual content if Blank.pdf doesn't work well - if (!File.Exists(testFilePath)) - { - testFilePath = Path.GetFullPath("../../../TestDocuments/Widget.pdf"); - } - - if (!File.Exists(testFilePath)) - { - Console.WriteLine($"Test PDF file not found. Skipping LLM test."); - return; - } - - Console.WriteLine($"\nTesting with PDF: {testFilePath}"); - - Document doc = new Document(testFilePath); - Console.WriteLine($"Document loaded: {doc.PageCount} page(s)"); - - string markdownStr = MuPDF4LLM.ToMarkdown(doc); - - doc.Close(); - - string markdownFile = "TestLLM.md"; - File.WriteAllText(markdownFile, markdownStr, Encoding.UTF8); - Console.WriteLine("\nLLM test completed successfully."); - } - catch (Exception ex) - { - Console.WriteLine($"Error in TestLLM: {ex.Message}"); - Console.WriteLine($"Stack trace: {ex.StackTrace}"); - } - } - - static void TestIssue1880() - { - Console.WriteLine("\n=== TestIssue1880 ======================="); - - string testFilePath = Path.GetFullPath(@"../../../TestDocuments/issue_1880.pdf"); - - Document doc = new Document(testFilePath); - - for (int i = 0; i < doc.PageCount; i++) - { - Page page = doc[i]; - - List barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.DM, pureBarcode:true); - foreach (Barcode barcode in barcodes) - { - BarcodePoint[] points = barcode.ResultPoints; - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); - } - - page.Dispose(); - } - - doc.Close(); - } - - static void TestIssue213() - { - Console.WriteLine("\n=== TestIssue213 ======================="); - - string origfilename = @"../../../TestDocuments/issue_213.pdf"; - string outfilename = @"../../../TestDocuments/Blank.pdf"; - float newWidth = 0.5f; - - Document inputDoc = new Document(origfilename); - Document outputDoc = new Document(outfilename); - - if (inputDoc.PageCount != outputDoc.PageCount) - { - return; - } - - for (int pagNum = 0; pagNum < inputDoc.PageCount; pagNum++) - { - Page page = inputDoc.LoadPage(pagNum); - - Pixmap pxmp = page.GetPixmap(); - pxmp.Save(@"output.png"); - pxmp.Dispose(); - - Page outPage = outputDoc.LoadPage(pagNum); - List paths = page.GetDrawings(extended: false); - int totalPaths = paths.Count; - - int i = 0; - foreach (PathInfo pathInfo in paths) - { - Shape shape = outPage.NewShape(); - foreach (Item item in pathInfo.Items) - { - if (item != null) - { - if (item.Type == "l") - { - shape.DrawLine(item.P1, item.LastPoint); - //writer.Write($"{i:000}\\] line: {item.Type} >>> {item.P1}, {item.LastPoint}\\n"); - } - else if (item.Type == "re") - { - shape.DrawRect(item.Rect, item.Orientation); - //writer.Write($"{i:000}\\] rect: {item.Type} >>> {item.Rect}, {item.Orientation}\\n"); - } - else if (item.Type == "qu") - { - shape.DrawQuad(item.Quad); - //writer.Write($"{i:000}\\] quad: {item.Type} >>> {item.Quad}\\n"); - } - else if (item.Type == "c") - { - shape.DrawBezier(item.P1, item.P2, item.P3, item.LastPoint); - //writer.Write($"{i:000}\\] curve: {item.Type} >>> {item.P1}, {item.P2}, {item.P3}, {item.LastPoint}\\n"); - } - else - { - throw new Exception("unhandled drawing. Aborting..."); - } - } - } - - //pathInfo.Items.get - float newLineWidth = pathInfo.Width; - if (pathInfo.Width <= newWidth) - { - newLineWidth = newWidth; - } - - int lineCap = 0; - if (pathInfo.LineCap != null && pathInfo.LineCap.Count > 0) - lineCap = (int)pathInfo.LineCap[0]; - shape.Finish( - fill: pathInfo.Fill, - color: pathInfo.Color, //this.\_m_DEFAULT_COLOR, - evenOdd: pathInfo.EvenOdd, - closePath: pathInfo.ClosePath, - lineJoin: (int)pathInfo.LineJoin, - lineCap: lineCap, - width: newLineWidth, - strokeOpacity: pathInfo.StrokeOpacity, - fillOpacity: pathInfo.FillOpacity, - dashes: pathInfo.Dashes - ); - - // file_export.write(f'Path {i:03}\] width: {lwidth}, dashes: {path\["dashes"\]}, closePath: {path\["closePath"\]}\\n') - //writer.Write($"Path {i:000}\\] with: {newLineWidth}, dashes: {pathInfo.Dashes}, closePath: {pathInfo.ClosePath}\\n"); - - i++; - shape.Commit(); - } - } - - inputDoc.Close(); - - outputDoc.Save(@"output.pdf"); - outputDoc.Close(); - - //writer.Close(); - } - - static void CreateAnnotDocument() - { - Console.WriteLine("\n=== CreateAnnotDocument ======================="); - Rect r = Constants.r; // use the rectangle defined in Constants.cs - - Document doc = new Document(); - Page page = doc.NewPage(); - - page.SetRotation(0); // no rotation - - TextWriter pw = new TextWriter(page.TrimBox); - string txt = "Origin 100.100"; - pw.Append(new Point(100, 500), txt, new Font("tiro"), fontSize: 24); - pw.WriteText(page, new float[]{0,0.4f,1}, oc: 0); - - - - Annot annot = page.AddRectAnnot(r); // 'Square' - annot.SetBorder(width: 1f, dashes: new int[] { 1, 2 }); - annot.SetColors(stroke: Constants.blue, fill: Constants.gold); - annot.Update(opacity: 0.5f); - - doc.Save(@"CreateAnnotDocument.pdf"); - - doc.Close(); - } - - static void TestDrawShape() - { - string origfilename = @"../../../TestDocuments/NewAnnots.pdf"; - string outfilename = @"../../../TestDocuments/Blank.pdf"; - float newWidth = 0.5f; - - Document inputDoc = new Document(origfilename); - Document outputDoc = new Document(outfilename); - - //string filePath = @"D:\\Vectorlab\\Jobs\\2025\\PACE\\pdf_fix\\assets\\exported_paths_net.txt"; - //StreamWriter writer = new StreamWriter(filePath); - - if (inputDoc.PageCount != outputDoc.PageCount) - { - return; - } - - for (int pagNum = 0; pagNum < inputDoc.PageCount; pagNum++) - { - Page page = inputDoc.LoadPage(pagNum); - Page outPage = outputDoc.LoadPage(pagNum); - List paths = page.GetDrawings(extended: false); - int totalPaths = paths.Count; - - int i = 0; - foreach (PathInfo pathInfo in paths) - { - Shape shape = outPage.NewShape(); - foreach (Item item in pathInfo.Items) - { - if (item != null) - { - if (item.Type == "l") - { - shape.DrawLine(item.P1, item.LastPoint); - //writer.Write($"{i:000}\\] line: {item.Type} >>> {item.P1}, {item.LastPoint}\\n"); - } - else if (item.Type == "re") - { - shape.DrawRect(item.Rect, item.Orientation); - //writer.Write($"{i:000}\\] rect: {item.Type} >>> {item.Rect}, {item.Orientation}\\n"); - } - else if (item.Type == "qu") - { - shape.DrawQuad(item.Quad); - //writer.Write($"{i:000}\\] quad: {item.Type} >>> {item.Quad}\\n"); - } - else if (item.Type == "c") - { - shape.DrawBezier(item.P1, item.P2, item.P3, item.LastPoint); - //writer.Write($"{i:000}\\] curve: {item.Type} >>> {item.P1}, {item.P2}, {item.P3}, {item.LastPoint}\\n"); - } - else - { - throw new Exception("unhandled drawing. Aborting..."); - } - } - } - - //pathInfo.Items.get - float newLineWidth = pathInfo.Width; - if (pathInfo.Width <= newWidth) - { - newLineWidth = newWidth; - } - - int lineCap = 0; - if (pathInfo.LineCap != null && pathInfo.LineCap.Count > 0) - lineCap = (int)pathInfo.LineCap[0]; - shape.Finish( - fill: pathInfo.Fill, - color: pathInfo.Color, //this.\_m_DEFAULT_COLOR, - evenOdd: pathInfo.EvenOdd, - closePath: pathInfo.ClosePath, - lineJoin: (int)pathInfo.LineJoin, - lineCap: lineCap, - width: newLineWidth, - strokeOpacity: pathInfo.StrokeOpacity, - fillOpacity: pathInfo.FillOpacity, - dashes: pathInfo.Dashes - ); - - // file_export.write(f'Path {i:03}\] width: {lwidth}, dashes: {path\["dashes"\]}, closePath: {path\["closePath"\]}\\n') - //writer.Write($"Path {i:000}\\] with: {newLineWidth}, dashes: {pathInfo.Dashes}, closePath: {pathInfo.ClosePath}\\n"); - - i++; - shape.Commit(); - } - } - - inputDoc.Close(); - - outputDoc.Save(@"TestDrawShape.pdf"); - outputDoc.Close(); - - //writer.Close(); - } - - static void TestImageFilter() - { - const string inputPath = @"../../../TestDocuments/Image/table.jpg"; - const string outputPath = @"output.png"; - - // Load the image file into SKBitmap - using (var bitmap = SKBitmap.Decode(inputPath)) - { - if (bitmap == null) - { - Console.WriteLine("Failed to load image."); - return; - } - - SKBitmap inputBitmap = bitmap.Copy(); - - // build the pipeline - var pipeline = new ImageFilterPipeline(); - - // clear any defaults if you’re reusing the instance - pipeline.Clear(); - - // add filters one-by-one - pipeline.AddDeskew(minAngle: 0.5); // replaces any existing deskew step - pipeline.AddRemoveHorizontalLines(); // also replaces existing horizontal-removal step - pipeline.AddRemoveVerticalLines(); - pipeline.AddGrayscale(); - //pipeline.AddMedian(blockSize: 2, replaceExisting: true); - //pipeline.AddGamma(gamma: 1.2); // brighten slightly - //pipeline.AddContrast(contrast: 100); - //pipeline.AddFit(100); - //pipeline.AddDilation(); - //pipeline.AddScale(scaleFactor: 1.75, quality: SKFilterQuality.Medium); - pipeline.AddInvert(); - - // apply the pipeline (bitmap is modified in place) - pipeline.Apply(ref inputBitmap); - - using (var data = inputBitmap.Encode(SKEncodedImageFormat.Png, 100)) // 100 = quality - { - using (var stream = File.OpenWrite(outputPath)) - { - data.SaveTo(stream); - } - } - - Console.WriteLine($"Loaded image: {bitmap.Width}x{bitmap.Height} pixels"); - } - } - - static void TestImageFilterOcr() - { - const string inputPath = @"../../../TestDocuments/Image/boxedpage.jpg"; - - using (Pixmap pxmp = new Pixmap(inputPath)) - { - // build the pipeline - var pipeline = new ImageFilterPipeline(); - - // clear any defaults if you’re reusing the instance - pipeline.Clear(); - - // add filters one-by-one - //pipeline.AddDeskew(minAngle: 0.5); // replaces any existing deskew step - //pipeline.AddRemoveHorizontalLines(); // also replaces existing horizontal-removal step - //pipeline.AddRemoveVerticalLines(); - //pipeline.AddGrayscale(); - //pipeline.AddMedian(blockSize: 2, replaceExisting: true); - pipeline.AddGamma(gamma: 1.2); // brighten slightly - //pipeline.AddContrast(contrast: 100); - //pipeline.AddScaleFit(100); - //pipeline.AddDilation(); - pipeline.AddScale(scaleFactor: 1.75, quality: SKFilterQuality.High); - //pipeline.AddInvert(); - - string txt = pxmp.GetTextFromOcr(pipeline); - Console.WriteLine(txt); - } - } - - static void TestMoveFile() - { - string origfilename = @"../../../TestDocuments/Blank.pdf"; - - string filePath = @"testmove.pdf"; - - File.Copy(origfilename, filePath, true); - - Document d = new Document(filePath); - - Page page = d[0]; - - Point tl = new Point(100, 120); - Point br = new Point(300, 150); - - Rect rect = new Rect(tl, br); - - TextWriter pw = new TextWriter(page.TrimBox); - /* - Font font = new Font(fontName: "tiro"); - - List<(string, float)> ret = pw.FillTextbox(rect, "This is a test to overwrite the original file and move it", font, fontSize: 24); - */ - pw.WriteText(page); - - page.Dispose(); - - MemoryStream tmp = new MemoryStream(); - - d.Save(tmp, garbage: 3, deflateFonts: 1, deflate: 1); - - d.Close(); - - File.WriteAllBytes(filePath, tmp.ToArray()); - - tmp.Dispose(); - - File.Move(filePath, @"moved.pdf", true); - } - - static void TestMetadata() - { - Console.WriteLine("\n=== TestMetadata ====================="); - - string testFilePath = @"../../../TestDocuments/Annot.pdf"; - - Document doc = new Document(testFilePath); - - Dictionary metaDict = doc.MetaData; - - foreach (string key in metaDict.Keys) - { - Console.WriteLine(key + ": " + metaDict[key]); - } - - doc.Close(); - - Console.WriteLine("TestMetadata completed."); - } - - static void TestMorph() - { - Console.WriteLine("\n=== TestMorph ====================="); - - string testFilePath = @"../../../TestDocuments/Morph.pdf"; - - Document doc = new Document(testFilePath); - Page page = doc[0]; - Rect printrect = new Rect(180, 30, 650, 60); - int pagerot = page.Rotation; - TextWriter pw = new TextWriter(page.TrimBox); - string txt = "Origin 100.100"; - pw.Append(new Point(100, 100), txt, new Font("tiro"), fontSize: 24); - pw.WriteText(page); - - txt = "rotated 270 - 100.100"; - Matrix matrix = new IdentityMatrix(); - matrix.Prerotate(270); - Morph mo = new Morph(new Point(100, 100), matrix); - pw = new TextWriter(page.TrimBox); - pw.Append(new Point(100, 100), txt, new Font("tiro"), fontSize: 24); - pw.WriteText(page, morph:mo); - page.SetRotation(270); - - page.Dispose(); - doc.Save(@"morph.pdf"); - doc.Close(); - } - - static void TestUnicodeDocument() - { - Console.WriteLine("\n=== TestUnicodeDocument ====================="); - - string testFilePath = @"../../../TestDocuments/你好.pdf"; - - Document doc = new Document(testFilePath); - - doc.Save(@"你好_.pdf"); - doc.Close(); - - Console.WriteLine("TestUnicodeDocument completed."); - } - - static void TestWriteBarcode1() - { - Console.WriteLine("\n=== TestWriteBarcode1 ====================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/Blank.pdf"); - Document doc = new Document(testFilePath); - - Page page = doc[0]; - - // CODE39 - Rect rect = new Rect( - X0: Units.MmToPoints(50), - X1: Units.MmToPoints(80), - Y0: Units.MmToPoints(70), - Y1: Units.MmToPoints(85)); - - page.WriteBarcode(rect, "JJBEA6500", BarcodeFormat.CODE39, forceFitToRect: true, pureBarcode: true, narrowBarWidth:1); - - rect = new Rect( - X0: Units.MmToPoints(50), - X1: Units.MmToPoints(160), - Y0: Units.MmToPoints(100), - Y1: Units.MmToPoints(105)); - - page.WriteBarcode(rect, "JJBEA6500", BarcodeFormat.CODE39, forceFitToRect: true, pureBarcode: true, narrowBarWidth: 2); - - // CODE128 - Rect rect1 = new Rect( - X0: Units.MmToPoints(50), - X1: Units.MmToPoints(100), - Y0: Units.MmToPoints(50), - Y1: Units.MmToPoints(60)); - - page.WriteBarcode(rect1, "JJBEA6500063000000177922", BarcodeFormat.CODE128, forceFitToRect: false, pureBarcode: true, narrowBarWidth: 1); - - rect1 = new Rect( - X0: Units.MmToPoints(50), - X1: Units.MmToPoints(200), - Y0: Units.MmToPoints(80), - Y1: Units.MmToPoints(120)); - - page.WriteBarcode(rect1, "JJBEA6500063000000177922", BarcodeFormat.CODE128, forceFitToRect: true, pureBarcode: true, narrowBarWidth: 1); - - Rect rect2 = new Rect( - X0: Units.MmToPoints(100), - X1: Units.MmToPoints(140), - Y0: Units.MmToPoints(40), - Y1: Units.MmToPoints(80)); - - page.WriteBarcode(rect2, "01030000110444408000", BarcodeFormat.DM, forceFitToRect: false, pureBarcode: true, narrowBarWidth: 3); - - Pixmap pxmp = Utils.GetBarcodePixmap("JJBEA6500063000000177922", BarcodeFormat.CODE128, width: 500, pureBarcode: true, marginLeft:0, marginTop:0, marginRight:0, marginBottom:0, narrowBarWidth: 1); - - pxmp.Save(@"PxmpBarcode3.png"); - - byte[] imageBytes = pxmp.ToBytes(); - - using var stream = new SKMemoryStream(imageBytes); - using var codec = SKCodec.Create(stream); - var info = codec.Info; - var bitmap = SKBitmap.Decode(codec); - - using var data = bitmap.Encode(SKEncodedImageFormat.Png, 100); // 100 = quality - using var stream1 = File.OpenWrite(@"output.png"); - data.SaveTo(stream1); - - doc.Save(@"TestWriteBarcode1.pdf"); - - page.Dispose(); - doc.Close(); - - Console.WriteLine("TestWriteBarcode1 completed."); - } - - static void TestReadDataMatrix() - { - int i = 0; - - Console.WriteLine("\n=== TestReadDataMatrix ======================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/Barcodes/datamatrix.pdf"); - Document doc = new Document(testFilePath); - - Page page = doc[0]; - - List barcodes = page.ReadBarcodes(decodeEmbeddedOnly: false); - - foreach (Barcode barcode in barcodes) - { - BarcodePoint[] points = barcode.ResultPoints; - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); - } - /* - List blocks = page.GetImageInfo(); - - foreach (Block block in blocks) - { - Rect blockRect = block.Bbox; - barcodes = page.ReadBarcodes(clip:blockRect); - foreach (Barcode barcode in barcodes) - { - BarcodePoint[] points = barcode.ResultPoints; - if (points.Length == 2) - { - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); - } - else if (points.Length == 4) - { - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[2]}]"); - } - } - } - */ - /* - List imlist = page.GetImages(); - foreach (Entry im in imlist) - { - ImageInfo img = doc.ExtractImage(im.Xref); - File.WriteAllBytes(@"copy.png", img.Image); - - List barcodes = Utils.ReadBarcodes(@"copy.png", new Rect(0,0,img.Width,img.Height)); - - foreach (Barcode barcode in barcodes) - { - BarcodePoint[] points = barcode.ResultPoints; - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); - } - } - */ - - page.Dispose(); - doc.Close(); - } - - static void TestMemoryLeak() - { - Console.WriteLine("\n=== TestMemoryLeak ======================="); - string testFilePath = Path.GetFullPath("../../../TestDocuments/Blank.pdf"); - - for (int i = 0; i < 100; i++) - { - Document doc = new Document(testFilePath); - Page page = doc.NewPage(); - page.Dispose(); - doc.Close(); - } - - Console.WriteLine("Memory leak test completed. No leaks should be detected."); - } - - static void DrawLine(Page page, float startX, float startY, float endX, float endY, Color lineColor = null, float lineWidth = 1, bool dashed = false) - { - Console.WriteLine("\n=== DrawLine ======================="); - - if (lineColor == null) - { - lineColor = new Color(); // Default to black - lineColor.Stroke = new float[] { 0, 0, 0 }; // RGB black - } - Shape img = page.NewShape(); - Point startPoint = new Point(startX, startY); - Point endPoint = new Point(endX, endY); - - String dashString = ""; - if (dashed == true) - { - dashString = "[2] 0"; // Example dash pattern - } - - img.DrawLine(startPoint, endPoint); - img.Finish(width: lineWidth, color: lineColor.Stroke, dashes: dashString); - img.Commit(); - - Console.WriteLine($"Line drawn from ({startX}, {startY}) to ({endX}, {endY}) with color {lineColor.Stroke} and width {lineWidth}."); - } - - static void TestDrawLine() - { - Console.WriteLine("\n=== TestDrawLine ======================="); - - Document doc = new Document(); - - Page page = doc.NewPage(); - - string fontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); - - page.DrawLine(new Point(45, 50), new Point(80, 50), width: 0.5f, dashes: "[5] 0"); - page.DrawLine(new Point(90, 50), new Point(150, 50), width: 0.5f, dashes: "[5] 0"); - page.DrawLine(new Point(45, 80), new Point(180, 80), width: 0.5f, dashes: "[5] 0"); - page.DrawLine(new Point(45, 100), new Point(180, 100), width: 0.5f, dashes: "[5] 0"); - - //DrawLine(page, 45, 50, 80, 50, lineWidth: 0.5f, dashed: true); - //DrawLine(page, 90, 60, 150, 60, lineWidth: 0.5f, dashed: true); - //DrawLine(page, 45, 80, 180, 80, lineWidth: 0.5f, dashed: true); - //DrawLine(page, 45, 100, 180, 100, lineWidth: 0.5f, dashed: true); - - doc.Save(@"TestDrawLine.pdf"); - - page.Dispose(); - doc.Close(); - - Console.WriteLine("Write to TestDrawLine.pdf"); - } - - static void TestTextFont(string[] args) - { - Console.WriteLine("\n=== TestTextFont ======================="); - //for (int i = 0; i < 100; i++) - { - Document doc = new Document(); - - Page page0 = doc.NewPage(); - Page page1 = doc.NewPage(pno: -1, width: 595, height: 842); - - string fontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); - - float[] blue = new float[] { 0.0f, 0.0f, 1.0f }; - float[] red = new float[] { 1.0f, 0.0f, 0.0f }; - - Rect rect1 = new Rect(100, 100, 510, 200); - Rect rect2 = new Rect(100, 250, 300, 400); - - MuPDF.NET.Font font1 = new MuPDF.NET.Font("asdfasdf"); - //MuPDF.NET.Font font1 = new MuPDF.NET.Font("arial", fontDir+"\\arial_0.ttf"); - MuPDF.NET.Font font2 = new MuPDF.NET.Font("times", fontDir + "\\times.ttf"); - - string text1 = "This is a test of the FillTextbox method with Arial font."; - string text2 = "This is another test with Times New Roman font."; - - MuPDF.NET.TextWriter tw1 = new MuPDF.NET.TextWriter(page0.Rect); - tw1.FillTextbox(rect: rect1, text: text1, font: font1, fontSize:20); - font1.Dispose(); - tw1.WriteText(page0); - - MuPDF.NET.TextWriter tw2 = new MuPDF.NET.TextWriter(page0.Rect, color: red); - tw2.FillTextbox(rect: rect2, text: text2, font: font2, fontSize: 10, align: (int)TextAlign.TEXT_ALIGN_LEFT); - font2.Dispose(); - tw2.WriteText(page0); - - doc.Save(@"TestTextFont.pdf"); - - page0.Dispose(); - doc.Close(); - - Console.WriteLine("Write to TestTextFont.pdf"); - } - - } - - static void TestInsertHtmlbox() - { - Console.WriteLine("\n=== TestInsertHtmlbox ======================="); - - Rect rect = new Rect(100, 100, 550, 2250); - Document doc = new Document(); - Page page = doc.NewPage(); - - string htmlString = "

生产准备:

1. 每日生产进行维护保养,请参照并填写Philips 自动螺丝起点检表《WI-Screw assembly-Makita DF010&Kilews

SKD-B512L-F01》

2 .扭力计UNIT选择‘lbf.in’,‘P-P’模式,每四小时检查一次,每次检查5组数据,只有合格才可以生产;并填写

力扭矩记录表,表单号 : F-EN-34 。

1.电动起子力矩: 5±1 in-lbs,电动螺丝起编号:5.0。

2.电动起子力矩:10±1 in-lbs,电动螺丝起编号:10.0。

3.电动起子力矩:12±1 in-lbs,电动螺丝起编号:12.0。

Colten - line break

生产准备:

1. 每日生产进行维护保养,请参照并填写Philips 自动螺丝起点检表《WI-Screw assembly-Makita DF010&Kilews

SKD-B512L-F01》

2 .扭力计UNIT选择‘lbf.in’,‘P-P’模式,每四小时检查一次,每次检查5组数据,只有合格才可以生产;并填写

力扭矩记录表,表单号 : F-EN-34 。

1.电动起子力矩: 5±1 in-lbs,电动螺丝起编号:5.0。

2.电动起子力矩:10±1 in-lbs,电动螺丝起编号:10.0。

3.电动起子力矩:12±1 in-lbs,电动螺丝起编号:12.0。

Colten - line break

生产准备:

1. 每日生产进行维护保养,请参照并填写Philips 自动螺丝起点检表《WI-Screw assembly-Makita DF010&Kilews

SKD-B512L-F01》

2 .扭力计UNIT选择‘lbf.in’,‘P-P’模式,每四小时检查一次,每次检查5组数据,只有合格才可以生产;并填写

力扭矩记录表,表单号 : F-EN-34 。

1.电动起子力矩: 5±1 in-lbs,电动螺丝起编号:5.0。

2.电动起子力矩:10±1 in-lbs,电动螺丝起编号:10.0。

3.电动起子力矩:12±1 in-lbs,电动螺丝起编号:12.0。

Colten - line break

生产准备:

1. 每日生产进行维护保养,请参照并填写Philips 自动螺丝起点检表《WI-Screw assembly-Makita DF010&Kilews

SKD-B512L-F01》

2 .扭力计UNIT选择‘lbf.in’,‘P-P’模式,每四小时检查一次,每次检查5组数据,只有合格才可以生产;并填写

力扭矩记录表,表单号 : F-EN-34 。

1.电动起子力矩: 5±1 in-lbs,电动螺丝起编号:5.0。

2.电动起子力矩:10±1 in-lbs,电动螺丝起编号:10.0。

3.电动起子力矩:12±1 in-lbs,电动螺丝起编号:12.0。

"; - (float s, float scale) = page.InsertHtmlBox(rect, htmlString, scaleLow: 0f); - doc.Save(@"TestInsertHtmlbox.pdf"); - - page.Dispose(); - doc.Close(); - - Console.WriteLine($"Inserted HTML box with scale: {scale} and size: {s}"); - } - - static void TestLineAnnot() - { - Console.WriteLine("\n=== TestLineAnnot ======================="); - Document newDoc = new Document(); - Page newPage = newDoc.NewPage(); - - newPage.AddLineAnnot(new Point(100, 100), new Point(300, 300)); - - newDoc.Save(@"TestLineAnnot1.pdf"); - newDoc.Close(); - - Document doc = new Document(@"TestLineAnnot1.pdf"); // open a document - List annotationsToUpdate = new List(); - Page page = doc[0]; - // Fix: Correctly handle the IEnumerable returned by GetAnnots() - IEnumerable annots = page.GetAnnots(); - foreach (Annot annot in annots) - { - Console.WriteLine("Annotation on page width before modified: " + annot.Border.Width); - annot.SetBorder(width: 8); - annot.Update(); - Console.WriteLine("Annotation on page width after modified: " + annot.Border.Width); - } - annotationsToUpdate.Clear(); - doc.Save(@"TestLineAnnot2.pdf"); // Save the modified document - doc.Close(); // Close the document - } - - static void TestHelloWorldToNewDocument(string[] args) - { - Console.WriteLine("\n=== TestHelloWorldToNewDocument ======================="); - Document doc = new Document(); - Page page = doc.NewPage(); - - //{ "helv", "Helvetica" }, - //{ "heit", "Helvetica-Oblique" }, - //{ "hebo", "Helvetica-Bold" }, - //{ "hebi", "Helvetica-BoldOblique" }, - //{ "cour", "Courier" }, - //{ "cobo", "Courier-Bold" }, - //{ "cobi", "Courier-BoldOblique" }, - //{ "tiro", "Times-Roman" }, - //{ "tibo", "Times-Bold" }, - //{ "tiit", "Times-Italic" }, - //{ "tibi", "Times-BoldItalic" }, - //{ "symb", "Symbol" }, - //{ "zadb", "ZapfDingbats" } - MuPDF.NET.TextWriter writer = new MuPDF.NET.TextWriter(page.Rect); - var ret = writer.FillTextbox(page.Rect, "Hello World!", new MuPDF.NET.Font(fontName: "helv"), rtl: true); - writer.WriteText(page); - doc.Save("text.pdf", pretty: 1); - doc.Close(); - - Console.WriteLine($"Text written to 'text.pdf' in: {page.Rect}"); - } - - static void TestHelloWorldToExistingDocument(string[] args) - { - Console.WriteLine("\n=== TestHelloWorldToExistingDocument ======================="); - string testFilePath = Path.GetFullPath("../../../TestDocuments/Blank.pdf"); - Document doc = new Document(testFilePath); - - Page page = doc[0]; - - Rect rect = new Rect(100, 100, 510, 210); - page.DrawRect(rect); - - MuPDF.NET.TextWriter writer = new MuPDF.NET.TextWriter(page.Rect); - //Font font = new Font("kenpixel", "../../../kenpixel.ttf", isBold: 1); - Font font = new Font("cobo", isBold: 0); - var ret = writer.FillTextbox(page.Rect, "123456789012345678901234567890Peter Test- this is a string that is too long to fit into the TextBox", font, rtl: false); - writer.WriteText(page); - - doc.Save("text1.pdf", pretty: 1); - - doc.Close(); - - Console.WriteLine($"Text written to 'text1.pdf' in: {page.Rect}"); - } - - static void TestReadBarcode(string[] args) - { - int i = 0; - - Console.WriteLine("\n=== TestReadBarcode ======================="); - - Console.WriteLine("--- Read from image file ----------"); - string testFilePath1 = Path.GetFullPath("../../../TestDocuments/Barcodes/rendered.bmp"); - - Rect rect1 = new Rect(1260, 390, 1720, 580); - List barcodes2 = Utils.ReadBarcodes(testFilePath1, clip:rect1); - - i = 0; - foreach (Barcode barcode in barcodes2) - { - BarcodePoint[] points = barcode.ResultPoints; - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); - } - - Console.WriteLine("--- Read from pdf file ----------"); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/Barcodes/Samples.pdf"); - Document doc = new Document(testFilePath); - - Page page = doc[0]; - //Rect rect = new Rect(290, 590, 420, 660); - List barcodes = page.ReadBarcodes(); - - foreach (Barcode barcode in barcodes) - { - BarcodePoint[] points = barcode.ResultPoints; - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); - } - doc.Close(); - } - - static void TestReadQrCode(string[] args) - { - Console.WriteLine("\n=== TestReadQrCode ======================="); - int i = 0; - /* - Console.WriteLine("=== Read from image file ====================="); - string testFilePath1 = Path.GetFullPath("../../../TestDocuments/Barcodes/2.png"); - - List barcodes2 = Utils.ReadBarcodes(testFilePath1, autoRotate:true); - - i = 0; - foreach (Barcode barcode in barcodes2) - { - BarcodePoint[] points = barcode.ResultPoints; - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); - } - */ - ///* - Console.WriteLine("--- Read from pdf file ----------"); - - string testImagePath = @"test.png"; - string testFilePath = Path.GetFullPath("../../../TestDocuments/Barcodes/input.pdf"); - Document doc = new Document(testFilePath); - - Page page = doc[0]; - page.RemoveRotation(); // remove rotation to read barcodes correctly - - // Apply 2x scale (both X and Y) - var matrix = new Matrix(3.0f, 3.0f); - - // Render the page using the scaled matrix - var pixmap = page.GetPixmap(matrix); - - pixmap.GammaWith(3.2f); // apply gamma correction to improve barcode detection - - pixmap.Save(testImagePath); - - /* - Rect rect = new Rect(400, 700, page.Rect.X1, page.Rect.Y1); - List barcodes = page.ReadBarcodes(rect); - - foreach (Barcode barcode in barcodes) - { - BarcodePoint[] points = barcode.ResultPoints; - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); - } - */ - - pixmap.Dispose(); - doc.Close(); - - List barcodes2 = Utils.ReadBarcodes(testImagePath); - - i = 0; - foreach (Barcode barcode in barcodes2) - { - BarcodePoint[] points = barcode.ResultPoints; - Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); - } - //*/ - } - - static void TestWriteBarcode(string[] args) - { - Console.WriteLine("\n=== TestWriteBarcode ======================="); - Console.WriteLine("--- Write to pdf file ----------"); - string testFilePath = Path.GetFullPath("../../../TestDocuments/Blank.pdf"); - Document doc = new Document(testFilePath); - Page page = doc[0]; - - MuPDF.NET.TextWriter writer = new MuPDF.NET.TextWriter(page.Rect); - Font font = new Font("cour", isBold: 1); - writer.FillTextbox(page.Rect, "QR_CODE", font, pos: new Point(0, 10)); - writer.FillTextbox(page.Rect, "EAN_8", font, pos: new Point(0, 110)); - writer.FillTextbox(page.Rect, "EAN_13", font, pos: new Point(0, 165)); - writer.FillTextbox(page.Rect, "UPC_A", font, pos: new Point(0, 220)); - writer.FillTextbox(page.Rect, "CODE_39", font, pos: new Point(0, 275)); - writer.FillTextbox(page.Rect, "CODE_128", font, pos: new Point(0, 330)); - writer.FillTextbox(page.Rect, "ITF", font, pos: new Point(0, 385)); - writer.FillTextbox(page.Rect, "PDF_417", font, pos: new Point(0, 440)); - writer.FillTextbox(page.Rect, "CODABAR", font, pos: new Point(0, 520)); - writer.FillTextbox(page.Rect, "DATA_MATRIX", font, pos: new Point(0, 620)); - writer.WriteText(page); - - // QR_CODE - Rect rect = new Rect(100, 20, 300, 80); - page.WriteBarcode(rect, "Hello World!", BarcodeFormat.QR, forceFitToRect:false, pureBarcode:false, marginLeft:0); - - // EAN_8 - rect = new Rect(100, 100, 300, 120); - page.WriteBarcode(rect, "1234567", BarcodeFormat.EAN8, forceFitToRect: false, pureBarcode: false, marginBottom: 20); - - // EAN_13 - rect = new Rect(100, 155, 300, 200); - page.WriteBarcode(rect, "123456789012", BarcodeFormat.EAN13, forceFitToRect: false, pureBarcode: true, marginBottom: 0); - - // UPC_A - rect = new Rect(100, 210, 300, 255); - page.WriteBarcode(rect, "123456789012", BarcodeFormat.UPC_A, forceFitToRect: false, pureBarcode: true, marginBottom: 0); - - // CODE_39 - rect = new Rect(100, 265, 600, 285); - page.WriteBarcode(rect, "Hello World!", BarcodeFormat.CODE39, forceFitToRect: false, pureBarcode: false, marginBottom: 0); - - // CODE_128 - rect = new Rect(100, 320, 400, 355); - page.WriteBarcode(rect, "Hello World!", BarcodeFormat.CODE128, forceFitToRect: true, pureBarcode: true, marginBottom: 0); - - // ITF - rect = new Rect(100, 385, 300, 420); - page.WriteBarcode(rect, "12345678901234567890", BarcodeFormat.I2OF5, forceFitToRect: false, pureBarcode: false, marginBottom: 0); - - // PDF_417 - rect = new Rect(100, 430, 400, 435); - page.WriteBarcode(rect, "Hello World!", BarcodeFormat.PDF417, forceFitToRect: false, pureBarcode: true, marginBottom: 0); - - // CODABAR - rect = new Rect(100, 540, 400, 580); - page.WriteBarcode(rect, "12345678901234567890", BarcodeFormat.CODABAR, forceFitToRect: false, pureBarcode: true, marginBottom: 0); - - // DATA_MATRIX - rect = new Rect(100, 620, 140, 660); - page.WriteBarcode(rect, "01100000110419257000", BarcodeFormat.DM, forceFitToRect: false, pureBarcode: false, marginBottom: 0); - - doc.Save("barcode.pdf"); - - Console.WriteLine($"Barcodes written to 'barcode.pdf' in: {page.Rect}"); - doc.Close(); - - Console.WriteLine("--- Write to image file ----------"); - - // QR_CODE - Utils.WriteBarcode("QR_CODE.png", "Hello World!", BarcodeFormat.QR, width: 600, height: 600, forceFitToRect: true, pureBarcode: false, marginBottom: 0); - - // EAN_8 - Utils.WriteBarcode("EAN_8.png", "1234567", BarcodeFormat.EAN8, width: 300, height: 20, forceFitToRect: false, pureBarcode: false, marginBottom: 4); - - // EAN_13 - Utils.WriteBarcode("EAN_13.png", "123456789012", BarcodeFormat.EAN13, width: 300, height: 0, forceFitToRect: false, pureBarcode: false, marginBottom: 10); - - // UPC_A - Utils.WriteBarcode("UPC_A.png", "123456789012", BarcodeFormat.UPC_A, width: 300, height: 20, forceFitToRect: false, pureBarcode: false, marginBottom: 10); - - // CODE_39 - Utils.WriteBarcode("CODE_39.png", "Hello World!", BarcodeFormat.CODE39, width: 300, height: 70, forceFitToRect: false, pureBarcode: false, marginBottom: 20); - - // CODE_128 - Utils.WriteBarcode("CODE_128.png", "Hello World!", BarcodeFormat.CODE128, width: 300, height: 150, forceFitToRect: false, pureBarcode: false, marginBottom: 20); - - // ITF - Utils.WriteBarcode("ITF.png", "12345678901234567890", BarcodeFormat.I2OF5, width: 300, height: 120, forceFitToRect: false, pureBarcode: false, marginBottom: 20); - - // PDF_417 - Utils.WriteBarcode("PDF_417.png", "Hello World!", BarcodeFormat.PDF417, width: 300, height: 10, forceFitToRect: false, pureBarcode: false, marginBottom: 0); - - // CODABAR - Utils.WriteBarcode("CODABAR.png", "12345678901234567890", BarcodeFormat.CODABAR, width: 300, height: 150, forceFitToRect: false, pureBarcode: false, marginBottom: 20); - - // DATA_MATRIX - Utils.WriteBarcode("DATA_MATRIX.png", "01100000110419257000", BarcodeFormat.DM, width: 300, height: 300, forceFitToRect: false, pureBarcode: true, marginBottom: 1); - - Console.WriteLine("Barcodes written to image files in the current directory."); - } - - static void TestExtractTextWithLayout(string[] args) - { - Console.WriteLine("\n=== TestExtractTextWithLayout ====================="); - string testFilePath = Path.GetFullPath("../../../TestDocuments/columns.pdf"); - Document doc = new Document(testFilePath); - - FileStream wstream = File.Create("columns.txt"); - - for (int i = 0; i < 1/*doc.PageCount*/; i++) - { - Page page = doc[i]; - string textWithLayout = page.GetTextWithLayout(tolerance: 3); - if (!string.IsNullOrEmpty(textWithLayout)) - { - byte[] bytes = Encoding.UTF8.GetBytes(textWithLayout); - wstream.Write(bytes, 0, bytes.Length); - } - } - - wstream.Close(); - - doc.Close(); - - Console.WriteLine("Created columns.txt file"); - } - - static void TestWidget(string[] args) - { - Console.WriteLine("\n=== TestWidget ====================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/Widget.pdf"); - Document doc = new Document(testFilePath); - for (int i = 0; i < 1; i++) - { - var page = doc[i]; - - List entries = page.GetXObjects(); - - Widget fWidget = page.FirstWidget; - while (fWidget != null) - { - Console.WriteLine($"Widget: {fWidget}"); - Console.WriteLine($"FieldName: {fWidget.FieldName}"); - Console.WriteLine($"FieldType: {fWidget.FieldType}"); - Console.WriteLine($"FieldValue: {fWidget.FieldValue}"); - Console.WriteLine($"FieldFlags: {fWidget.FieldFlags}"); - Console.WriteLine($"FieldLabel: {fWidget.FieldLabel}"); - Console.WriteLine($"TextFont: {fWidget.TextFont}"); - Console.WriteLine($"TextFontSize: {fWidget.TextFontSize}"); - Console.WriteLine($"TextColor: {string.Join(",", fWidget.TextColor)}"); - fWidget = (Widget)fWidget.Next; - } - - foreach (var widget in page.GetWidgets()) - { - Console.WriteLine($"Widget: {widget}"); - Console.WriteLine($"FieldName: {widget.FieldName}"); - Console.WriteLine($"FieldType: {widget.FieldType}"); - Console.WriteLine($"FieldValue: {widget.FieldValue}"); - Console.WriteLine($"FieldFlags: {widget.FieldFlags}"); - Console.WriteLine($"FieldLabel: {widget.FieldLabel}"); - Console.WriteLine($"TextFont: {widget.TextFont}"); - Console.WriteLine($"TextFontSize: {widget.TextFontSize}"); - Console.WriteLine($"TextColor: {string.Join(",", widget.TextColor)}"); - - } - } - - doc.Close(); - Console.WriteLine("Widget test completed."); - } - - static void TestColor(string[] args) - { - Console.WriteLine("\n=== TestColor ====================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/Color.pdf"); - Document doc = new Document(testFilePath); - List images = doc.GetPageImages(0); - Console.WriteLine($"CaName: {images[0].CsName}"); - doc.Recolor(0, 4); - images = doc.GetPageImages(0); - Console.WriteLine($"CaName: {images[0].AltCsName}"); - doc.Save("ReColor.pdf"); - doc.Close(); - - Console.WriteLine("Color test completed."); - } - - static void TestCMYKRecolor(string[] args) - { - Console.WriteLine("\n=== TestCMYKRecolor ====================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/CMYK_Recolor.pdf"); - Document doc = new Document(testFilePath); - //List images = doc.GetPageImages(0); - //Console.WriteLine($"CaName: {images[0].CsName}"); - doc.Recolor(0, "CMYK"); - //images = doc.GetPageImages(0); - //Console.WriteLine($"CaName: {images[0].AltCsName}"); - doc.Save("CMYKRecolor.pdf"); - doc.Close(); - - Console.WriteLine("CMYK Recolor test completed."); - } - - static void TestSVGRecolor(string[] args) - { - Console.WriteLine("\n=== TestSVGRecolor ====================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/SvgTest.pdf"); - Document doc = new Document(testFilePath); - doc.Recolor(0, "RGB"); - doc.Save("SVGRecolor.pdf"); - doc.Close(); - - Console.WriteLine("SVG Recolor test completed."); - } - - static void TestReplaceImage(string[] args) - { - Console.WriteLine("\n=== TestReplaceImage ====================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/Color.pdf"); - Document doc = new Document(testFilePath); - Page page = doc[0]; - List images = page.GetImages(true); - List imgs = page.GetImageRects(images[0].Xref); - - List infos = page.GetImageInfo(xrefs: true); - - page.ReplaceImage(images[0].Xref, "../../../TestDocuments/Image/_apple.png"); - page.ReplaceImage(images[0].Xref, "../../../TestDocuments/Image/_bb-logo.png"); - - infos = page.GetImageInfo(xrefs: true); - //page.DeleteImage(images[0].Xref); - - //int newXref = page.InsertImage(imgs[0].Rect, "../../../TestDocuments/Sample.png"); - - //images = page.GetImages(true); - //imgs = page.GetImageRects(images[0].Xref); - - //page.ReplaceImage(infos[0].Xref, "../../../TestDocuments/Sample.png"); - //page.DeleteImage(images[0].Xref); - - //page.InsertImage(imgs[0].Rect, "../../../TestDocuments/Sample.jpg"); - - doc.Save("ReplaceImage.pdf"); - doc.Close(); - - Console.WriteLine("Image replacement test completed."); - } - - static void TestInsertImage(string[] args) - { - Console.WriteLine("\n=== TestInsertImage ====================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/Image/test.pdf"); - Document doc = new Document(testFilePath); - Page page = doc[0]; - - var pixmap1 = new Pixmap("../../../TestDocuments/Image/_apple.png"); - //var pixmap1 = new Pixmap("../../../TestDocuments/Image/30mb.jpg"); - var pixmap2 = new Pixmap("../../../TestDocuments/Image/_bb-logo.png"); - var imageRect1 = new Rect(0, 0, 100, 100); - var imageRect2 = new Rect(100, 100, 200, 200); - var imageRect3 = new Rect(100, 200, 200, 300); - var imageRect4 = new Rect(100, 300, 200, 400); - var imageRect5 = new Rect(100, 400, 200, 500); - var imageRect6 = new Rect(100, 500, 200, 600); - - var img_xref = page.InsertImage(imageRect1, pixmap: pixmap1); - Console.WriteLine(img_xref); - - //img_xref = page.InsertImage(imageRect2, "../../../TestDocuments/Image/_apple.png"); - img_xref = page.InsertImage(imageRect2, pixmap: pixmap1); - Console.WriteLine(img_xref); - img_xref = page.InsertImage(imageRect3, pixmap: pixmap2); - Console.WriteLine(img_xref); - img_xref = page.InsertImage(imageRect4, "../../../TestDocuments/Image/_bb-logo.png"); - Console.WriteLine(img_xref); - page.InsertImage(imageRect5, xref: img_xref); - Console.WriteLine(img_xref); - page.InsertImage(imageRect6, xref: img_xref); - - doc.Save("TestInsertImage.pdf"); - doc.Close(); - - Console.WriteLine("Image insertion test completed."); - } - - static void TestGetImageInfo(string[] args) - { - Console.WriteLine("\n=== TestGetImageInfo ====================="); - - string testFilePath = Path.GetFullPath("../../../TestDocuments/Image/TestInsertImage.pdf"); - Document doc = new Document(testFilePath); - Page page = doc[0]; - - List infos = page.GetImageInfo(xrefs: true); - - doc.Close(); - - Console.WriteLine("Image info test completed."); - } - - static void TestGetTextPageOcr(string[] args) - { - Console.WriteLine("\n=== TestGetTextPageOcr ====================="); - - string testFilePath = Path.GetFullPath(@"../../../TestDocuments/Ocr.pdf"); - Document doc = new Document(testFilePath); - Page page = doc[0]; - - page.RemoveRotation(); - Pixmap pixmap = page.GetPixmap(); - - List blocks = page.GetText("dict", flags: (int)TextFlags.TEXT_PRESERVE_IMAGES)?.Blocks; - foreach (Block block in blocks) - { - Console.WriteLine(block.Image.Length); - } - - // build the pipeline - var pipeline = new ImageFilterPipeline(); - pipeline.Clear(); - //pipeline.AddDeskew(minAngle: 0.5); // replaces any existing deskew step - //pipeline.AddRemoveHorizontalLines(); // also replaces existing horizontal-removal step - //pipeline.AddRemoveVerticalLines(); - //pipeline.AddGrayscale(); - //pipeline.AddMedian(blockSize: 2, replaceExisting: true); - pipeline.AddGamma(gamma: 1.2); // brighten slightly - //pipeline.AddScaleFit(100); - pipeline.AddScale(scaleFactor: 3f, quality: SKFilterQuality.High); - //pipeline.AddContrast(contrast: 100); - //pipeline.AddDilation(); - //pipeline.AddInvert(); - - TextPage tp = page.GetTextPageOcr((int)TextFlags.TEXT_PRESERVE_SPANS, full: true, imageFilters: pipeline); - string txt = tp.ExtractText(); - Console.WriteLine(txt); - - doc.Close(); - - Console.WriteLine("OCR text extraction test completed."); - } - - static void TestCreateImagePage(string[] args) - { - Console.WriteLine("\n=== TestCreateImagePage ====================="); - - Pixmap pxmp = new Pixmap("../../../TestDocuments/Image/_bb-logo.png"); - - Document doc = new Document(); - Page page = doc.NewPage(width:pxmp.W, height:pxmp.H); - - page.InsertImage(page.Rect, pixmap: pxmp); - - pxmp.Dispose(); - - doc.Save("_bb-logo.pdf", pretty: 1); - doc.Close(); - - Console.WriteLine("Image page creation test completed."); - } - - static void TestJoinPdfPages(string[] args) - { - Console.WriteLine("\n=== TestJoinPdfPages ====================="); - - string testFilePath1 = Path.GetFullPath(@"../../../TestDocuments/Widget.pdf"); - Document doc1 = new Document(testFilePath1); - string testFilePath2 = Path.GetFullPath(@"../../../TestDocuments/Color.pdf"); - Document doc2 = new Document(testFilePath2); - - doc1.InsertPdf(doc2, 0, 0, 2); - - doc1.Save("Joined.pdf", pretty: 1); - - doc2.Close(); - doc1.Close(); - - Console.WriteLine("PDF pages joined successfully into 'Joined.pdf'."); - } - - static void TestFreeTextAnnot(string[] args) + private static void Main(string[] args) { - Console.WriteLine("\n=== TestFreeTextAnnot ====================="); - - Rect r = new Rect(72, 72, 220, 100); - string t1 = "têxt üsès Lätiñ charß,\nEUR: €, mu: µ, super scripts: ²³!"; - Rect rect = new Rect(100,100,200,200); - float[] red = new float[] { 1, 0, 0 }; - float[] blue = new float[] { 0, 0, 1 }; - float[] gold = new float[] { 1, 1, 0 }; - float[] green = new float[] { 0, 1, 0 }; - float[] white = new float[] { 1, 1, 1 }; - - Document doc = new Document(); - Page page = doc.NewPage(); - - Annot annot = page.AddFreeTextAnnot( - rect, - t1, - fontSize: 10, - rotate: 90, - textColor: red, - fillColor: gold, - align: (int)TextAlign.TEXT_ALIGN_CENTER, - dashes: new int[] { 2 } - ); - - annot.SetBorder(border: null, width: 0.3f, dashes: new int[] { 2 }); - annot.Update(textColor: blue); - //annot.Update(textColor: red, fillColor: blue); - - doc.Save("FreeTextAnnot.pdf"); - - doc.Close(); - - Console.WriteLine("Free text annotation created and saved to 'FreeTextAnnot.pdf'."); + SampleMenu.Run(args); } } } diff --git a/Demo/SampleMenu.cs b/Demo/SampleMenu.cs new file mode 100644 index 0000000..d6def26 --- /dev/null +++ b/Demo/SampleMenu.cs @@ -0,0 +1,171 @@ +namespace Demo +{ + /// + /// Demo samples grouped by MuPDF.NET / MuPDF.NET4LLM feature areas. With no arguments, runs every sample. + /// Use dotnet run -- help for the list, or dotnet run -- <name> for one sample. + /// + public static class SampleMenu + { + /// Library-facing group (matches folders under Samples/ and major API surfaces). + private sealed record Sample(string Category, string Name, string Description, Action Run); + + /// Order matches Samples/ layout; MuPDF.NET4LLM extras live in Samples/Llm/Program.Llm.*.Fixtures.cs. + private static readonly Sample[] Samples = + { + // —— Document & I/O (MuPDF.NET Document, open/save, streams) —— Samples/Document + new("Document & I/O", "hello-new-pdf", "Hello World on a new PDF", a => Program.TestHelloWorldToNewDocument(a)), + new("Document & I/O", "hello-existing-pdf", "Hello World on existing Blank.pdf", a => Program.TestHelloWorldToExistingDocument(a)), + new("Document & I/O", "join-pdf", "Insert pages from another PDF", a => Program.TestJoinPdfPages(a)), + new("Document & I/O", "metadata", "Print document metadata", _ => Program.TestMetadata()), + new("Document & I/O", "move-file", "Save through MemoryStream and move output", _ => Program.TestMoveFile()), + new("Document & I/O", "unicode-doc", "Save PDF with unicode filename", _ => Program.TestUnicodeDocument()), + new("Document & I/O", "memory-leak", "Open/close documents in a loop", _ => Program.TestMemoryLeak()), + + // —— Text, story & vector drawing (Page, Story, TextWriter, Shape) —— Samples/TextDrawing + new("Text, story & drawing", "insert-htmlbox", "Insert HTML story box into a new page", _ => Program.TestInsertHtmlbox()), + new("Text, story & drawing", "text-font", "FillTextbox with fonts", a => Program.TestTextFont(a)), + new("Text, story & drawing", "morph", "TextWriter with morph / rotation", _ => Program.TestMorph()), + new("Text, story & drawing", "gettext", "GetText dict dump per page", _ => Program.TestGetText()), + new("Text, story & drawing", "extract-text-layout", "Extract text with reading order (columns.pdf)", a => Program.TestExtractTextWithLayout(a)), + new("Text, story & drawing", "draw-line", "Draw dashed lines on a page", _ => Program.TestDrawLine()), + new("Text, story & drawing", "draw-shape", "Copy vector paths between PDFs", _ => Program.TestDrawShape()), + + // —— Annotations —— Samples/Annotations + new("Annotations", "line-annot", "Create and modify line annotations", _ => Program.TestLineAnnot()), + new("Annotations", "annot-freetext1", "Free-text annotation sample (1)", a => Program.TestAnnotationsFreeText1(a)), + new("Annotations", "annot-freetext2", "Free-text annotation sample (2)", a => Program.TestAnnotationsFreeText2(a)), + new("Annotations", "new-annots", "Caret, markers, shapes, stamp, redaction, etc.", a => NewAnnots.Run(a)), + new("Annotations", "annot-doc", "Rectangle annotation + text", _ => Program.CreateAnnotDocument()), + new("Annotations", "freetext-annot", "Add free-text annotation (unicode)", a => Program.TestFreeTextAnnot(a)), + + // —— Pages, widgets, images & color —— Samples/PageContent + new("Pages, widgets, images & color", "widget", "Inspect form widgets", a => Program.TestWidget(a)), + new("Pages, widgets, images & color", "color", "Recolor page images", a => Program.TestColor(a)), + new("Pages, widgets, images & color", "cmyk-recolor", "CMYK recolor", a => Program.TestCMYKRecolor(a)), + new("Pages, widgets, images & color", "svg-recolor", "SVG / RGB recolor", a => Program.TestSVGRecolor(a)), + new("Pages, widgets, images & color", "replace-image", "Replace embedded images", a => Program.TestReplaceImage(a)), + new("Pages, widgets, images & color", "insert-image", "Insert images from pixmaps and files", a => Program.TestInsertImage(a)), + new("Pages, widgets, images & color", "get-image-info", "Dump image xref info", a => Program.TestGetImageInfo(a)), + new("Pages, widgets, images & color", "page-ocr", "OCR text page with image filter pipeline", a => Program.TestGetTextPageOcr(a)), + new("Pages, widgets, images & color", "create-image-page", "New PDF page from PNG pixmap", a => Program.TestCreateImagePage(a)), + + // —— Image filters (Skia) —— Samples/ImageFilters + new("Image filters (Skia)", "image-filter", "Skia pipeline on table.jpg → output.png", _ => Program.TestImageFilter()), + new("Image filters (Skia)", "image-filter-ocr", "Pixmap OCR with filter pipeline", _ => Program.TestImageFilterOcr()), + + // —— Barcodes —— Samples/Barcodes + new("Barcodes", "read-barcode", "Read barcodes from image and PDF", a => Program.TestReadBarcode(a)), + new("Barcodes", "read-datamatrix", "Read Data Matrix from PDF", _ => Program.TestReadDataMatrix()), + new("Barcodes", "read-qrcode", "Render PDF page and read QR from PNG", a => Program.TestReadQrCode(a)), + 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()), + + // —— MuPDF.NET4LLM —— Samples/Llm + new("MuPDF.NET4LLM", "llm", "MuPDF4LLM.ToMarkdown quick test", _ => Program.TestLLM()), + new("MuPDF.NET4LLM", "rag-markdown", "MuPdfRag.ToMarkdown (Magazine.pdf)", _ => Program.TestPyMuPdfRagToMarkdown()), + new("MuPDF.NET4LLM", "table", "Detect tables and export markdown", _ => Program.TestTable()), + new("MuPDF.NET4LLM", "markdown-reader", "LlamaIndex PDFMarkdownReader", _ => Program.TestMarkdownReader()), + new("MuPDF.NET4LLM", "llm-to-markdown-fixture-370", "ToMarkdown vs tests/test_370_expected.md (needs tests/test_370.pdf)", a => Program.Test4LlmToMarkdownCompareExpected370(a)), + new("MuPDF.NET4LLM", "llm-to-markdown-ocr-1", "ToMarkdown + U+FFFD fixture (tests/test_ocr_loremipsum_FFFD.pdf)", a => Program.Test4LlmToMarkdownOcrFixture1(a)), + new("MuPDF.NET4LLM", "llm-to-markdown-ocr-2", "ToMarkdown useOcr=false on FFFD fixture", a => Program.Test4LlmToMarkdownOcrFixture2(a)), + new("MuPDF.NET4LLM", "llm-to-markdown-ocr-3", "ToMarkdown OCR on/off on SVG fixture", a => Program.Test4LlmToMarkdownOcrFixture3(a)), + new("MuPDF.NET4LLM", "llm-pdf-reader-empty", "PDFMarkdownReader: new PDF, one blank page", a => Program.Test4LlmPdfMarkdownReaderEmptyPage(a)), + new("MuPDF.NET4LLM", "llm-pdf-reader-missing-file", "PDFMarkdownReader: missing path → FileNotFoundException", a => Program.Test4LlmPdfMarkdownReaderMissingFile(a)), + + // —— Regression & diagnostics —— Samples/Regression + new("Regression & diagnostics", "issue-213", "Repro: drawing paths / line width", _ => Program.TestIssue213()), + new("Regression & diagnostics", "issue-1880", "Repro: read Data Matrix barcodes", _ => Program.TestIssue1880()), + new("Regression & diagnostics", "issue-234", "Repro: pixmap scale + insert image", _ => Program.TestIssue234()), + new("Regression & diagnostics", "jbig2", "Rewrite images with FAX recompression", _ => Program.TestRecompressJBIG2()), + }; + + private static readonly Dictionary ByName = BuildIndex(); + + private static Dictionary BuildIndex() + { + var d = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var s in Samples) + { + d[s.Name] = s; + } + return d; + } + + public static void Run(string[] args) + { + if (args.Length > 0 && IsHelp(args[0])) + { + PrintUsage(); + return; + } + + if (args.Length == 0 || IsRunAllSwitch(args[0])) + { + RunAll(); + return; + } + + if (!ByName.TryGetValue(args[0], out var sample)) + { + Console.Error.WriteLine($"Unknown sample: {args[0]}"); + PrintUsage(); + Environment.ExitCode = 1; + return; + } + + Console.WriteLine($"--- Sample: {sample.Name} ({sample.Category}) ---"); + sample.Run(args); + } + + private static bool IsHelp(string a) => + a is "-h" or "-?" or "/?" or "help" or "--help"; + + private static bool IsRunAllSwitch(string a) => + string.Equals(a, "all", StringComparison.OrdinalIgnoreCase) + || string.Equals(a, "-all", StringComparison.OrdinalIgnoreCase) + || string.Equals(a, "--all", StringComparison.OrdinalIgnoreCase); + + private static void RunAll() + { + var sampleArgs = Array.Empty(); + foreach (var s in Samples) + { + Console.WriteLine(); + Console.WriteLine($"========== {s.Category} / {s.Name} =========="); + try + { + s.Run(sampleArgs); + } + catch (Exception ex) + { + Console.Error.WriteLine($"FAILED {s.Name}: {ex.Message}"); + } + } + } + + private static void PrintUsage() + { + Console.WriteLine("MuPDF.NET Demo — samples mirror library areas under Demo/Samples/. Default: run all."); + Console.WriteLine(); + Console.WriteLine(" dotnet run (or: dotnet run -- -all)"); + Console.WriteLine(" dotnet run -- "); + Console.WriteLine(" dotnet run -- help"); + Console.WriteLine(); + Console.WriteLine("Samples by category:"); + var lastCat = ""; + foreach (var s in Samples) + { + if (s.Category != lastCat) + { + Console.WriteLine(); + Console.WriteLine($" [{s.Category}]"); + lastCat = s.Category; + } + + Console.WriteLine($" {s.Name,-22} {s.Description}"); + } + + Console.WriteLine(); + } + } +} diff --git a/Demo/new-annots.cs b/Demo/Samples/Annotations/NewAnnots.cs similarity index 100% rename from Demo/new-annots.cs rename to Demo/Samples/Annotations/NewAnnots.cs diff --git a/Demo/Samples/Annotations/Program.Annotations.FreeText.cs b/Demo/Samples/Annotations/Program.Annotations.FreeText.cs new file mode 100644 index 0000000..4f398e5 --- /dev/null +++ b/Demo/Samples/Annotations/Program.Annotations.FreeText.cs @@ -0,0 +1,75 @@ +namespace Demo +{ + internal partial class Program + { + /// Three stacked FreeText annotations (plain text, fonts, rotation). + internal static void TestAnnotationsFreeText1(string[] args) + { + _ = args; + Console.WriteLine("\n=== TestAnnotationsFreeText1 ======================="); + + Document doc = new Document(); + Page page = doc.NewPage(); + + Rect r1 = new Rect(100, 100, 200, 150); + Rect r2 = r1 + new Rect(0, 75, 0, 75); + Rect r3 = r2 + new Rect(0, 75, 0, 75); + + string t = "¡Un pequeño texto para practicar!"; + + Annot a1 = page.AddFreeTextAnnot(r1, t, textColor: Constants.red); + Annot a2 = page.AddFreeTextAnnot(r2, t, fontName: "Ti", textColor: Constants.blue); + Annot a3 = page.AddFreeTextAnnot(r3, t, fontName: "Co", textColor: Constants.blue, rotate: 90); + a3.SetBorder(width: 0); + a3.Update(fontSize: 8, fillColor: Constants.gold); + + doc.Save("a-freetext.pdf"); + doc.Close(); + + Console.WriteLine("Saved to a-freetext.pdf"); + } + + /// FreeText with rich text, styling, and callout line. + internal static void TestAnnotationsFreeText2(string[] args) + { + _ = args; + Console.WriteLine("\n=== TestAnnotationsFreeText2 ======================="); + + string ds = "font-size: 11pt; font-family: sans-serif;"; + string bullet = "\u2610\u2611\u2612"; + + string text = $@"

+MuPDF.NET འདི་ ཡིག་ཆ་བཀྲམ་སྤེལ་གྱི་དོན་ལུ་ པའི་ཐོན་ཐུམ་སྒྲིལ་དྲག་ཤོས་དང་མགྱོགས་ཤོས་ཅིག་ཨིན། +Here is some bold and italic text, followed by bold-italic. Text-based check boxes: {bullet}. +

"; + + Document doc = new Document(); + Page page = doc.NewPage(); + + Rect rect = new Rect(100, 100, 350, 200); + Point p2 = rect.TopRight + new Point(50, 30); + Point p3 = p2 + new Point(0, 30); + + Annot annot = page.AddFreeTextAnnot( + rect, + text, + fillColor: Constants.gold, + opacity: 1, + rotate: 0, + borderWidth: 1, + dashes: null, + richtext: true, + style: ds, + callout: new Point[] { p3, p2, rect.TopRight }, + lineEnd: PdfLineEnding.PDF_ANNOT_LE_OPEN_ARROW, + borderColor: Constants.green + ); + + const string outName = "AnnotationsFreeText2.pdf"; + doc.Save(outName, pretty: 1); + doc.Close(); + + Console.WriteLine("Saved to " + outName); + } + } +} diff --git a/Demo/Samples/Barcodes/Program.Barcodes.cs b/Demo/Samples/Barcodes/Program.Barcodes.cs new file mode 100644 index 0000000..792c161 --- /dev/null +++ b/Demo/Samples/Barcodes/Program.Barcodes.cs @@ -0,0 +1,341 @@ +namespace Demo +{ + internal partial class Program + { + internal static void TestWriteBarcode1() + { + Console.WriteLine("\n=== TestWriteBarcode1 ====================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/Blank.pdf"); + Document doc = new Document(testFilePath); + + Page page = doc[0]; + + // CODE39 + Rect rect = new Rect( + X0: Units.MmToPoints(50), + X1: Units.MmToPoints(80), + Y0: Units.MmToPoints(70), + Y1: Units.MmToPoints(85)); + + page.WriteBarcode(rect, "JJBEA6500", BarcodeFormat.CODE39, forceFitToRect: true, pureBarcode: true, narrowBarWidth:1); + + rect = new Rect( + X0: Units.MmToPoints(50), + X1: Units.MmToPoints(160), + Y0: Units.MmToPoints(100), + Y1: Units.MmToPoints(105)); + + page.WriteBarcode(rect, "JJBEA6500", BarcodeFormat.CODE39, forceFitToRect: true, pureBarcode: true, narrowBarWidth: 2); + + // CODE128 + Rect rect1 = new Rect( + X0: Units.MmToPoints(50), + X1: Units.MmToPoints(100), + Y0: Units.MmToPoints(50), + Y1: Units.MmToPoints(60)); + + page.WriteBarcode(rect1, "JJBEA6500063000000177922", BarcodeFormat.CODE128, forceFitToRect: false, pureBarcode: true, narrowBarWidth: 1); + + rect1 = new Rect( + X0: Units.MmToPoints(50), + X1: Units.MmToPoints(200), + Y0: Units.MmToPoints(80), + Y1: Units.MmToPoints(120)); + + page.WriteBarcode(rect1, "JJBEA6500063000000177922", BarcodeFormat.CODE128, forceFitToRect: true, pureBarcode: true, narrowBarWidth: 1); + + Rect rect2 = new Rect( + X0: Units.MmToPoints(100), + X1: Units.MmToPoints(140), + Y0: Units.MmToPoints(40), + Y1: Units.MmToPoints(80)); + + page.WriteBarcode(rect2, "01030000110444408000", BarcodeFormat.DM, forceFitToRect: false, pureBarcode: true, narrowBarWidth: 3); + + Pixmap pxmp = Utils.GetBarcodePixmap("JJBEA6500063000000177922", BarcodeFormat.CODE128, width: 500, pureBarcode: true, marginLeft:0, marginTop:0, marginRight:0, marginBottom:0, narrowBarWidth: 1); + + pxmp.Save(@"PxmpBarcode3.png"); + + byte[] imageBytes = pxmp.ToBytes(); + + using var stream = new SKMemoryStream(imageBytes); + using var codec = SKCodec.Create(stream); + var info = codec.Info; + var bitmap = SKBitmap.Decode(codec); + + using var data = bitmap.Encode(SKEncodedImageFormat.Png, 100); // 100 = quality + using var stream1 = File.OpenWrite(@"output.png"); + data.SaveTo(stream1); + + doc.Save(@"TestWriteBarcode1.pdf"); + + page.Dispose(); + doc.Close(); + + Console.WriteLine("TestWriteBarcode1 completed."); + } + + internal static void TestReadDataMatrix() + { + int i = 0; + + Console.WriteLine("\n=== TestReadDataMatrix ======================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/Barcodes/datamatrix.pdf"); + Document doc = new Document(testFilePath); + + Page page = doc[0]; + + List barcodes = page.ReadBarcodes(decodeEmbeddedOnly: false); + + foreach (Barcode barcode in barcodes) + { + BarcodePoint[] points = barcode.ResultPoints; + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); + } + /* + List blocks = page.GetImageInfo(); + + foreach (Block block in blocks) + { + Rect blockRect = block.Bbox; + barcodes = page.ReadBarcodes(clip:blockRect); + foreach (Barcode barcode in barcodes) + { + BarcodePoint[] points = barcode.ResultPoints; + if (points.Length == 2) + { + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); + } + else if (points.Length == 4) + { + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[2]}]"); + } + } + } + */ + /* + List imlist = page.GetImages(); + foreach (Entry im in imlist) + { + ImageInfo img = doc.ExtractImage(im.Xref); + File.WriteAllBytes(@"copy.png", img.Image); + + List barcodes = Utils.ReadBarcodes(@"copy.png", new Rect(0,0,img.Width,img.Height)); + + foreach (Barcode barcode in barcodes) + { + BarcodePoint[] points = barcode.ResultPoints; + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); + } + } + */ + + page.Dispose(); + doc.Close(); + } + + + internal static void TestReadBarcode(string[] args) + { + int i = 0; + + Console.WriteLine("\n=== TestReadBarcode ======================="); + + Console.WriteLine("--- Read from image file ----------"); + string testFilePath1 = Path.GetFullPath("../../../TestDocuments/Barcodes/rendered.bmp"); + + Rect rect1 = new Rect(1260, 390, 1720, 580); + List barcodes2 = Utils.ReadBarcodes(testFilePath1, clip:rect1); + + i = 0; + foreach (Barcode barcode in barcodes2) + { + BarcodePoint[] points = barcode.ResultPoints; + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); + } + + Console.WriteLine("--- Read from pdf file ----------"); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/Barcodes/Samples.pdf"); + Document doc = new Document(testFilePath); + + Page page = doc[0]; + //Rect rect = new Rect(290, 590, 420, 660); + List barcodes = page.ReadBarcodes(); + + foreach (Barcode barcode in barcodes) + { + BarcodePoint[] points = barcode.ResultPoints; + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); + } + doc.Close(); + } + + internal static void TestReadQrCode(string[] args) + { + Console.WriteLine("\n=== TestReadQrCode ======================="); + int i = 0; + /* + Console.WriteLine("=== Read from image file ====================="); + string testFilePath1 = Path.GetFullPath("../../../TestDocuments/Barcodes/2.png"); + + List barcodes2 = Utils.ReadBarcodes(testFilePath1, autoRotate:true); + + i = 0; + foreach (Barcode barcode in barcodes2) + { + BarcodePoint[] points = barcode.ResultPoints; + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); + } + */ + ///* + Console.WriteLine("--- Read from pdf file ----------"); + + string testImagePath = @"test.png"; + string testFilePath = Path.GetFullPath("../../../TestDocuments/Barcodes/input.pdf"); + Document doc = new Document(testFilePath); + + Page page = doc[0]; + page.RemoveRotation(); // remove rotation to read barcodes correctly + + // Apply 2x scale (both X and Y) + var matrix = new Matrix(3.0f, 3.0f); + + // Render the page using the scaled matrix + var pixmap = page.GetPixmap(matrix); + + pixmap.GammaWith(3.2f); // apply gamma correction to improve barcode detection + + pixmap.Save(testImagePath); + + /* + Rect rect = new Rect(400, 700, page.Rect.X1, page.Rect.Y1); + List barcodes = page.ReadBarcodes(rect); + + foreach (Barcode barcode in barcodes) + { + BarcodePoint[] points = barcode.ResultPoints; + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); + } + */ + + pixmap.Dispose(); + doc.Close(); + + List barcodes2 = Utils.ReadBarcodes(testImagePath); + + i = 0; + foreach (Barcode barcode in barcodes2) + { + BarcodePoint[] points = barcode.ResultPoints; + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); + } + //*/ + } + + internal static void TestWriteBarcode(string[] args) + { + Console.WriteLine("\n=== TestWriteBarcode ======================="); + Console.WriteLine("--- Write to pdf file ----------"); + string testFilePath = Path.GetFullPath("../../../TestDocuments/Blank.pdf"); + Document doc = new Document(testFilePath); + Page page = doc[0]; + + MuPDF.NET.TextWriter writer = new MuPDF.NET.TextWriter(page.Rect); + Font font = new Font("cour", isBold: 1); + writer.FillTextbox(page.Rect, "QR_CODE", font, pos: new Point(0, 10)); + writer.FillTextbox(page.Rect, "EAN_8", font, pos: new Point(0, 110)); + writer.FillTextbox(page.Rect, "EAN_13", font, pos: new Point(0, 165)); + writer.FillTextbox(page.Rect, "UPC_A", font, pos: new Point(0, 220)); + writer.FillTextbox(page.Rect, "CODE_39", font, pos: new Point(0, 275)); + writer.FillTextbox(page.Rect, "CODE_128", font, pos: new Point(0, 330)); + writer.FillTextbox(page.Rect, "ITF", font, pos: new Point(0, 385)); + writer.FillTextbox(page.Rect, "PDF_417", font, pos: new Point(0, 440)); + writer.FillTextbox(page.Rect, "CODABAR", font, pos: new Point(0, 520)); + writer.FillTextbox(page.Rect, "DATA_MATRIX", font, pos: new Point(0, 620)); + writer.WriteText(page); + + // QR_CODE + Rect rect = new Rect(100, 20, 300, 80); + page.WriteBarcode(rect, "Hello World!", BarcodeFormat.QR, forceFitToRect:false, pureBarcode:false, marginLeft:0); + + // EAN_8 + rect = new Rect(100, 100, 300, 120); + page.WriteBarcode(rect, "1234567", BarcodeFormat.EAN8, forceFitToRect: false, pureBarcode: false, marginBottom: 20); + + // EAN_13 + rect = new Rect(100, 155, 300, 200); + page.WriteBarcode(rect, "123456789012", BarcodeFormat.EAN13, forceFitToRect: false, pureBarcode: true, marginBottom: 0); + + // UPC_A + rect = new Rect(100, 210, 300, 255); + page.WriteBarcode(rect, "123456789012", BarcodeFormat.UPC_A, forceFitToRect: false, pureBarcode: true, marginBottom: 0); + + // CODE_39 + rect = new Rect(100, 265, 600, 285); + page.WriteBarcode(rect, "Hello World!", BarcodeFormat.CODE39, forceFitToRect: false, pureBarcode: false, marginBottom: 0); + + // CODE_128 + rect = new Rect(100, 320, 400, 355); + page.WriteBarcode(rect, "Hello World!", BarcodeFormat.CODE128, forceFitToRect: true, pureBarcode: true, marginBottom: 0); + + // ITF + rect = new Rect(100, 385, 300, 420); + page.WriteBarcode(rect, "12345678901234567890", BarcodeFormat.I2OF5, forceFitToRect: false, pureBarcode: false, marginBottom: 0); + + // PDF_417 + rect = new Rect(100, 430, 400, 435); + page.WriteBarcode(rect, "Hello World!", BarcodeFormat.PDF417, forceFitToRect: false, pureBarcode: true, marginBottom: 0); + + // CODABAR + rect = new Rect(100, 540, 400, 580); + page.WriteBarcode(rect, "12345678901234567890", BarcodeFormat.CODABAR, forceFitToRect: false, pureBarcode: true, marginBottom: 0); + + // DATA_MATRIX + rect = new Rect(100, 620, 140, 660); + page.WriteBarcode(rect, "01100000110419257000", BarcodeFormat.DM, forceFitToRect: false, pureBarcode: false, marginBottom: 0); + + doc.Save("barcode.pdf"); + + Console.WriteLine($"Barcodes written to 'barcode.pdf' in: {page.Rect}"); + doc.Close(); + + Console.WriteLine("--- Write to image file ----------"); + + // QR_CODE + Utils.WriteBarcode("QR_CODE.png", "Hello World!", BarcodeFormat.QR, width: 600, height: 600, forceFitToRect: true, pureBarcode: false, marginBottom: 0); + + // EAN_8 + Utils.WriteBarcode("EAN_8.png", "1234567", BarcodeFormat.EAN8, width: 300, height: 20, forceFitToRect: false, pureBarcode: false, marginBottom: 4); + + // EAN_13 + Utils.WriteBarcode("EAN_13.png", "123456789012", BarcodeFormat.EAN13, width: 300, height: 0, forceFitToRect: false, pureBarcode: false, marginBottom: 10); + + // UPC_A + Utils.WriteBarcode("UPC_A.png", "123456789012", BarcodeFormat.UPC_A, width: 300, height: 20, forceFitToRect: false, pureBarcode: false, marginBottom: 10); + + // CODE_39 + Utils.WriteBarcode("CODE_39.png", "Hello World!", BarcodeFormat.CODE39, width: 300, height: 70, forceFitToRect: false, pureBarcode: false, marginBottom: 20); + + // CODE_128 + Utils.WriteBarcode("CODE_128.png", "Hello World!", BarcodeFormat.CODE128, width: 300, height: 150, forceFitToRect: false, pureBarcode: false, marginBottom: 20); + + // ITF + Utils.WriteBarcode("ITF.png", "12345678901234567890", BarcodeFormat.I2OF5, width: 300, height: 120, forceFitToRect: false, pureBarcode: false, marginBottom: 20); + + // PDF_417 + Utils.WriteBarcode("PDF_417.png", "Hello World!", BarcodeFormat.PDF417, width: 300, height: 10, forceFitToRect: false, pureBarcode: false, marginBottom: 0); + + // CODABAR + Utils.WriteBarcode("CODABAR.png", "12345678901234567890", BarcodeFormat.CODABAR, width: 300, height: 150, forceFitToRect: false, pureBarcode: false, marginBottom: 20); + + // DATA_MATRIX + Utils.WriteBarcode("DATA_MATRIX.png", "01100000110419257000", BarcodeFormat.DM, width: 300, height: 300, forceFitToRect: false, pureBarcode: true, marginBottom: 1); + + Console.WriteLine("Barcodes written to image files in the current directory."); + } + + } +} diff --git a/Demo/Samples/Document/Program.Document.cs b/Demo/Samples/Document/Program.Document.cs new file mode 100644 index 0000000..0e23500 --- /dev/null +++ b/Demo/Samples/Document/Program.Document.cs @@ -0,0 +1,125 @@ +namespace Demo +{ + internal partial class Program + { + internal static void TestMoveFile() + { + string origfilename = @"../../../TestDocuments/Blank.pdf"; + + string filePath = @"testmove.pdf"; + + File.Copy(origfilename, filePath, true); + + Document d = new Document(filePath); + + Page page = d[0]; + + Point tl = new Point(100, 120); + Point br = new Point(300, 150); + + Rect rect = new Rect(tl, br); + + TextWriter pw = new TextWriter(page.TrimBox); + /* + Font font = new Font(fontName: "tiro"); + + List<(string, float)> ret = pw.FillTextbox(rect, "This is a test to overwrite the original file and move it", font, fontSize: 24); + */ + pw.WriteText(page); + + page.Dispose(); + + MemoryStream tmp = new MemoryStream(); + + d.Save(tmp, garbage: 3, deflateFonts: 1, deflate: 1); + + d.Close(); + + File.WriteAllBytes(filePath, tmp.ToArray()); + + tmp.Dispose(); + + File.Move(filePath, @"moved.pdf", true); + } + + internal static void TestMetadata() + { + Console.WriteLine("\n=== TestMetadata ====================="); + + string testFilePath = @"../../../TestDocuments/Annot.pdf"; + + Document doc = new Document(testFilePath); + + Dictionary metaDict = doc.MetaData; + + foreach (string key in metaDict.Keys) + { + Console.WriteLine(key + ": " + metaDict[key]); + } + + doc.Close(); + + Console.WriteLine("TestMetadata completed."); + } + + internal static void TestMorph() + { + Console.WriteLine("\n=== TestMorph ====================="); + + string testFilePath = @"../../../TestDocuments/Morph.pdf"; + + Document doc = new Document(testFilePath); + Page page = doc[0]; + Rect printrect = new Rect(180, 30, 650, 60); + int pagerot = page.Rotation; + TextWriter pw = new TextWriter(page.TrimBox); + string txt = "Origin 100.100"; + pw.Append(new Point(100, 100), txt, new Font("tiro"), fontSize: 24); + pw.WriteText(page); + + txt = "rotated 270 - 100.100"; + Matrix matrix = new IdentityMatrix(); + matrix.Prerotate(270); + Morph mo = new Morph(new Point(100, 100), matrix); + pw = new TextWriter(page.TrimBox); + pw.Append(new Point(100, 100), txt, new Font("tiro"), fontSize: 24); + pw.WriteText(page, morph:mo); + page.SetRotation(270); + + page.Dispose(); + doc.Save(@"morph.pdf"); + doc.Close(); + } + + internal static void TestUnicodeDocument() + { + Console.WriteLine("\n=== TestUnicodeDocument ====================="); + + string testFilePath = @"../../../TestDocuments/Σ╜áσÑ╜.pdf"; + + Document doc = new Document(testFilePath); + + doc.Save(@"Σ╜áσÑ╜_.pdf"); + doc.Close(); + + Console.WriteLine("TestUnicodeDocument completed."); + } + + internal static void TestMemoryLeak() + { + Console.WriteLine("\n=== TestMemoryLeak ======================="); + string testFilePath = Path.GetFullPath("../../../TestDocuments/Blank.pdf"); + + for (int i = 0; i < 100; i++) + { + Document doc = new Document(testFilePath); + Page page = doc.NewPage(); + page.Dispose(); + doc.Close(); + } + + Console.WriteLine("Memory leak test completed. No leaks should be detected."); + } + + } +} diff --git a/Demo/Samples/ImageFilters/Program.ImageFilters.cs b/Demo/Samples/ImageFilters/Program.ImageFilters.cs new file mode 100644 index 0000000..f51e3d7 --- /dev/null +++ b/Demo/Samples/ImageFilters/Program.ImageFilters.cs @@ -0,0 +1,86 @@ +namespace Demo +{ + internal partial class Program + { + internal static void TestImageFilter() + { + const string inputPath = @"../../../TestDocuments/Image/table.jpg"; + const string outputPath = @"output.png"; + + // Load the image file into SKBitmap + using (var bitmap = SKBitmap.Decode(inputPath)) + { + if (bitmap == null) + { + Console.WriteLine("Failed to load image."); + return; + } + + SKBitmap inputBitmap = bitmap.Copy(); + + // build the pipeline + var pipeline = new ImageFilterPipeline(); + + // clear any defaults if youΓÇÖre reusing the instance + pipeline.Clear(); + + // add filters one-by-one + pipeline.AddDeskew(minAngle: 0.5); // replaces any existing deskew step + pipeline.AddRemoveHorizontalLines(); // also replaces existing horizontal-removal step + pipeline.AddRemoveVerticalLines(); + pipeline.AddGrayscale(); + //pipeline.AddMedian(blockSize: 2, replaceExisting: true); + //pipeline.AddGamma(gamma: 1.2); // brighten slightly + //pipeline.AddContrast(contrast: 100); + //pipeline.AddFit(100); + //pipeline.AddDilation(); + //pipeline.AddScale(scaleFactor: 1.75, quality: SKFilterQuality.Medium); + pipeline.AddInvert(); + + // apply the pipeline (bitmap is modified in place) + pipeline.Apply(ref inputBitmap); + + using (var data = inputBitmap.Encode(SKEncodedImageFormat.Png, 100)) // 100 = quality + { + using (var stream = File.OpenWrite(outputPath)) + { + data.SaveTo(stream); + } + } + + Console.WriteLine($"Loaded image: {bitmap.Width}x{bitmap.Height} pixels"); + } + } + + internal static void TestImageFilterOcr() + { + const string inputPath = @"../../../TestDocuments/Image/boxedpage.jpg"; + + using (Pixmap pxmp = new Pixmap(inputPath)) + { + // build the pipeline + var pipeline = new ImageFilterPipeline(); + + // clear any defaults if youΓÇÖre reusing the instance + pipeline.Clear(); + + // add filters one-by-one + //pipeline.AddDeskew(minAngle: 0.5); // replaces any existing deskew step + //pipeline.AddRemoveHorizontalLines(); // also replaces existing horizontal-removal step + //pipeline.AddRemoveVerticalLines(); + //pipeline.AddGrayscale(); + //pipeline.AddMedian(blockSize: 2, replaceExisting: true); + pipeline.AddGamma(gamma: 1.2); // brighten slightly + //pipeline.AddContrast(contrast: 100); + //pipeline.AddScaleFit(100); + //pipeline.AddDilation(); + pipeline.AddScale(scaleFactor: 1.75, quality: SKFilterQuality.High); + //pipeline.AddInvert(); + + string txt = pxmp.GetTextFromOcr(pipeline); + Console.WriteLine(txt); + } + } + + } +} diff --git a/Demo/Samples/Llm/Program.Llm.PdfMarkdownReader.Fixtures.cs b/Demo/Samples/Llm/Program.Llm.PdfMarkdownReader.Fixtures.cs new file mode 100644 index 0000000..2100de1 --- /dev/null +++ b/Demo/Samples/Llm/Program.Llm.PdfMarkdownReader.Fixtures.cs @@ -0,0 +1,49 @@ +namespace Demo +{ + /// + /// demos aligned with MuPDF.NET4LLM / repository reader tests. + /// + internal partial class Program + { + /// Empty in-memory PDF, save, then (one page → one Llama document). + internal static void Test4LlmPdfMarkdownReaderEmptyPage(string[] args) + { + _ = args; + Console.WriteLine("\n=== Test4LlmPdfMarkdownReaderEmptyPage ======================="); + + string path = Path.Combine(AppContext.BaseDirectory, "llm_reader_empty_page.pdf"); + Document document = new Document(); + try + { + document.NewPage(); + document.Save(path); + } + finally + { + document.Close(); + } + + var reader = new PDFMarkdownReader(); + var documents = reader.LoadData(path); + Console.WriteLine($"Loaded {documents.Count} document(s)."); + } + + /// with a non-existent path → . + internal static void Test4LlmPdfMarkdownReaderMissingFile(string[] args) + { + _ = args; + Console.WriteLine("\n=== Test4LlmPdfMarkdownReaderMissingFile ======================="); + + var reader = new PDFMarkdownReader(); + try + { + reader.LoadData(Path.Combine(LlmRepositoryTestsDirectory(), "fake", "path", "nope.pdf")); + Console.WriteLine("Unexpected: LoadData should throw for missing file."); + } + catch (FileNotFoundException ex) + { + Console.WriteLine($"OK: FileNotFoundException — {ex.Message}"); + } + } + } +} diff --git a/Demo/Samples/Llm/Program.Llm.ToMarkdown.Fixtures.cs b/Demo/Samples/Llm/Program.Llm.ToMarkdown.Fixtures.cs new file mode 100644 index 0000000..c1a4dc2 --- /dev/null +++ b/Demo/Samples/Llm/Program.Llm.ToMarkdown.Fixtures.cs @@ -0,0 +1,211 @@ +namespace Demo +{ + /// + /// MuPDF.NET4LLM demos aligned with repository tests/ fixtures (golden markdown, OCR behavior). + /// PDFs live under repo tests/; samples skip if files are missing. + /// + internal partial class Program + { + private static string LlmRepositoryRootFromAppBase() => + Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..")); + + private static string LlmRepositoryTestsDirectory() => + Path.Combine(LlmRepositoryRootFromAppBase(), "tests"); + + private static bool LlmOcrEnvironmentLikelyAvailable() => + !string.IsNullOrEmpty(Utils.TESSDATA_PREFIX); + + /// ToMarkdown with fixed flags vs tests/test_370_expected.md (fixture: tests/test_370.pdf). + internal static void Test4LlmToMarkdownCompareExpected370(string[] args) + { + _ = args; + Console.WriteLine("\n=== Test4LlmToMarkdownCompareExpected370 (MuPDF.NET4LLM) ======================="); + + string testsDir = LlmRepositoryTestsDirectory(); + string pdfPath = Path.Combine(testsDir, "test_370.pdf"); + string expectedPath = Path.Combine(testsDir, "test_370_expected.md"); + if (!File.Exists(pdfPath) || !File.Exists(expectedPath)) + { + Console.WriteLine($"Skip: need test_370.pdf and test_370_expected.md in: {testsDir}"); + return; + } + + string expected = File.ReadAllText(expectedPath, Encoding.UTF8); + Document document = new Document(pdfPath); + try + { + string actual = MuPDF4LLM.ToMarkdown( + document, + header: false, + footer: false, + writeImages: false, + embedImages: false, + imageFormat: "jpg", + showProgress: true, + forceText: true, + pageSeparators: true); + + string actualPath = Path.Combine(AppContext.BaseDirectory, "llm_fixture_370_actual.md"); + File.WriteAllText(actualPath, actual, Encoding.UTF8); + Console.WriteLine($"Wrote actual markdown: {actualPath}"); + + if (!string.Equals(actual, expected, StringComparison.Ordinal)) + { + Console.WriteLine("Mismatch vs tests/test_370_expected.md (first differences):"); + LlmPrintLineDiff(expected, actual, maxLines: 40); + } + else + { + Console.WriteLine("OK: actual matches test_370_expected.md"); + } + } + finally + { + document.Close(); + } + } + + /// Default ToMarkdown on FFFD fixture; U+FFFD vs TESSDATA_PREFIX (fixture: tests/test_ocr_loremipsum_FFFD.pdf). + internal static void Test4LlmToMarkdownOcrFixture1(string[] args) + { + _ = args; + Console.WriteLine("\n=== Test4LlmToMarkdownOcrFixture1 ======================="); + + string pdfPath = Path.Combine(LlmRepositoryTestsDirectory(), "test_ocr_loremipsum_FFFD.pdf"); + if (!File.Exists(pdfPath)) + { + Console.WriteLine($"Skip: missing {pdfPath}"); + return; + } + + Document doc = new Document(pdfPath); + string md; + try + { + md = MuPDF4LLM.ToMarkdown(doc); + } + finally + { + doc.Close(); + } + + File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "llm_ocr_fixture_1.md"), md, Encoding.UTF8); + bool ocr = LlmOcrEnvironmentLikelyAvailable(); + Console.WriteLine($"TESSDATA_PREFIX set: {ocr}"); + bool hasReplacement = md.Contains(MuPDF.NET4LLM.Ocr.TesseractApi.ReplacementUnicode); + if (ocr && hasReplacement) + Console.WriteLine("Note: U+FFFD still present—check tessdata / language / PDF."); + else if (ocr && !hasReplacement) + Console.WriteLine("OK: no U+FFFD when tessdata is configured."); + else if (!ocr && hasReplacement) + Console.WriteLine("OK: U+FFFD present without tessdata."); + else + Console.WriteLine("Note: no U+FFFD without tessdata—compare llm_ocr_fixture_1.md."); + } + + /// ToMarkdown(..., useOcr: false) on FFFD fixture. + internal static void Test4LlmToMarkdownOcrFixture2(string[] args) + { + _ = args; + Console.WriteLine("\n=== Test4LlmToMarkdownOcrFixture2 ======================="); + + string pdfPath = Path.Combine(LlmRepositoryTestsDirectory(), "test_ocr_loremipsum_FFFD.pdf"); + if (!File.Exists(pdfPath)) + { + Console.WriteLine($"Skip: missing {pdfPath}"); + return; + } + + Document doc = new Document(pdfPath); + string md; + try + { + md = MuPDF4LLM.ToMarkdown(doc, useOcr: false); + } + finally + { + doc.Close(); + } + + File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "llm_ocr_fixture_2.md"), md, Encoding.UTF8); + bool hasReplacement = md.Contains(MuPDF.NET4LLM.Ocr.TesseractApi.ReplacementUnicode); + Console.WriteLine(hasReplacement + ? "OK: U+FFFD present with useOcr=false." + : "Note: no U+FFFD with OCR off—fixture-dependent."); + } + + /// SVG text fixture: compare default vs useOcr: false output size (fixture: tests/test_ocr_loremipsum_svg.pdf). + internal static void Test4LlmToMarkdownOcrFixture3(string[] args) + { + _ = args; + Console.WriteLine("\n=== Test4LlmToMarkdownOcrFixture3 ======================="); + + string pdfPath = Path.Combine(LlmRepositoryTestsDirectory(), "test_ocr_loremipsum_svg.pdf"); + if (!File.Exists(pdfPath)) + { + Console.WriteLine($"Skip: missing {pdfPath}"); + return; + } + + Document doc = new Document(pdfPath); + string md; + string mdNoOcr; + try + { + md = MuPDF4LLM.ToMarkdown(doc); + mdNoOcr = MuPDF4LLM.ToMarkdown(doc, useOcr: false); + } + finally + { + doc.Close(); + } + + string baseDir = AppContext.BaseDirectory; + File.WriteAllText(Path.Combine(baseDir, "llm_ocr_fixture_3.md"), md, Encoding.UTF8); + File.WriteAllText(Path.Combine(baseDir, "llm_ocr_fixture_3_no_ocr.md"), mdNoOcr, Encoding.UTF8); + + bool ocr = LlmOcrEnvironmentLikelyAvailable(); + if (ocr) + { + if (mdNoOcr.Length < md.Length) + Console.WriteLine($"OK: with tessdata, no-OCR shorter ({mdNoOcr.Length} < {md.Length})."); + else + Console.WriteLine($"Note: lengths OCR={md.Length}, no-OCR={mdNoOcr.Length} (environment-dependent)."); + } + else + { + Console.WriteLine(string.Equals(md, mdNoOcr, StringComparison.Ordinal) + ? "OK: without tessdata, OCR on/off often match." + : "Note: outputs differ; compare llm_ocr_fixture_3*.md."); + } + } + + private static void LlmPrintLineDiff(string expected, string actual, int maxLines) + { + string[] a = expected.Replace("\r\n", "\n").Split('\n'); + string[] b = actual.Replace("\r\n", "\n").Split('\n'); + int n = Math.Max(a.Length, b.Length); + int printed = 0; + for (int i = 0; i < n && printed < maxLines; i++) + { + string lineA = i < a.Length ? a[i] : ""; + string lineB = i < b.Length ? b[i] : ""; + if (lineA == lineB) + continue; + Console.WriteLine($" line {i + 1}:"); + Console.WriteLine($" expected: {LlmTruncateForConsole(lineA)}"); + Console.WriteLine($" actual: {LlmTruncateForConsole(lineB)}"); + printed++; + } + if (printed >= maxLines) + Console.WriteLine(" ... (truncated)"); + } + + private static string LlmTruncateForConsole(string s, int max = 200) + { + if (string.IsNullOrEmpty(s)) + return s; + return s.Length <= max ? s : s.Substring(0, max) + "…"; + } + } +} diff --git a/Demo/Samples/Llm/Program.Llm.cs b/Demo/Samples/Llm/Program.Llm.cs new file mode 100644 index 0000000..0353ba2 --- /dev/null +++ b/Demo/Samples/Llm/Program.Llm.cs @@ -0,0 +1,437 @@ +namespace Demo +{ + internal partial class Program + { + internal static void TestMarkdownReader() + { + Console.WriteLine("\n=== TestMarkdownReader ======================="); + + var reader = new PDFMarkdownReader(); + string testFilePath = Path.GetFullPath("../../../TestDocuments/columns.pdf"); + + var docs = reader.LoadData(testFilePath); + + foreach (var doc in docs) + { + Console.WriteLine(doc.Text); + } + } + + internal static void TestGetText() + { + Console.WriteLine("\n=== TestGetText ======================="); + + var reader = new PDFMarkdownReader(); + string testFilePath = Path.GetFullPath("../../../TestDocuments/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/err_table.pdf"); + + if (!File.Exists(testFilePath)) + { + Console.WriteLine($"Error: Test file not found: {testFilePath}"); + return; + } + + Console.WriteLine($"Loading PDF: {testFilePath}"); + 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, + vertical_strategy: "lines_strict", + horizontal_strategy: "lines_strict"); + + Console.WriteLine($"Found {tables.Count} table(s) on page 0"); + + if (tables.Count > 0) + { + for (int i = 0; i < tables.Count; i++) + { + 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..."); + 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)}..."); + } + } + catch (Exception ex) + { + Console.WriteLine($" Error converting to markdown: {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, + vertical_strategy: "lines_strict", + horizontal_strategy: "lines_strict"); + + if (pageTables.Count > 0) + { + Console.WriteLine($" Page {pageNum}: {pageTables.Count} table(s)"); + totalTables += pageTables.Count; + } + currentPage.Dispose(); + } + Console.WriteLine($"Total tables found across all pages: {totalTables}"); + + page.Dispose(); + doc.Close(); + + Console.WriteLine("\n=== TestTable completed successfully ==="); + } + catch (Exception ex) + { + Console.WriteLine($"Error in TestTable: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + throw; + } + } + + internal static void TestPyMuPdfRagToMarkdown() + { + Console.WriteLine("\n=== TestPyMuPdfRagToMarkdown ======================="); + + try + { + // Find a test PDF file + //string testFilePath = Path.GetFullPath("../../../TestDocuments/national-capitals.pdf"); + string testFilePath = Path.GetFullPath("../../../TestDocuments/Magazine.pdf"); + + Document doc = new Document(testFilePath); + Console.WriteLine($"Document loaded: {doc.PageCount} page(s)"); + Console.WriteLine($"Document name: {doc.Name}"); + + // Test 1: Basic ToMarkdown with default settings + Console.WriteLine("\n--- Test 1: Basic ToMarkdown (default settings) ---"); + try + { + List pages = new List(); + pages.Add(0); + string markdown = MuPdfRag.ToMarkdown( + doc, + pages: pages, // All pages + hdrInfo: null, // Auto-detect headers + writeImages: false, + embedImages: false, + ignoreImages: false, + ignoreGraphics: false, + detectBgColor: true, + imagePath: "", + imageFormat: "png", + imageSizeLimit: 0.05f, + filename: testFilePath, + forceText: true, + pageChunks: false, + pageSeparators: false, + margins: null, + dpi: 150, + pageWidth: 612, + pageHeight: null, + tableStrategy: "lines_strict", + graphicsLimit: null, + fontsizeLimit: 3.0f, + ignoreCode: false, + extractWords: false, + showProgress: false, + useGlyphs: false, + ignoreAlpha: false + ); + + string markdownFile = "TestPyMuPdfRag_Output.md"; + File.WriteAllText(markdownFile, markdown, Encoding.UTF8); + Console.WriteLine($"Markdown output saved to: {markdownFile}"); + Console.WriteLine($"Markdown length: {markdown.Length} characters"); + if (markdown.Length > 0) + { + int previewLength = Math.Min(300, markdown.Length); + Console.WriteLine($"Preview (first {previewLength} chars):\n{markdown.Substring(0, previewLength)}..."); + } + } + catch (Exception ex) + { + 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 = "TestPyMuPdfRag_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 = "TestPyMuPdfRag_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 = "TestPyMuPdfRag_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 = "TestPyMuPdfRag_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) + { + Console.WriteLine($"An unexpected error occurred during PyMuPdfRag test: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + } + + Console.WriteLine("\n=== TestPyMuPdfRagToMarkdown Completed ======================="); + } + + internal static void TestLLM() + { + Console.WriteLine("\n=== TestLLM ======================="); + + try + { + // Display version information + Console.WriteLine($"MuPDF.NET4LLM Version: {MuPDF4LLM.Version}"); + var versionTuple = MuPDF4LLM.VersionTuple; + Console.WriteLine($"Version Tuple: ({versionTuple.major}, {versionTuple.minor}, {versionTuple.patch})"); + + // Test with a sample PDF file + string testFilePath = Path.GetFullPath("../../../TestDocuments/national-capitals.pdf"); + //string testFilePath = Path.GetFullPath("../../../TestDocuments/Magazine.pdf"); + + // Try to find a PDF with actual content if Blank.pdf doesn't work well + if (!File.Exists(testFilePath)) + { + testFilePath = Path.GetFullPath("../../../TestDocuments/Widget.pdf"); + } + + if (!File.Exists(testFilePath)) + { + Console.WriteLine($"Test PDF file not found. Skipping LLM test."); + return; + } + + Console.WriteLine($"\nTesting with PDF: {testFilePath}"); + + Document doc = new Document(testFilePath); + Console.WriteLine($"Document loaded: {doc.PageCount} page(s)"); + + string markdownStr = MuPDF4LLM.ToMarkdown(doc); + + doc.Close(); + + string markdownFile = "TestLLM.md"; + File.WriteAllText(markdownFile, markdownStr, Encoding.UTF8); + Console.WriteLine("\nLLM test completed successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"Error in TestLLM: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + } + } + + } +} diff --git a/Demo/Samples/PageContent/Program.PageContent.cs b/Demo/Samples/PageContent/Program.PageContent.cs new file mode 100644 index 0000000..89f6042 --- /dev/null +++ b/Demo/Samples/PageContent/Program.PageContent.cs @@ -0,0 +1,291 @@ +namespace Demo +{ + internal partial class Program + { + internal static void TestExtractTextWithLayout(string[] args) + { + Console.WriteLine("\n=== TestExtractTextWithLayout ====================="); + string testFilePath = Path.GetFullPath("../../../TestDocuments/columns.pdf"); + Document doc = new Document(testFilePath); + + FileStream wstream = File.Create("columns.txt"); + + for (int i = 0; i < 1/*doc.PageCount*/; i++) + { + Page page = doc[i]; + string textWithLayout = page.GetTextWithLayout(tolerance: 3); + if (!string.IsNullOrEmpty(textWithLayout)) + { + byte[] bytes = Encoding.UTF8.GetBytes(textWithLayout); + wstream.Write(bytes, 0, bytes.Length); + } + } + + wstream.Close(); + + doc.Close(); + + Console.WriteLine("Created columns.txt file"); + } + + internal static void TestWidget(string[] args) + { + Console.WriteLine("\n=== TestWidget ====================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/Widget.pdf"); + Document doc = new Document(testFilePath); + for (int i = 0; i < 1; i++) + { + var page = doc[i]; + + List entries = page.GetXObjects(); + + Widget fWidget = page.FirstWidget; + while (fWidget != null) + { + Console.WriteLine($"Widget: {fWidget}"); + Console.WriteLine($"FieldName: {fWidget.FieldName}"); + Console.WriteLine($"FieldType: {fWidget.FieldType}"); + Console.WriteLine($"FieldValue: {fWidget.FieldValue}"); + Console.WriteLine($"FieldFlags: {fWidget.FieldFlags}"); + Console.WriteLine($"FieldLabel: {fWidget.FieldLabel}"); + Console.WriteLine($"TextFont: {fWidget.TextFont}"); + Console.WriteLine($"TextFontSize: {fWidget.TextFontSize}"); + Console.WriteLine($"TextColor: {string.Join(",", fWidget.TextColor)}"); + fWidget = (Widget)fWidget.Next; + } + + foreach (var widget in page.GetWidgets()) + { + Console.WriteLine($"Widget: {widget}"); + Console.WriteLine($"FieldName: {widget.FieldName}"); + Console.WriteLine($"FieldType: {widget.FieldType}"); + Console.WriteLine($"FieldValue: {widget.FieldValue}"); + Console.WriteLine($"FieldFlags: {widget.FieldFlags}"); + Console.WriteLine($"FieldLabel: {widget.FieldLabel}"); + Console.WriteLine($"TextFont: {widget.TextFont}"); + Console.WriteLine($"TextFontSize: {widget.TextFontSize}"); + Console.WriteLine($"TextColor: {string.Join(",", widget.TextColor)}"); + + } + } + + doc.Close(); + Console.WriteLine("Widget test completed."); + } + + internal static void TestColor(string[] args) + { + Console.WriteLine("\n=== TestColor ====================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/Color.pdf"); + Document doc = new Document(testFilePath); + List images = doc.GetPageImages(0); + Console.WriteLine($"CaName: {images[0].CsName}"); + doc.Recolor(0, 4); + images = doc.GetPageImages(0); + Console.WriteLine($"CaName: {images[0].AltCsName}"); + doc.Save("ReColor.pdf"); + doc.Close(); + + Console.WriteLine("Color test completed."); + } + + internal static void TestCMYKRecolor(string[] args) + { + Console.WriteLine("\n=== TestCMYKRecolor ====================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/CMYK_Recolor.pdf"); + Document doc = new Document(testFilePath); + //List images = doc.GetPageImages(0); + //Console.WriteLine($"CaName: {images[0].CsName}"); + doc.Recolor(0, "CMYK"); + //images = doc.GetPageImages(0); + //Console.WriteLine($"CaName: {images[0].AltCsName}"); + doc.Save("CMYKRecolor.pdf"); + doc.Close(); + + Console.WriteLine("CMYK Recolor test completed."); + } + + internal static void TestSVGRecolor(string[] args) + { + Console.WriteLine("\n=== TestSVGRecolor ====================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/SvgTest.pdf"); + Document doc = new Document(testFilePath); + doc.Recolor(0, "RGB"); + doc.Save("SVGRecolor.pdf"); + doc.Close(); + + Console.WriteLine("SVG Recolor test completed."); + } + + internal static void TestReplaceImage(string[] args) + { + Console.WriteLine("\n=== TestReplaceImage ====================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/Color.pdf"); + Document doc = new Document(testFilePath); + Page page = doc[0]; + List images = page.GetImages(true); + List imgs = page.GetImageRects(images[0].Xref); + + List infos = page.GetImageInfo(xrefs: true); + + page.ReplaceImage(images[0].Xref, "../../../TestDocuments/Image/_apple.png"); + page.ReplaceImage(images[0].Xref, "../../../TestDocuments/Image/_bb-logo.png"); + + infos = page.GetImageInfo(xrefs: true); + //page.DeleteImage(images[0].Xref); + + //int newXref = page.InsertImage(imgs[0].Rect, "../../../TestDocuments/Sample.png"); + + //images = page.GetImages(true); + //imgs = page.GetImageRects(images[0].Xref); + + //page.ReplaceImage(infos[0].Xref, "../../../TestDocuments/Sample.png"); + //page.DeleteImage(images[0].Xref); + + //page.InsertImage(imgs[0].Rect, "../../../TestDocuments/Sample.jpg"); + + doc.Save("ReplaceImage.pdf"); + doc.Close(); + + Console.WriteLine("Image replacement test completed."); + } + + internal static void TestInsertImage(string[] args) + { + Console.WriteLine("\n=== TestInsertImage ====================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/Image/test.pdf"); + Document doc = new Document(testFilePath); + Page page = doc[0]; + + var pixmap1 = new Pixmap("../../../TestDocuments/Image/_apple.png"); + //var pixmap1 = new Pixmap("../../../TestDocuments/Image/30mb.jpg"); + var pixmap2 = new Pixmap("../../../TestDocuments/Image/_bb-logo.png"); + var imageRect1 = new Rect(0, 0, 100, 100); + var imageRect2 = new Rect(100, 100, 200, 200); + var imageRect3 = new Rect(100, 200, 200, 300); + var imageRect4 = new Rect(100, 300, 200, 400); + var imageRect5 = new Rect(100, 400, 200, 500); + var imageRect6 = new Rect(100, 500, 200, 600); + + var img_xref = page.InsertImage(imageRect1, pixmap: pixmap1); + Console.WriteLine(img_xref); + + //img_xref = page.InsertImage(imageRect2, "../../../TestDocuments/Image/_apple.png"); + img_xref = page.InsertImage(imageRect2, pixmap: pixmap1); + Console.WriteLine(img_xref); + img_xref = page.InsertImage(imageRect3, pixmap: pixmap2); + Console.WriteLine(img_xref); + img_xref = page.InsertImage(imageRect4, "../../../TestDocuments/Image/_bb-logo.png"); + Console.WriteLine(img_xref); + page.InsertImage(imageRect5, xref: img_xref); + Console.WriteLine(img_xref); + page.InsertImage(imageRect6, xref: img_xref); + + doc.Save("TestInsertImage.pdf"); + doc.Close(); + + Console.WriteLine("Image insertion test completed."); + } + + internal static void TestGetImageInfo(string[] args) + { + Console.WriteLine("\n=== TestGetImageInfo ====================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/Image/TestInsertImage.pdf"); + Document doc = new Document(testFilePath); + Page page = doc[0]; + + List infos = page.GetImageInfo(xrefs: true); + + doc.Close(); + + Console.WriteLine("Image info test completed."); + } + + internal static void TestGetTextPageOcr(string[] args) + { + Console.WriteLine("\n=== TestGetTextPageOcr ====================="); + + string testFilePath = Path.GetFullPath(@"../../../TestDocuments/Ocr.pdf"); + Document doc = new Document(testFilePath); + Page page = doc[0]; + + page.RemoveRotation(); + Pixmap pixmap = page.GetPixmap(); + + List blocks = page.GetText("dict", flags: (int)TextFlags.TEXT_PRESERVE_IMAGES)?.Blocks; + foreach (Block block in blocks) + { + Console.WriteLine(block.Image.Length); + } + + // build the pipeline + var pipeline = new ImageFilterPipeline(); + pipeline.Clear(); + //pipeline.AddDeskew(minAngle: 0.5); // replaces any existing deskew step + //pipeline.AddRemoveHorizontalLines(); // also replaces existing horizontal-removal step + //pipeline.AddRemoveVerticalLines(); + //pipeline.AddGrayscale(); + //pipeline.AddMedian(blockSize: 2, replaceExisting: true); + pipeline.AddGamma(gamma: 1.2); // brighten slightly + //pipeline.AddScaleFit(100); + pipeline.AddScale(scaleFactor: 3f, quality: SKFilterQuality.High); + //pipeline.AddContrast(contrast: 100); + //pipeline.AddDilation(); + //pipeline.AddInvert(); + + TextPage tp = page.GetTextPageOcr((int)TextFlags.TEXT_PRESERVE_SPANS, full: true, imageFilters: pipeline); + string txt = tp.ExtractText(); + Console.WriteLine(txt); + + doc.Close(); + + Console.WriteLine("OCR text extraction test completed."); + } + + internal static void TestCreateImagePage(string[] args) + { + Console.WriteLine("\n=== TestCreateImagePage ====================="); + + Pixmap pxmp = new Pixmap("../../../TestDocuments/Image/_bb-logo.png"); + + Document doc = new Document(); + Page page = doc.NewPage(width:pxmp.W, height:pxmp.H); + + page.InsertImage(page.Rect, pixmap: pxmp); + + pxmp.Dispose(); + + doc.Save("_bb-logo.pdf", pretty: 1); + doc.Close(); + + Console.WriteLine("Image page creation test completed."); + } + + internal static void TestJoinPdfPages(string[] args) + { + Console.WriteLine("\n=== TestJoinPdfPages ====================="); + + string testFilePath1 = Path.GetFullPath(@"../../../TestDocuments/Widget.pdf"); + Document doc1 = new Document(testFilePath1); + string testFilePath2 = Path.GetFullPath(@"../../../TestDocuments/Color.pdf"); + Document doc2 = new Document(testFilePath2); + + doc1.InsertPdf(doc2, 0, 0, 2); + + doc1.Save("Joined.pdf", pretty: 1); + + doc2.Close(); + doc1.Close(); + + Console.WriteLine("PDF pages joined successfully into 'Joined.pdf'."); + } + + } +} diff --git a/Demo/Samples/Regression/Program.Regression.cs b/Demo/Samples/Regression/Program.Regression.cs new file mode 100644 index 0000000..e9b10b8 --- /dev/null +++ b/Demo/Samples/Regression/Program.Regression.cs @@ -0,0 +1,168 @@ +namespace Demo +{ + internal partial class Program + { + internal static void TestIssue234() + { + Console.WriteLine("\n=== TestIssue234 ======================="); + + var pix = new Pixmap("../../../TestDocuments/Image/boxedpage.jpg"); // 629x1000 image + var scaled = new Pixmap(pix, 943, 1500, null); // scale up + byte[] jpeg = scaled.ToBytes("jpg", 65); + + using var doc = new Document(); + Page page = doc.NewPage(0, 943, 1500); + page.InsertImage(page.Rect, stream: jpeg); + page.Dispose(); + doc.Save("issue_234.pdf"); + doc.Close(); + } + + internal static void TestRecompressJBIG2() + { + Console.WriteLine("\n=== TestJBIG2 ======================="); + + string testFilePath = Path.GetFullPath("../../../TestDocuments/Jbig2.pdf"); + + Document doc = new Document(testFilePath); + + PdfImageRewriterOptions opts = new PdfImageRewriterOptions(); + + opts.bitonal_image_recompress_method = mupdf.mupdf.FZ_RECOMPRESS_FAX; + opts.recompress_when = mupdf.mupdf.FZ_RECOMPRESS_WHEN_ALWAYS; + + doc.RewriteImage(options: opts); + + doc.Save(@"e:\TestRecompressJBIG2.pdf"); + doc.Close(); + } + + internal static void TestIssue1880() + { + Console.WriteLine("\n=== TestIssue1880 ======================="); + + string testFilePath = Path.GetFullPath(@"../../../TestDocuments/issue_1880.pdf"); + + Document doc = new Document(testFilePath); + + for (int i = 0; i < doc.PageCount; i++) + { + Page page = doc[i]; + + List barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.DM, pureBarcode:true); + foreach (Barcode barcode in barcodes) + { + BarcodePoint[] points = barcode.ResultPoints; + Console.WriteLine($"Page {i++} - Type: {barcode.BarcodeFormat} - Value: {barcode.Text} - Rect: [{points[0]},{points[1]}]"); + } + + page.Dispose(); + } + + doc.Close(); + } + + internal static void TestIssue213() + { + Console.WriteLine("\n=== TestIssue213 ======================="); + + string origfilename = @"../../../TestDocuments/issue_213.pdf"; + string outfilename = @"../../../TestDocuments/Blank.pdf"; + float newWidth = 0.5f; + + Document inputDoc = new Document(origfilename); + Document outputDoc = new Document(outfilename); + + if (inputDoc.PageCount != outputDoc.PageCount) + { + return; + } + + for (int pagNum = 0; pagNum < inputDoc.PageCount; pagNum++) + { + Page page = inputDoc.LoadPage(pagNum); + + Pixmap pxmp = page.GetPixmap(); + pxmp.Save(@"output.png"); + pxmp.Dispose(); + + Page outPage = outputDoc.LoadPage(pagNum); + List paths = page.GetDrawings(extended: false); + int totalPaths = paths.Count; + + int i = 0; + foreach (PathInfo pathInfo in paths) + { + Shape shape = outPage.NewShape(); + foreach (Item item in pathInfo.Items) + { + if (item != null) + { + if (item.Type == "l") + { + shape.DrawLine(item.P1, item.LastPoint); + //writer.Write($"{i:000}\\] line: {item.Type} >>> {item.P1}, {item.LastPoint}\\n"); + } + else if (item.Type == "re") + { + shape.DrawRect(item.Rect, item.Orientation); + //writer.Write($"{i:000}\\] rect: {item.Type} >>> {item.Rect}, {item.Orientation}\\n"); + } + else if (item.Type == "qu") + { + shape.DrawQuad(item.Quad); + //writer.Write($"{i:000}\\] quad: {item.Type} >>> {item.Quad}\\n"); + } + else if (item.Type == "c") + { + shape.DrawBezier(item.P1, item.P2, item.P3, item.LastPoint); + //writer.Write($"{i:000}\\] curve: {item.Type} >>> {item.P1}, {item.P2}, {item.P3}, {item.LastPoint}\\n"); + } + else + { + throw new Exception("unhandled drawing. Aborting..."); + } + } + } + + //pathInfo.Items.get + float newLineWidth = pathInfo.Width; + if (pathInfo.Width <= newWidth) + { + newLineWidth = newWidth; + } + + int lineCap = 0; + if (pathInfo.LineCap != null && pathInfo.LineCap.Count > 0) + lineCap = (int)pathInfo.LineCap[0]; + shape.Finish( + fill: pathInfo.Fill, + color: pathInfo.Color, //this.\_m_DEFAULT_COLOR, + evenOdd: pathInfo.EvenOdd, + closePath: pathInfo.ClosePath, + lineJoin: (int)pathInfo.LineJoin, + lineCap: lineCap, + width: newLineWidth, + strokeOpacity: pathInfo.StrokeOpacity, + fillOpacity: pathInfo.FillOpacity, + dashes: pathInfo.Dashes + ); + + // file_export.write(f'Path {i:03}\] width: {lwidth}, dashes: {path\["dashes"\]}, closePath: {path\["closePath"\]}\\n') + //writer.Write($"Path {i:000}\\] with: {newLineWidth}, dashes: {pathInfo.Dashes}, closePath: {pathInfo.ClosePath}\\n"); + + i++; + shape.Commit(); + } + } + + inputDoc.Close(); + + outputDoc.Save(@"output.pdf"); + outputDoc.Close(); + + //writer.Close(); + } + + } +} diff --git a/Demo/Samples/TextDrawing/Program.TextDrawing.cs b/Demo/Samples/TextDrawing/Program.TextDrawing.cs new file mode 100644 index 0000000..d927a16 --- /dev/null +++ b/Demo/Samples/TextDrawing/Program.TextDrawing.cs @@ -0,0 +1,366 @@ +namespace Demo +{ + internal partial class Program + { + internal static void CreateAnnotDocument() + { + Console.WriteLine("\n=== CreateAnnotDocument ======================="); + Rect r = Constants.r; // use the rectangle defined in Constants.cs + + Document doc = new Document(); + Page page = doc.NewPage(); + + page.SetRotation(0); // no rotation + + TextWriter pw = new TextWriter(page.TrimBox); + string txt = "Origin 100.100"; + pw.Append(new Point(100, 500), txt, new Font("tiro"), fontSize: 24); + pw.WriteText(page, new float[]{0,0.4f,1}, oc: 0); + + + + Annot annot = page.AddRectAnnot(r); // 'Square' + annot.SetBorder(width: 1f, dashes: new int[] { 1, 2 }); + annot.SetColors(stroke: Constants.blue, fill: Constants.gold); + annot.Update(opacity: 0.5f); + + doc.Save(@"CreateAnnotDocument.pdf"); + + doc.Close(); + } + + internal static void TestDrawShape() + { + string origfilename = @"../../../TestDocuments/NewAnnots.pdf"; + string outfilename = @"../../../TestDocuments/Blank.pdf"; + float newWidth = 0.5f; + + Document inputDoc = new Document(origfilename); + Document outputDoc = new Document(outfilename); + + //string filePath = @"D:\\Vectorlab\\Jobs\\2025\\PACE\\pdf_fix\\assets\\exported_paths_net.txt"; + //StreamWriter writer = new StreamWriter(filePath); + + if (inputDoc.PageCount != outputDoc.PageCount) + { + return; + } + + for (int pagNum = 0; pagNum < inputDoc.PageCount; pagNum++) + { + Page page = inputDoc.LoadPage(pagNum); + Page outPage = outputDoc.LoadPage(pagNum); + List paths = page.GetDrawings(extended: false); + int totalPaths = paths.Count; + + int i = 0; + foreach (PathInfo pathInfo in paths) + { + Shape shape = outPage.NewShape(); + foreach (Item item in pathInfo.Items) + { + if (item != null) + { + if (item.Type == "l") + { + shape.DrawLine(item.P1, item.LastPoint); + //writer.Write($"{i:000}\\] line: {item.Type} >>> {item.P1}, {item.LastPoint}\\n"); + } + else if (item.Type == "re") + { + shape.DrawRect(item.Rect, item.Orientation); + //writer.Write($"{i:000}\\] rect: {item.Type} >>> {item.Rect}, {item.Orientation}\\n"); + } + else if (item.Type == "qu") + { + shape.DrawQuad(item.Quad); + //writer.Write($"{i:000}\\] quad: {item.Type} >>> {item.Quad}\\n"); + } + else if (item.Type == "c") + { + shape.DrawBezier(item.P1, item.P2, item.P3, item.LastPoint); + //writer.Write($"{i:000}\\] curve: {item.Type} >>> {item.P1}, {item.P2}, {item.P3}, {item.LastPoint}\\n"); + } + else + { + throw new Exception("unhandled drawing. Aborting..."); + } + } + } + + //pathInfo.Items.get + float newLineWidth = pathInfo.Width; + if (pathInfo.Width <= newWidth) + { + newLineWidth = newWidth; + } + + int lineCap = 0; + if (pathInfo.LineCap != null && pathInfo.LineCap.Count > 0) + lineCap = (int)pathInfo.LineCap[0]; + shape.Finish( + fill: pathInfo.Fill, + color: pathInfo.Color, //this.\_m_DEFAULT_COLOR, + evenOdd: pathInfo.EvenOdd, + closePath: pathInfo.ClosePath, + lineJoin: (int)pathInfo.LineJoin, + lineCap: lineCap, + width: newLineWidth, + strokeOpacity: pathInfo.StrokeOpacity, + fillOpacity: pathInfo.FillOpacity, + dashes: pathInfo.Dashes + ); + + // file_export.write(f'Path {i:03}\] width: {lwidth}, dashes: {path\["dashes"\]}, closePath: {path\["closePath"\]}\\n') + //writer.Write($"Path {i:000}\\] with: {newLineWidth}, dashes: {pathInfo.Dashes}, closePath: {pathInfo.ClosePath}\\n"); + + i++; + shape.Commit(); + } + } + + inputDoc.Close(); + + outputDoc.Save(@"TestDrawShape.pdf"); + outputDoc.Close(); + + //writer.Close(); + } + + internal static void DrawLine(Page page, float startX, float startY, float endX, float endY, Color lineColor = null, float lineWidth = 1, bool dashed = false) + { + Console.WriteLine("\n=== DrawLine ======================="); + + if (lineColor == null) + { + lineColor = new Color(); // Default to black + lineColor.Stroke = new float[] { 0, 0, 0 }; // RGB black + } + Shape img = page.NewShape(); + Point startPoint = new Point(startX, startY); + Point endPoint = new Point(endX, endY); + + String dashString = ""; + if (dashed == true) + { + dashString = "[2] 0"; // Example dash pattern + } + + img.DrawLine(startPoint, endPoint); + img.Finish(width: lineWidth, color: lineColor.Stroke, dashes: dashString); + img.Commit(); + + Console.WriteLine($"Line drawn from ({startX}, {startY}) to ({endX}, {endY}) with color {lineColor.Stroke} and width {lineWidth}."); + } + + internal static void TestDrawLine() + { + Console.WriteLine("\n=== TestDrawLine ======================="); + + Document doc = new Document(); + + Page page = doc.NewPage(); + + string fontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); + + page.DrawLine(new Point(45, 50), new Point(80, 50), width: 0.5f, dashes: "[5] 0"); + page.DrawLine(new Point(90, 50), new Point(150, 50), width: 0.5f, dashes: "[5] 0"); + page.DrawLine(new Point(45, 80), new Point(180, 80), width: 0.5f, dashes: "[5] 0"); + page.DrawLine(new Point(45, 100), new Point(180, 100), width: 0.5f, dashes: "[5] 0"); + + //DrawLine(page, 45, 50, 80, 50, lineWidth: 0.5f, dashed: true); + //DrawLine(page, 90, 60, 150, 60, lineWidth: 0.5f, dashed: true); + //DrawLine(page, 45, 80, 180, 80, lineWidth: 0.5f, dashed: true); + //DrawLine(page, 45, 100, 180, 100, lineWidth: 0.5f, dashed: true); + + doc.Save(@"TestDrawLine.pdf"); + + page.Dispose(); + doc.Close(); + + Console.WriteLine("Write to TestDrawLine.pdf"); + } + + internal static void TestTextFont(string[] args) + { + Console.WriteLine("\n=== TestTextFont ======================="); + //for (int i = 0; i < 100; i++) + { + Document doc = new Document(); + + Page page0 = doc.NewPage(); + Page page1 = doc.NewPage(pno: -1, width: 595, height: 842); + + string fontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); + + float[] blue = new float[] { 0.0f, 0.0f, 1.0f }; + float[] red = new float[] { 1.0f, 0.0f, 0.0f }; + + Rect rect1 = new Rect(100, 100, 510, 200); + Rect rect2 = new Rect(100, 250, 300, 400); + + MuPDF.NET.Font font1 = new MuPDF.NET.Font("asdfasdf"); + //MuPDF.NET.Font font1 = new MuPDF.NET.Font("arial", fontDir+"\\arial_0.ttf"); + MuPDF.NET.Font font2 = new MuPDF.NET.Font("times", fontDir + "\\times.ttf"); + + string text1 = "This is a test of the FillTextbox method with Arial font."; + string text2 = "This is another test with Times New Roman font."; + + MuPDF.NET.TextWriter tw1 = new MuPDF.NET.TextWriter(page0.Rect); + tw1.FillTextbox(rect: rect1, text: text1, font: font1, fontSize:20); + font1.Dispose(); + tw1.WriteText(page0); + + MuPDF.NET.TextWriter tw2 = new MuPDF.NET.TextWriter(page0.Rect, color: red); + tw2.FillTextbox(rect: rect2, text: text2, font: font2, fontSize: 10, align: (int)TextAlign.TEXT_ALIGN_LEFT); + font2.Dispose(); + tw2.WriteText(page0); + + doc.Save(@"TestTextFont.pdf"); + + page0.Dispose(); + doc.Close(); + + Console.WriteLine("Write to TestTextFont.pdf"); + } + + } + + internal static void TestInsertHtmlbox() + { + Console.WriteLine("\n=== TestInsertHtmlbox ======================="); + + Rect rect = new Rect(100, 100, 550, 2250); + Document doc = new Document(); + Page page = doc.NewPage(); + + string htmlString = "

生产准备:

1. 每日生产进行维护保养,请参照并填写Philips 自动螺丝起点检表《WI-Screw assembly-Makita DF010&Kilews

SKD-B512L-F01》

2 .扭力计UNIT选择‘lbf.in’,‘P-P’模式,每四小时检查一次,每次检查5组数据,只有合格才可以生产;并填写

力扭矩记录表,表单号 : F-EN-34 。

1.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü 5±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü5.0πÇé

2.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü10±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü10.0πÇé

3.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü12±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü12.0πÇé

Colten - line break

生产准备:

1. 每日生产进行维护保养,请参照并填写Philips 自动螺丝起点检表《WI-Screw assembly-Makita DF010&Kilews

SKD-B512L-F01》

2 .扭力计UNIT选择‘lbf.in’,‘P-P’模式,每四小时检查一次,每次检查5组数据,只有合格才可以生产;并填写

力扭矩记录表,表单号 : F-EN-34 。

1.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü 5±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü5.0πÇé

2.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü10±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü10.0πÇé

3.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü12±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü12.0πÇé

Colten - line break

生产准备:

1. 每日生产进行维护保养,请参照并填写Philips 自动螺丝起点检表《WI-Screw assembly-Makita DF010&Kilews

SKD-B512L-F01》

2 .扭力计UNIT选择‘lbf.in’,‘P-P’模式,每四小时检查一次,每次检查5组数据,只有合格才可以生产;并填写

力扭矩记录表,表单号 : F-EN-34 。

1.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü 5±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü5.0πÇé

2.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü10±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü10.0πÇé

3.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü12±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü12.0πÇé

Colten - line break

生产准备:

1. 每日生产进行维护保养,请参照并填写Philips 自动螺丝起点检表《WI-Screw assembly-Makita DF010&Kilews

SKD-B512L-F01》

2 .扭力计UNIT选择‘lbf.in’,‘P-P’模式,每四小时检查一次,每次检查5组数据,只有合格才可以生产;并填写

力扭矩记录表,表单号 : F-EN-34 。

1.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü 5±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü5.0πÇé

2.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü10±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü10.0πÇé

3.τö╡σè¿Φ╡╖σ¡Éσè¢τƒ⌐∩╝Ü12±1 in-lbs∩╝îτö╡σè¿Φ₧║Σ╕¥Φ╡╖τ╝ûσÅ╖∩╝Ü12.0πÇé

"; + (float s, float scale) = page.InsertHtmlBox(rect, htmlString, scaleLow: 0f); + doc.Save(@"TestInsertHtmlbox.pdf"); + + page.Dispose(); + doc.Close(); + + Console.WriteLine($"Inserted HTML box with scale: {scale} and size: {s}"); + } + + internal static void TestLineAnnot() + { + Console.WriteLine("\n=== TestLineAnnot ======================="); + Document newDoc = new Document(); + Page newPage = newDoc.NewPage(); + + newPage.AddLineAnnot(new Point(100, 100), new Point(300, 300)); + + newDoc.Save(@"TestLineAnnot1.pdf"); + newDoc.Close(); + + Document doc = new Document(@"TestLineAnnot1.pdf"); // open a document + List annotationsToUpdate = new List(); + Page page = doc[0]; + // Fix: Correctly handle the IEnumerable returned by GetAnnots() + IEnumerable annots = page.GetAnnots(); + foreach (Annot annot in annots) + { + Console.WriteLine("Annotation on page width before modified: " + annot.Border.Width); + annot.SetBorder(width: 8); + annot.Update(); + Console.WriteLine("Annotation on page width after modified: " + annot.Border.Width); + } + annotationsToUpdate.Clear(); + doc.Save(@"TestLineAnnot2.pdf"); // Save the modified document + doc.Close(); // Close the document + } + + internal static void TestHelloWorldToNewDocument(string[] args) + { + Console.WriteLine("\n=== TestHelloWorldToNewDocument ======================="); + Document doc = new Document(); + Page page = doc.NewPage(); + + //{ "helv", "Helvetica" }, + //{ "heit", "Helvetica-Oblique" }, + //{ "hebo", "Helvetica-Bold" }, + //{ "hebi", "Helvetica-BoldOblique" }, + //{ "cour", "Courier" }, + //{ "cobo", "Courier-Bold" }, + //{ "cobi", "Courier-BoldOblique" }, + //{ "tiro", "Times-Roman" }, + //{ "tibo", "Times-Bold" }, + //{ "tiit", "Times-Italic" }, + //{ "tibi", "Times-BoldItalic" }, + //{ "symb", "Symbol" }, + //{ "zadb", "ZapfDingbats" } + MuPDF.NET.TextWriter writer = new MuPDF.NET.TextWriter(page.Rect); + var ret = writer.FillTextbox(page.Rect, "Hello World!", new MuPDF.NET.Font(fontName: "helv"), rtl: true); + writer.WriteText(page); + doc.Save("text.pdf", pretty: 1); + doc.Close(); + + Console.WriteLine($"Text written to 'text.pdf' in: {page.Rect}"); + } + + internal static void TestHelloWorldToExistingDocument(string[] args) + { + Console.WriteLine("\n=== TestHelloWorldToExistingDocument ======================="); + string testFilePath = Path.GetFullPath("../../../TestDocuments/Blank.pdf"); + Document doc = new Document(testFilePath); + + Page page = doc[0]; + + Rect rect = new Rect(100, 100, 510, 210); + page.DrawRect(rect); + + MuPDF.NET.TextWriter writer = new MuPDF.NET.TextWriter(page.Rect); + //Font font = new Font("kenpixel", "../../../kenpixel.ttf", isBold: 1); + Font font = new Font("cobo", isBold: 0); + var ret = writer.FillTextbox(page.Rect, "123456789012345678901234567890Peter Test- this is a string that is too long to fit into the TextBox", font, rtl: false); + writer.WriteText(page); + + doc.Save("text1.pdf", pretty: 1); + + doc.Close(); + + Console.WriteLine($"Text written to 'text1.pdf' in: {page.Rect}"); + } + + internal static void TestFreeTextAnnot(string[] args) + { + Console.WriteLine("\n=== TestFreeTextAnnot ====================="); + + Rect r = new Rect(72, 72, 220, 100); + string t1 = "t├¬xt ├╝s├¿s L├ñti├▒ char├ƒ,\nEUR: Γé¼, mu: ┬╡, super scripts: ┬▓┬│!"; + Rect rect = new Rect(100,100,200,200); + float[] red = new float[] { 1, 0, 0 }; + float[] blue = new float[] { 0, 0, 1 }; + float[] gold = new float[] { 1, 1, 0 }; + float[] green = new float[] { 0, 1, 0 }; + float[] white = new float[] { 1, 1, 1 }; + + Document doc = new Document(); + Page page = doc.NewPage(); + + Annot annot = page.AddFreeTextAnnot( + rect, + t1, + fontSize: 10, + rotate: 90, + textColor: red, + fillColor: gold, + align: (int)TextAlign.TEXT_ALIGN_CENTER, + dashes: new int[] { 2 } + ); + + annot.SetBorder(border: null, width: 0.3f, dashes: new int[] { 2 }); + annot.Update(textColor: blue); + //annot.Update(textColor: red, fillColor: blue); + + doc.Save("FreeTextAnnot.pdf"); + + doc.Close(); + + Console.WriteLine("Free text annotation created and saved to 'FreeTextAnnot.pdf'."); + } + + } +} diff --git a/Demo/_Constants.cs b/Demo/Support/Constants.cs similarity index 100% rename from Demo/_Constants.cs rename to Demo/Support/Constants.cs diff --git a/Demo/Support/Units.cs b/Demo/Support/Units.cs new file mode 100644 index 0000000..730f7d3 --- /dev/null +++ b/Demo/Support/Units.cs @@ -0,0 +1,14 @@ +namespace Demo +{ + public static class Units + { + public const float InchesPerMm = 1.0f / 25.4f; + public const float PointsPerInch = 72.0f; + + public static float MmToPoints(float mm) => mm * InchesPerMm * PointsPerInch; + public static float PointsToMm(float points) => points / PointsPerInch / InchesPerMm; + + public static float MmToPixels(float mm, float dpi) => mm * InchesPerMm * dpi; + public static float PixelsToMm(float px, float dpi) => px / dpi / InchesPerMm; + } +} diff --git a/Demo/annotations-freetext1.cs b/Demo/annotations-freetext1.cs deleted file mode 100644 index f9ea709..0000000 --- a/Demo/annotations-freetext1.cs +++ /dev/null @@ -1,42 +0,0 @@ -using MuPDF.NET; -using SkiaSharp; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; - -namespace Demo -{ - public static class AnnotationsFreeText1 - { - public static void Run(string[] args) - { - Console.WriteLine("\n=== AnnotationsFreeText1 ======================="); - Document doc = new Document(); - Page page = doc.NewPage(); - - // 3 rectangles, same size, above each other - Rect r1 = new Rect(100, 100, 200, 150); - Rect r2 = r1 + new Rect(0, 75, 0, 75); - Rect r3 = r2 + new Rect(0, 75, 0, 75); - - // the text, Latin alphabet - string t = "¡Un pequeño texto para practicar!"; - - // add 3 annots, modify the last one somewhat - Annot a1 = page.AddFreeTextAnnot(r1, t, textColor: Constants.red); - Annot a2 = page.AddFreeTextAnnot(r2, t, fontName: "Ti", textColor: Constants.blue); - Annot a3 = page.AddFreeTextAnnot(r3, t, fontName: "Co", textColor: Constants.blue, rotate: 90); - a3.SetBorder(width: 0); - a3.Update(fontSize: 8, fillColor: Constants.gold); - - doc.Save("a-freetext.pdf"); - - doc.Close(); - - Console.WriteLine("Saved to a-freetext.pdf"); - } - } -} diff --git a/Demo/annotations-freetext2.cs b/Demo/annotations-freetext2.cs deleted file mode 100644 index fb3a0e3..0000000 --- a/Demo/annotations-freetext2.cs +++ /dev/null @@ -1,65 +0,0 @@ -using MuPDF.NET; -using SkiaSharp; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.IO.Ports; -using System.Linq; -using System.Security.Policy; -using System.Text; -using static System.Net.Mime.MediaTypeNames; - -namespace Demo -{ - public static class AnnotationsFreeText2 - { - // Use rich text for FreeText annotations - public static void Run(string[] args) - { - Console.WriteLine("\n=== AnnotationsFreeText2 ======================="); - // define an overall styling - string ds = "font-size: 11pt; font-family: sans-serif;"; - // some special characters - string bullet = "\u2610\u2611\u2612"; // Output: ☐☑☒ - - // the annotation text with HTML and styling syntax - string text = $@"

-MuPDF.NET འདི་ ཡིག་ཆ་བཀྲམ་སྤེལ་གྱི་དོན་ལུ་ པའི་ཐོན་ཐུམ་སྒྲིལ་དྲག་ཤོས་དང་མགྱོགས་ཤོས་ཅིག་ཨིན། -Here is some bold and italic text, followed by bold-italic. Text-based check boxes: {bullet}. -

"; - - Document doc = new Document(); - Page page = doc.NewPage(); - - // 3 rectangles, same size, above each other - Rect rect = new Rect(100, 100, 350, 200); - - // define some points for callout lines - Point p2 = rect.TopRight + new Point(50, 30); - Point p3 = p2 + new Point(0, 30); - - // define the annotation - Annot annot = page.AddFreeTextAnnot( - rect, - text, - fillColor: Constants.gold, // fill color - opacity: 1, // non-transparent - rotate: 0, // no rotation - borderWidth: 1, // border and callout line width - dashes: null, // no dashing - richtext: true, // this is rich text - style: ds, // my styling default - callout: new Point[]{ p3, p2, rect.TopRight }, // define end, knee, start points - lineEnd: PdfLineEnding.PDF_ANNOT_LE_OPEN_ARROW, // symbol shown at p3 - borderColor: Constants.green - ); - - doc.Save(typeof(AnnotationsFreeText2).Name + ".pdf", pretty:1); - - doc.Close(); - - Console.WriteLine("Saved to " + typeof(AnnotationsFreeText2).Name + ".pdf"); - } - } -} diff --git a/MuPDF.NET.sln b/MuPDF.NET.sln index 755bc8f..85787e7 100644 --- a/MuPDF.NET.sln +++ b/MuPDF.NET.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.14.36511.14 diff --git a/MuPDF.NET/MuPDF.NET.csproj b/MuPDF.NET/MuPDF.NET.csproj index 64e8d06..4ba8b61 100644 --- a/MuPDF.NET/MuPDF.NET.csproj +++ b/MuPDF.NET/MuPDF.NET.csproj @@ -60,4 +60,13 @@ + + + + + + diff --git a/MuPDF.NET/MuPDF.NET.nuspec b/MuPDF.NET/MuPDF.NET.nuspec index 83c6a4a..7c0e641 100644 --- a/MuPDF.NET/MuPDF.NET.nuspec +++ b/MuPDF.NET/MuPDF.NET.nuspec @@ -29,24 +29,24 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + diff --git a/MuPDF.NET4LLM/Description.md b/MuPDF.NET4LLM/Description.md new file mode 100644 index 0000000..7ea5283 --- /dev/null +++ b/MuPDF.NET4LLM/Description.md @@ -0,0 +1,17 @@ +## About + +**MuPDF.NET4LLM** 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 MuPDF.NET4LLM`. MuPDF.NET is installed automatically as a dependency. + +## License and Copyright + +**MuPDF.NET4LLM** 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/MuPDF.NET4LLM/LICENSE.md b/MuPDF.NET4LLM/LICENSE.md new file mode 100644 index 0000000..97ae10b --- /dev/null +++ b/MuPDF.NET4LLM/LICENSE.md @@ -0,0 +1,60 @@ +# ARTIFEX COMMUNITY LICENSE + +**Version 1, 11th December 2024** + +This **Artifex Community License** ("License") governs the use, reproduction, and distribution of **MuPDF.NET4LLM** ("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**. + +## 1. Definitions + +- **Commercial Use**: Any use of the Software for commercial purposes, including, but not limited to, use as part of, or in any workflow in support of, a product, service, or platform that directly or indirectly generates revenue. +- **Non-Commercial Use**: Any use of the software that does not fall into the **Commercial Use** category, such as for personal, or educational purposes. +- **Licensor**: Artifex Software, Inc. +- **You**: Any individual or entity using the Software. + +## 2. Grant of License + +1. **Non-Commercial Use**: + - You are granted a royalty-free, perpetual, non-exclusive license to use, modify, and distribute the Software for Non-Commercial Use, subject to the terms of this License. +2. **Commercial Use**: + - This license does not cover Commercial Use. + - If you intend to use the Software for Commercial Use you are required to obtain a Commercial Use License - [contact the Licensor](https://artifex.com/contact) to negotiate a Commercial Use License. + +## 3. Permitted Uses + +1. You may: + - Access and modify the Software’s source code. + - Use the Software for any lawful purpose, subject to the limitations outlined in Section 2. + - Combine the Software with other works, provided that any distribution complies with this License. + - Distribute unmodified source code. + +## 4. Restrictions + +1. You may not: + - Use the Software for any Commercial Use. If Commercial Use is required [contact the Licensor](https://artifex.com/contact) to negotiate a Commercial Use License. + - Remove or obscure any copyright, trademark, or attribution notices included in the Software. + - Distribute modified versions of the source code. + +2. Derivative works must include prominent attribution to the original Software and a copy of this License. + +## 5. Attribution + +All distributions of the Software, whether modified or unmodified, must include: +- A prominent notice stating: *"This software is based on MuPDF.NET, developed by Artifex Software, Inc. and licensed under the Artifex Community License."* +- A copy of this License. + +## 6. Disclaimer of Warranty + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## 7. Limitation of Liability + +IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +## 8. Termination + +1. This License is effective until terminated. +2. Your rights under this License will terminate automatically if you breach any of its terms. Upon termination, you must cease all use of the Software and destroy all copies in your possession. + + diff --git a/MuPDF.NET4LLM/MuPDF.NET4LLM.csproj b/MuPDF.NET4LLM/MuPDF.NET4LLM.csproj index 7a76650..71ecf9b 100644 --- a/MuPDF.NET4LLM/MuPDF.NET4LLM.csproj +++ b/MuPDF.NET4LLM/MuPDF.NET4LLM.csproj @@ -3,9 +3,12 @@ netstandard2.0;net461;net472;net48;net5.0;net6.0;net7.0;net8.0 AnyCPU;x64;x86 - false + True $(Platform) . + 0.3.4-rc.1 + $(MSBuildProjectDirectory)\MuPDF.NET4LLM.nuspec + Configuration=$(Configuration);version=$(Version);PlatformFolder=$(PlatformFolder) diff --git a/MuPDF.NET4LLM/MuPDF.NET4LLM.nuspec b/MuPDF.NET4LLM/MuPDF.NET4LLM.nuspec new file mode 100644 index 0000000..3fe57c6 --- /dev/null +++ b/MuPDF.NET4LLM/MuPDF.NET4LLM.nuspec @@ -0,0 +1,58 @@ + + + + MuPDF.NET4LLM + $version$ + Artifex Software Inc. + true + LICENSE.md + logo.png + Description.md + https://github.com/ArtifexSoftware/MuPDF.NET + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MuPDF.NET4LLM/README.md b/MuPDF.NET4LLM/README.md new file mode 100644 index 0000000..bc1a1f9 --- /dev/null +++ b/MuPDF.NET4LLM/README.md @@ -0,0 +1,95 @@ +# MuPDF.NET4LLM + +LLM/RAG helpers for [MuPDF.NET](https://www.nuget.org/packages/MuPDF.NET): PDF-to-Markdown conversion, layout parsing, document structure analysis. Designed for use with RAG pipelines and integration with LLMs. + +## Installation + +```bash +dotnet add package MuPDF.NET4LLM +``` + +MuPDF.NET4LLM depends on [MuPDF.NET](https://www.nuget.org/packages/MuPDF.NET); it is installed automatically. + +## Features + +- **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 +- **Plain text extraction** — Same layout analysis as Markdown, without syntax +- **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 + +## Quick Start + +### Convert PDF to Markdown + +```csharp +using MuPDF.NET; +using MuPDF.NET4LLM; + +Document doc = new Document("document.pdf"); +string markdown = MuPDF4LLM.ToMarkdown(doc); +doc.Close(); +``` + +### Convert to plain text + +```csharp +string text = MuPDF4LLM.ToText(doc); +``` + +### Get layout as JSON + +```csharp +string json = MuPDF4LLM.ToJson(doc); +``` + +### Use with LlamaIndex-style loading + +```csharp +var reader = MuPDF4LLM.LlamaMarkdownReader(); +var docs = reader.LoadData("document.pdf", extraInfo: new Dictionary()); +foreach (var d in docs) +{ + Console.WriteLine($"Page {d.ExtraInfo["page"]}: {d.Text}"); +} +``` + +### Extract form field values + +```csharp +var keyValues = MuPDF4LLM.GetKeyValues(doc); +``` + +## API Overview + +| Method | Description | +|--------|-------------| +| `ToMarkdown()` | Convert document (or selected pages) to Markdown with optional images | +| `ToText()` | Convert to plain text using layout analysis | +| `ToJson()` | Export layout structure as JSON | +| `ParseDocument()` | Return a `ParsedDocument` with pages, boxes, tables, images | +| `LlamaMarkdownReader()` | Create a LlamaIndex-compatible PDF reader | +| `GetKeyValues()` | Extract form field name/value pairs and page locations | + +## Options + +`ToMarkdown`, `ToText`, and `ToJson` support options such as: + +- `pages` — Restrict to specific pages (0-based) +- `writeImages` / `embedImages` — Save or embed images +- `imagePath`, `imageFormat` — Where and how to store images +- `useOcr`, `ocrLanguage` — OCR for scanned content +- `showProgress` — Log progress while processing +- `forceText` — Prefer text extraction over image backgrounds + +## Requirements + +- .NET Standard 2.0 or later (net461, net472, net48, net5.0, net6.0, net7.0, net8.0) +- [MuPDF.NET](https://www.nuget.org/packages/MuPDF.NET) 3.2.13 or newer + +**Note:** If you see "An assembly with the same simple name 'MuPDF.NET4LLM' has already been imported", the MuPDF.NET package you have includes MuPDF.NET4LLM. Use either MuPDF.NET alone (which has 4LLM bundled) or add only MuPDF.NET4LLM (which brings MuPDF.NET). Do not add both if MuPDF.NET already bundles 4LLM. A future MuPDF.NET release will exclude the bundle so MuPDF.NET4LLM can be used as a separate package without conflict. + +## License + +MuPDF.NET4LLM 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 use, please [contact Artifex](https://artifex.com/contact/mupdf-net). diff --git a/MuPDF.NET4LLM/VersionInfo.cs b/MuPDF.NET4LLM/VersionInfo.cs index fd4eba9..3fc6be1 100644 --- a/MuPDF.NET4LLM/VersionInfo.cs +++ b/MuPDF.NET4LLM/VersionInfo.cs @@ -7,6 +7,6 @@ namespace MuPDF.NET4LLM public static class VersionInfo { public static readonly (int Major, int Minor, int Patch) MinimumMuPDFVersion = (1, 27, 0); - public const string Version = "0.2.9"; + public const string Version = "0.3.4-rc.1"; } } diff --git a/MuPDF.NET4LLM/helpers/DocumentLayout.cs b/MuPDF.NET4LLM/helpers/DocumentLayout.cs index f31d8ba..d9a2099 100644 --- a/MuPDF.NET4LLM/helpers/DocumentLayout.cs +++ b/MuPDF.NET4LLM/helpers/DocumentLayout.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using MuPDF.NET; +using MuPDF.NET4LLM.Ocr; using Newtonsoft.Json; namespace MuPDF.NET4LLM.Helpers diff --git a/MuPDF.NET4LLM/logo.png b/MuPDF.NET4LLM/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..03784e0f76ec2c0827b5b074e50e26e5c5be70d5 GIT binary patch literal 59275 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±%hk4(gv%#^xiTOrdEh!g*e0v_b z$&T(xxFCZS`wbVg-LKB7bLTih(~9K3w#t`G#s1g%lY*r)k7GX#9)6zoDy{oTA`0Fx zKgpZyT%Jv|a5MZ|k!KRl7%N(JME6#ev+>t6IfLsy)!?Bc1gt*`boWcUoB(hqrdjau zw9_vOAJ4`Po{3Dz7_J+e+-1FTaxy`{J9??B{&{v+YoB$Od}+IIfj?*Jrbdz=%GF+K z^%t(>h@nQ^{+k6hIU6=@ozJ%DSN72UA8Uii{!YweN>x6DQp2)UdXiQDL)aTlUSN4{ zdcPX=n3EM$Js)Pv6tw32L(vZG0#gXoUDZh~4+WWd2??ECsx8;@c$8lLd5RO-ds@`C zT6I`+V&r1XaSo^PP+I1}PCCDE~WzFcC#mAD!Z$9b1&vf=#XXm9)Sh@JubK0lhgF#hLO}Iv_RI> z`v@1&_;E;ptw(||iC|fD6g3R}cPuq0`Zr0h3PkxUu_s8EOj>0Ge$*26GAcJ*%uv2{ z3w!73krMpaWf0&hX!EN879M?Xyx_1m1n15ZyK~Q(JM9zJ56eC;qAs9%VoIsAJ^KXR z#)sp;;__n9w;IF|&M>gF4AGm+2OvBw;1B(v23rOLxvI+dwl|tGYiHuO#|aZR#*=WY z*K`A&m6d-EyL43;R=Z_AuU|AdXQ$@bJl~FP*FRDF`Z^M@2yW_$-_J?ycOn|akw*1? zneA3Okz0|`e9h7WS0>bJ34tG%Yp0WQwxwT$_rWKp1=cueQ_gYjiCf(h9mc5(xns*h zUuO||k4X8_Ro(Y!#Dv!T!zlA3+&LXI(EOZL4wzsp-cw0Jp28Bo?3<5X+H4hUD^w1e z5>=nzDml2R1fL>#iOBXgGy=4^#HI>c6Y%+5J1h=6KLt!abLitq7BO2TB z_>*y!6&uj^F%>J0!v(`$T~&yRzU$wmP2{WFG_U$9>p4s9;M-K(;48dkIT5ZbAa{Jk zWN{IwH^P(Eu2!<-5thgQX7JxcoqN1m+eG+4xAwX@u0Dh~p2yxt*#hRjpCgl<67!}B zjGRc!)YXSwjU`pNI8iu?6NcpYg%ZmL=f{;QP{o(G3QM(Rwd;sd@uIHsj$h3r*Fb`M z*CL*Pz0#9tjA;OOe-ua>A9psrhYse&w(?&%a~kgy$_tv1=|1zQTntS*)~3)V$Y&>c zrAN`!p5Fa(a&9esaL0HgB|$hV=b8gw5zvAT*7!OF@v_H7UO%^b+$-?Z4%=hZLH^^? zm)g{une0PCy(1)~gBJ6o-*H^Wrjsu^)va-zQl5kH^*>WgO|XS}L8)625}dF2bH-Q9 z@ceJ%86U;Qj1&EbiI4WTvnI{EuML1hKyN-ay%FiulnaF>dD?7Y2j-cXcm#M2=|!~2 zmw7Z6kr`Dh+$0(%b5s^N6Z{?s(@#OQWc;gWcoZUv8IbKP1!Y`gVu=odip)jw5%HxR za2ngSJ*7ZbGgsJFncSQ^7g>}UX#OHfC z6SRB?9Hv@0od*DYyC_oYsbagwC}gM#3qtM5aF3&eLBaLG_-FO-SMSbGX>J!IcZRfu zMa>S6h*uvJkJH=kz21zoG2*`khbmu^o7^!1d^odyQu7ai`$}&T@=8O} zfuo&?Hi|02lO-TR7JG=IU_$!-h$#*$N2fkSH7OVs^|-&;et(jlZxqfQeDq5Alm9c8 z8O5jJNgwkzJ4FTPn{0H>+cKVsLo;Bu7pNZ3t|zxW?)iO2KVKLK=1F?G)j(9IzD@!< zOzBt+x{`4)rL42lAD+10|$aH+ zW~8z~&)VIgCyhI5KDvIykG{+#iV5+kMY-jwyh*9Hk;8Uh{2H>eg}{RbJ+z{{OZlN* zFJ7Rwoo_!bAk`>kDB9PkCklrLh!7sOs~duMgrj+M7D_c#qoWgN@pU1AAojy*ly>Uy z%$a-&-YJ{zI~vw$UqA-y>A3j`fv$l%&&`2minOH_Syk0wF*GD)BoVi0gaY3Nae$2)UWuZSNIvU`(aRwYVQq z9MZ8Yj@s4JS&;A3#;x4`fKEyaleo*3KT}c^M4Ygicjbo!nO!RB1aXThP>HGJtDHQ2 zG?2J#yAi|I;Bj|yaisrgMtZuf(y@SG09xaTIgK2iX2vZjTXtiL!6qSp{jd6TG=Ocz zCFBQRlZGqObzG<-&r+c6Pf>gPto?IpY{e91yn<9RVa+{Ds0>Ptnw7Md7*q5VJ_Q>W z%Z&%}SkL|b;x_V4*m~>gfduKdk)deKXrN%r?=0jO#O{MxLKKQKOA!>p{s2_wHhcLM!(v)T3+*61Zs^PN)qD29za107{H*R& zuknso9<888f}1wCpB?kn`ROj4xe!`rG*lf@u-%__9n-D6<;#D{qy^MU>&uIDW^kc? zUk7l_1=}wUTn;g?cImr6RmrpOr1XC}i8O846MFf8yQiD%|BdVEmA9Fle4}Tw(t&t5 zgqd$f|56+m-XY_h;wdSehNrjNUhU+sZgnhDTY8VX{#0~HZ>gWl3Y7Rz3{Hm25`YZP zpZj^dzjiw~qg-j0N5P{*A4+ep)uzMiTmV(4jD|6Dz3cmhjsE-BjGd{$8+6c>enCr^ zBqq0K_#w@~zb{R~$KE|B=l2E#;)--X&|+)zNorfmV|^&q27S7Co=kr`?2N3G+s~?^ z?=b|(nO~iaKFi1z0900hPW8V`)7RS5ng69v07DA|AW*=K5~I(6i1L3!jBWmZ$p8PB d{~Ket$9$NAa}F-OwZQ@|Ep + /// Package exports aligned with mupdf4llm.ocr.__init__ (OCRMode). + /// + public enum OcrMode + { + Never = 0, + SelectRemovingOld = 1, + SelectPreservingOld = 2, + AlwaysRemovingOld = 3, + AlwaysPreservingOld = 4, + } +} diff --git a/MuPDF.NET4LLM/ocr/paddleocr_api.cs b/MuPDF.NET4LLM/ocr/paddleocr_api.cs new file mode 100644 index 0000000..669537c --- /dev/null +++ b/MuPDF.NET4LLM/ocr/paddleocr_api.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using MuPDF.NET; + +namespace MuPDF.NET4LLM.Ocr +{ + ///

+ /// Same module contract as mupdf4llm.ocr.paddleocr_api (duplicate of rapidocr_api in upstream). + /// + public static class PaddleOcrApi + { + public const char ReplacementUnicode = '\uFFFD'; + + public static readonly Dictionary Kwargs = new Dictionary(); + + public static bool OcrText(Span span) => TesseractApi.OcrText(span); + + public static void ExecOcr(Page page, int dpi = 300, Pixmap pixmap = null, string language = "eng", bool keepOcrText = false) + { + throw new System.NotImplementedException( + "PaddleOcrApi.ExecOcr is not implemented for MuPDF.NET; see mupdf4llm.ocr.paddleocr_api."); + } + } +} diff --git a/MuPDF.NET4LLM/ocr/paddletess_api.cs b/MuPDF.NET4LLM/ocr/paddletess_api.cs new file mode 100644 index 0000000..14c6f5b --- /dev/null +++ b/MuPDF.NET4LLM/ocr/paddletess_api.cs @@ -0,0 +1,26 @@ +using MuPDF.NET; + +namespace MuPDF.NET4LLM.Ocr +{ + /// + /// Same module contract as mupdf4llm.ocr.paddletess_api (duplicate of rapidtess_api in upstream). + /// + public static class PaddleTessApi + { + public const char ReplacementUnicode = '\uFFFD'; + + public static bool OcrText(Span span) => TesseractApi.OcrText(span); + + public static string GetText(Pixmap pixmap, IRect irect, string language = "eng") + { + throw new System.NotImplementedException( + "PaddleTessApi.GetText requires RapidOCR + Tesseract integration; see mupdf4llm.ocr.paddletess_api."); + } + + public static void ExecOcr(Page page, int dpi = 300, Pixmap pixmap = null, string language = "eng", bool keepOcrText = false) + { + throw new System.NotImplementedException( + "PaddleTessApi.ExecOcr is not implemented for MuPDF.NET; see mupdf4llm.ocr.paddletess_api."); + } + } +} diff --git a/MuPDF.NET4LLM/ocr/rapidocr_api.cs b/MuPDF.NET4LLM/ocr/rapidocr_api.cs new file mode 100644 index 0000000..40a84e5 --- /dev/null +++ b/MuPDF.NET4LLM/ocr/rapidocr_api.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MuPDF.NET; + +namespace MuPDF.NET4LLM.Ocr +{ + /// + /// RapidOCR-only pipeline, aligned with mupdf4llm.mupdf4llm.ocr.rapidocr_api. + /// + public static class RapidOcrApi + { + public const char ReplacementUnicode = '\uFFFD'; + + /// + /// Keyword arguments passed to RapidOCR in Python (KWARGS). + /// + public static readonly Dictionary Kwargs = new Dictionary(); + + public static bool OcrText(Span span) => TesseractApi.OcrText(span); + + /// + /// mupdf4llm.ocr.rapidocr_api.exec_ocr — not ported. + /// + public static void ExecOcr(Page page, int dpi = 300, Pixmap pixmap = null, string language = "eng", bool keepOcrText = false) + { + throw new System.NotImplementedException( + "RapidOcrApi.ExecOcr is not implemented for MuPDF.NET; see mupdf4llm.ocr.rapidocr_api."); + } + } +} diff --git a/MuPDF.NET4LLM/ocr/rapidtess_api.cs b/MuPDF.NET4LLM/ocr/rapidtess_api.cs new file mode 100644 index 0000000..8ff1ea8 --- /dev/null +++ b/MuPDF.NET4LLM/ocr/rapidtess_api.cs @@ -0,0 +1,33 @@ +using MuPDF.NET; + +namespace MuPDF.NET4LLM.Ocr +{ + /// + /// RapidOCR + Tesseract pipeline, aligned with mupdf4llm.ocr.rapidtess_api. + /// Requires rapidocr_onnxruntime in Python; not implemented for .NET. + /// + public static class RapidTessApi + { + public const char ReplacementUnicode = '\uFFFD'; + + public static bool OcrText(Span span) => TesseractApi.OcrText(span); + + /// + /// mupdf4llm.ocr.rapidtess_api.get_text — not ported (Tesseract region OCR + options). + /// + public static string GetText(Pixmap pixmap, IRect irect, string language = "eng") + { + throw new System.NotImplementedException( + "RapidTessApi.GetText requires RapidOCR + Tesseract integration; see mupdf4llm.ocr.rapidtess_api."); + } + + /// + /// mupdf4llm.ocr.rapidtess_api.exec_ocr — not ported. + /// + public static void ExecOcr(Page page, int dpi = 300, Pixmap pixmap = null, string language = "eng", bool keepOcrText = false) + { + throw new System.NotImplementedException( + "RapidTessApi.ExecOcr is not implemented for MuPDF.NET; see mupdf4llm.ocr.rapidtess_api."); + } + } +} diff --git a/MuPDF.NET4LLM/helpers/CheckOcr.cs b/MuPDF.NET4LLM/ocr/tesseract_api.cs similarity index 69% rename from MuPDF.NET4LLM/helpers/CheckOcr.cs rename to MuPDF.NET4LLM/ocr/tesseract_api.cs index 39f8aec..326bacf 100644 --- a/MuPDF.NET4LLM/helpers/CheckOcr.cs +++ b/MuPDF.NET4LLM/ocr/tesseract_api.cs @@ -5,11 +5,39 @@ using mupdf; using Char = MuPDF.NET.Char; -namespace MuPDF.NET4LLM.Helpers +namespace MuPDF.NET4LLM.Ocr { /// - /// OCR decision and repair utilities. - /// Ported and adapted from LLM helpers. + /// Tesseract-oriented OCR API and page/span helpers, aligned with mupdf4llm.ocr.tesseract_api. + /// + public static class TesseractApi + { + public const char ReplacementUnicode = '\uFFFD'; + + /// + /// Mirrors mupdf4llm.ocr.tesseract_api.ocr_text(span). + /// + public static bool OcrText(Span span) + { + int flags = span?.Chars != null && span.Chars.Count > 0 + ? (int)(span.Flags) + : (int)(span?.Flags ?? 0); + return (flags & 32) == 0 && (flags & 16) == 0; + } + + /// + /// Full-page OCR callback from mupdf4llm (redaction + pdfocr_tobytes pipeline). + /// Not ported; use for span repair and OCR decisions. + /// + public static void ExecOcr(Page page, int dpi = 300, Pixmap pixmap = null, string language = "eng", bool keepOcrText = false) + { + throw new NotImplementedException( + "TesseractApi.ExecOcr (mupdf4llm.ocr.tesseract_api.exec_ocr) is not implemented for MuPDF.NET; use CheckOcr for span-level repair."); + } + } + + /// + /// OCR decision and repair utilities used by the layout pipeline (MuPDF.NET / Tesseract). /// public static class CheckOcr { @@ -18,46 +46,32 @@ public static class CheckOcr mupdf.mupdf.FZ_STEXT_COLLECT_VECTORS | (int)TextFlags.TEXT_PRESERVE_IMAGES | (int)TextFlags.TEXT_ACCURATE_BBOXES - // | mupdf.mupdf.FZ_STEXT_MEDIABOX_CLIP ); - /// - /// Return OCR'd span text using Tesseract. - /// - /// MuPDF Page - /// MuPDF Rect or its sequence - /// Resolution for OCR image - /// The OCR-ed text of the bbox. public static string GetSpanOcr(Page page, Rect bbox, int dpi = 300) { - // Step 1: Make a high-resolution image of the bbox. Pixmap pix = page.GetPixmap(dpi: dpi, clip: bbox); byte[] ocrPdfBytes = pix.PdfOCR2Bytes(true); - + Document ocrPdf = new Document("pdf", ocrPdfBytes); Page ocrPage = ocrPdf.LoadPage(0); string text = ocrPage.GetText(); - text = text.Replace("\n", " ").Trim(); // Get rid of line breaks - + text = text.Replace("\n", " ").Trim(); + ocrPage.Dispose(); ocrPdf.Close(); pix.Dispose(); - + return text; } - /// - /// Repair text blocks with missing glyphs using OCR. - /// - /// TODO: Support non-linear block structure. - /// public static List RepairBlocks(List inputBlocks, Page page, int dpi = 300) { List repairedBlocks = new List(); - + foreach (var block in inputBlocks) { - if (block.Type != 0) // Accept non-text blocks as is + if (block.Type != 0) { repairedBlocks.Add(block); continue; @@ -81,7 +95,7 @@ public static List RepairBlocks(List inputBlocks, Page page, int d spanText = span.Text ?? ""; } - if (!spanText.Contains(Utils.REPLACEMENT_CHARACTER)) + if (!spanText.Contains(MuPDF.NET4LLM.Helpers.Utils.REPLACEMENT_CHARACTER)) continue; int spanTextLen = spanText.Length; @@ -91,7 +105,6 @@ public static List RepairBlocks(List inputBlocks, Page page, int d if (span.Chars != null && span.Chars.Count > 0) { - // Rebuild chars array List newChars = new List(); int minLen = Math.Min(newText.Length, span.Chars.Count); for (int i = 0; i < minLen; i++) @@ -102,7 +115,6 @@ public static List RepairBlocks(List inputBlocks, Page page, int d C = newText[i], Origin = oldChar.Origin, Bbox = oldChar.Bbox, - // Copy other properties as needed }; newChars.Add(newChar); } @@ -118,42 +130,25 @@ public static List RepairBlocks(List inputBlocks, Page page, int d } repairedBlocks.Add(block); } - + return repairedBlocks; } - /// - /// Determine whether the page contains text worthwhile to OCR. - /// - /// MuPDF.NET Page object - /// DPI used for rasterization *if* we decide to OCR - /// Area to consider for text presence - /// - /// The full-page transformation matrix, the full-page pixmap and a - /// boolean indicating whether the page is photo-like (True) or - /// text-like (False). - /// public static (Matrix matrix, Pixmap pix, bool photo) GetPageImage( - Page page, - int dpi = 150, + Page page, + int dpi = 150, Rect covered = null) { if (covered == null) covered = page.Rect; - IRect irect = new IRect((int)covered.X0, (int)covered.Y0, - (int)covered.X1, (int)covered.Y1); - - // Make a gray pixmap of the covered area Rect clipRect = new Rect(covered); Pixmap pixCovered = page.GetPixmap(colorSpace: "gray", clip: clipRect); - - // Convert to byte array for image quality analysis (convert to numpy array) + int width = pixCovered.W; int height = pixCovered.H; byte[] samples = pixCovered.SAMPLES; - - // Create 2D array for image quality analysis + byte[,] gray = new byte[height, width]; int sampleIndex = 0; for (int y = 0; y < height; y++) @@ -164,19 +159,17 @@ public static (Matrix matrix, Pixmap pix, bool photo) GetPageImage( } } - // Run photo checks - var scores = ImageQuality.AnalyzeImage(gray); + var scores = MuPDF.NET4LLM.Helpers.ImageQuality.AnalyzeImage(gray); double score = scores.ContainsKey("score") ? scores["score"].value : 0; - + if (score >= 3) { pixCovered.Dispose(); - return (new Matrix(1, 0, 0, 1, 0, 0), null, true); // Identity matrix + return (new Matrix(1, 0, 0, 1, 0, 0), null, true); } else { Pixmap pix = page.GetPixmap(dpi: dpi); - IRect pixRect = new IRect(0, 0, pix.W, pix.H); Matrix matrix = new Matrix( page.Rect.Width / pix.W, 0, @@ -190,16 +183,6 @@ public static (Matrix matrix, Pixmap pix, bool photo) GetPageImage( } } - /// - /// Decide whether a MuPDF.NET page should be OCR'd. - /// - /// MuPDF.NET page object - /// DPI used for rasterization - /// Minimum number of vector paths to suggest glyph simulation - /// Fraction of page area covered by images to trigger OCR - /// Fraction of readable characters to skip OCR - /// Output of page.get_text("dict") if already available - /// Dictionary with decision and diagnostic flags public static Dictionary ShouldOcrPage( Page page, int dpi = 150, @@ -216,25 +199,19 @@ public static Dictionary ShouldOcrPage( ["readable_text"] = false, ["image_covers_page"] = false, ["has_vector_chars"] = false, - ["transform"] = new Matrix(1, 0, 0, 1, 0, 0), // Identity matrix + ["transform"] = new Matrix(1, 0, 0, 1, 0, 0), ["pixmap"] = null, }; - Rect pageRect = page.Rect; - float pageArea = Math.Abs(pageRect.Width * pageRect.Height); - - // Analyze the page - var analysis = Utils.AnalyzePage(page, blocks); + var analysis = MuPDF.NET4LLM.Helpers.Utils.AnalyzePage(page, blocks); - // Return if page is completely blank Rect covered = analysis["covered"] as Rect; - if (Utils.BboxIsEmpty(covered)) + if (MuPDF.NET4LLM.Helpers.Utils.BboxIsEmpty(covered)) { decision["should_ocr"] = false; return decision; } - // Return if page has been OCR'd already int ocrSpans = (int)analysis["ocr_spans"]; if (ocrSpans > 0) { @@ -250,8 +227,6 @@ public static Dictionary ShouldOcrPage( float imgArea = (float)analysis["img_area"]; int charsBad = (int)analysis["chars_bad"]; - // Preset OCR if very little text area exists - // Less than 5% text area in covered area if (txtArea < 0.05f && charsTotal < 200 && txtJoins < 0.3f) { if (vecArea >= vectorThresh) @@ -287,15 +262,12 @@ public static Dictionary ShouldOcrPage( if (!(bool)decision["readable_text"] && (bool)decision["has_text"]) return decision; - // We need OCR and do a final check for potential text presence if (!(bool)decision["has_text"]) { - // Rasterize and check for photo versus text-heaviness var (matrix, pix, photo) = GetPageImage(page, dpi, covered); if (photo) { - // This seems to be a non-text picture page decision["should_ocr"] = false; decision["pixmap"] = null; }