diff --git a/Demo/App.config b/Demo/App.config
deleted file mode 100644
index 5b023ab1..00000000
--- a/Demo/App.config
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj
index af7f04ee..d42373e5 100644
--- a/Demo/Demo.csproj
+++ b/Demo/Demo.csproj
@@ -1,14 +1,15 @@
-
Exenet8.0
- AnyCPU;x64;x86
+ Demo
+ Demo
+ enable
+ enable
-
diff --git a/Demo/GlobalUsings.cs b/Demo/GlobalUsings.cs
deleted file mode 100644
index 83493d33..00000000
--- a/Demo/GlobalUsings.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-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 PDF4LLM;
-global using static global::PDF4LLM.PdfExtractor;
-global using PDF4LLM.Helpers;
-global using PDF4LLM.Llama;
-global using PDF4LLM.Ocr;
-global using SkiaSharp;
-global using Box = MuPDF.NET.Box;
-global using Encoding = System.Text.Encoding;
-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 c5d29c0a..4cad628f 100644
--- a/Demo/Program.cs
+++ b/Demo/Program.cs
@@ -1,15 +1,689 @@
-using System.Threading.Tasks;
+// Demo - CLI tool ported from PyMuPDF's __main__.py
+// Provides: show, clean, join, extract, gettext, embed-info/add/del/extract/copy commands.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MuPDF.NET;
namespace Demo
{
- ///
- /// GitHub samples entry point. With no arguments, all samples run; see .
- ///
- internal partial class Program
+ internal static class Program
{
- private static void Main(string[] args)
+ static void TestAnnot()
+ {
+ Document doc = new Document();
+ for (int i = 0; i < 1; i++)
+ doc.NewPage();
+
+ Page page = doc[0];
+ var annot = page.AddCaretAnnot(new Point(100, 100));
+ annot.Update(rotate: 20);
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ doc.Save1(@"E:\Pdf\Tmp\Test\Annot_AddCaretAnnot.pdf", annot);
+ //doc.Save1(@"E:\Pdf\Tmp\Test\Annot_AddCaretAnnot.pdf", annot);
+ //annot.GetApnMatrix();
+
+ doc.Close();
+ }
+ static int Main(string[] args)
+ {
+ TestAnnot();
+ return 0;
+ /*
+ if (args.Length == 0)
+ {
+ PrintHelp();
+ return 0;
+ }
+ */
+ string command = "show"; //args[0].ToLower();
+
+ Console.WriteLine("\n========================================= Command: show =========================================\n");
+ Console.WriteLine("Usage: show [-password PW] [-catalog] [-trailer] [-metadata] [-xrefs 1,5-7] [-pages 1,5-7]");
+ string[] cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\test.pdf", "-catalog", "-trailer", "-metadata-catalog", "-trailer", "-metadata" }; //args.Skip(1).ToArray();
+ CommandShow(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: clean =========================================\n");
+ Console.WriteLine("Usage: clean [-password PW] [-pages 1,5-7] [-garbage 0-4] [-compress] [-linear] [-sanitize] [-pretty] [-ascii]");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\test.pdf", @"E:\Pdf\Tmp\6025432\2_.pdf", "-pages 1,5", "-compress", "-linear", "-sanitize", "-pretty", "-ascii" };
+ CommandClean(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: join =========================================\n");
+ Console.WriteLine("Usage: join [input2.pdf ...] -output ");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\test.pdf", @"E:\Pdf\Tmp\6025432\2_.pdf", "-output", @"E:\Pdf\Tmp\6025432\joined.pdf" };
+ CommandJoin(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: extract =========================================\n");
+ Console.WriteLine("Usage: extract [-images] [-fonts] [-output DIR] [-password PW] [-pages 1,5-7]");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\magazine.pdf", "-images", "-fonts", "-output", @"E:\Pdf\Tmp\6025432\extracted" };
+ CommandExtract(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: gettext =========================================\n");
+ Console.WriteLine("Usage: gettext [-password PW] [-mode simple|blocks|layout] [-pages 1,5-7,N] [-output FILE] [-grid N] [-fontsize N] [-noformfeed] [-skip-empty]");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\2.pdf", "-mode simple", "-pages 1", "-output", @"E:\Pdf\Tmp\6025432\output.txt", "-grid 4", "-fontsize 12", "-noformfeed", "-skip-empty" };
+ CommandGetText(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: embed-add =========================================\n");
+ Console.WriteLine("Usage: embed-add -name NAME -path FILE [-desc TEXT] [-output OUT.pdf] [-password PW]");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\2.pdf", "-name", "My_PDF", "-path", @"E:\Pdf\Tmp\6025432\image.png", "-desc Cover_Image", "-output", @"E:\Pdf\Tmp\6025432\output.pdf" };
+ CommandEmbedAdd(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: embed-copy =========================================\n");
+ Console.WriteLine("Usage: embed-copy -source [-name NAME ...] [-output OUT.pdf] [-password PW] [-pwdsource PW]");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\test.pdf", "-source", @"E:\Pdf\Tmp\6025432\output.pdf", "-name", "My_PDF", "-output", @"E:\Pdf\Tmp\6025432\output3.pdf" };
+ CommandEmbedCopy(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: embed-info =========================================\n");
+ Console.WriteLine("Usage: embed-info [-name NAME] [-detail] [-password PW]");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\output.pdf", "-name", "My_PDF", "-detail" };
+ CommandEmbedInfo(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: embed-extract =========================================\n");
+ Console.WriteLine("Usage: embed-extract -name NAME [-output FILE] [-password PW]");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\output.pdf", "-name", "My_PDF", "-output", @"E:\Pdf\Tmp\6025432\output1.png" };
+ CommandEmbedExtract(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: embed-del =========================================\n");
+ Console.WriteLine("Usage: embed-del -name NAME [-output OUT.pdf] [-password PW]");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\output.pdf", "-name", "My_PDF", "-output", @"E:\Pdf\Tmp\6025432\output.pdf" };
+ CommandEmbedDel(cmdArgs);
+
+ Console.WriteLine("\n========================================= Command: embed-info =========================================\n");
+ Console.WriteLine("Usage: embed-info [-name NAME] [-detail] [-password PW]");
+ cmdArgs = new[] { @"E:\Pdf\Tmp\6025432\output.pdf", "-name", "My_PDF", "-detail" };
+ CommandEmbedInfo(cmdArgs);
+
+ return 0;
+
+ try
+ {
+ return command switch
+ {
+ "show" => CommandShow(cmdArgs),
+ "clean" => CommandClean(cmdArgs),
+ "join" => CommandJoin(cmdArgs),
+ "extract" => CommandExtract(cmdArgs),
+ "gettext" => CommandGetText(cmdArgs),
+ "embed-info" => CommandEmbedInfo(cmdArgs),
+ "embed-add" => CommandEmbedAdd(cmdArgs),
+ "embed-del" => CommandEmbedDel(cmdArgs),
+ "embed-extract" => CommandEmbedExtract(cmdArgs),
+ "embed-copy" => CommandEmbedCopy(cmdArgs),
+ "-h" or "--help" or "help" => PrintHelp(),
+ _ => Error($"Unknown command: '{command}'. Use --help for usage.")
+ };
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ // ─── Helpers ────────────────────────────────────────────────────
+
+ static string Center(string text, int width = 75, char pad = '-')
+ {
+ string s = $" {text} ";
+ int total = Math.Max(width, s.Length);
+ int left = (total - s.Length) / 2;
+ int right = total - s.Length - left;
+ return new string(pad, left) + s + new string(pad, right);
+ }
+
+ static int Error(string message)
+ {
+ Console.Error.WriteLine(message);
+ return 1;
+ }
+
+ static int PrintHelp()
+ {
+ Console.WriteLine(Center("MuPDF.NET Demo CLI"));
+ Console.WriteLine();
+ Console.WriteLine("Usage: Demo [options]");
+ Console.WriteLine();
+ Console.WriteLine("Commands:");
+ Console.WriteLine(" show Display PDF document information");
+ Console.WriteLine(" clean Optimize PDF or create sub-PDF");
+ Console.WriteLine(" join Join pages from multiple PDFs");
+ Console.WriteLine(" extract Extract images and/or fonts to disk");
+ Console.WriteLine(" gettext Extract text in various formatting modes");
+ Console.WriteLine(" embed-info List embedded files");
+ Console.WriteLine(" embed-add Add an embedded file");
+ Console.WriteLine(" embed-del Delete an embedded file");
+ Console.WriteLine(" embed-extract Extract an embedded file to disk");
+ Console.WriteLine(" embed-copy Copy embedded files between PDFs");
+ Console.WriteLine();
+ Console.WriteLine("Use ' --help' for command-specific help.");
+ return 0;
+ }
+
+ static Document OpenFile(string filename, string? password = null, bool requirePdf = true)
+ {
+ var doc = new Document(filename);
+ if (!doc.IsPdf && requirePdf)
+ throw new InvalidOperationException("This command supports PDF files only.");
+ if (doc.NeedsPass)
+ {
+ if (string.IsNullOrEmpty(password))
+ throw new InvalidOperationException($"'{doc.Name}' requires a password.");
+ if (!doc.Authenticate(password))
+ throw new InvalidOperationException("Authentication unsuccessful.");
+ }
+ return doc;
+ }
+
+ static List ParsePageList(string spec, int pageCount)
+ {
+ int limit = pageCount + 1;
+ string resolved = spec.Replace("N", (pageCount).ToString()).Replace(" ", "");
+ var result = new List();
+ foreach (string item in resolved.Split(',', StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (int.TryParse(item, out int single))
+ {
+ if (single >= 1 && single < limit)
+ result.Add(single);
+ else
+ throw new ArgumentException($"Bad page specification: {item}");
+ }
+ else if (item.Contains('-'))
+ {
+ var parts = item.Split('-', 2);
+ int i1 = int.Parse(parts[0]);
+ int i2 = int.Parse(parts[1]);
+ if (i1 < 1 || i1 >= limit || i2 < 1 || i2 >= limit)
+ throw new ArgumentException($"Bad page range: {item}");
+ if (i1 <= i2)
+ for (int i = i1; i <= i2; i++) result.Add(i);
+ else
+ for (int i = i1; i >= i2; i--) result.Add(i);
+ }
+ else
+ throw new ArgumentException($"Bad specification: {item}");
+ }
+ return result;
+ }
+
+ static (Dictionary named, List positional) ParseArgs(
+ string[] args, params string[] flags)
+ {
+ var named = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var positional = new List();
+ var flagSet = new HashSet(flags, StringComparer.OrdinalIgnoreCase);
+
+ for (int i = 0; i < args.Length; i++)
+ {
+ if (args[i].StartsWith("-"))
+ {
+ string key = args[i].TrimStart('-').ToLower();
+ if (flagSet.Contains(key))
+ {
+ named[key] = "true";
+ }
+ else if (i + 1 < args.Length && !args[i + 1].StartsWith("-"))
+ {
+ named[key] = args[++i];
+ }
+ else
+ {
+ named[key] = "true";
+ }
+ }
+ else
+ {
+ positional.Add(args[i]);
+ }
+ }
+ return (named, positional);
+ }
+
+ static string? Get(Dictionary d, string key, string? def = null)
+ => d.TryGetValue(key, out var v) ? v : def;
+
+ static bool Flag(Dictionary d, string key)
+ => d.ContainsKey(key);
+
+ // ─── show ───────────────────────────────────────────────────────
+
+ static int CommandShow(string[] args)
+ {
+ var (opts, pos) = ParseArgs(args, "catalog", "trailer", "metadata", "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count == 0)
+ {
+ Console.WriteLine("Usage: show [-password PW] [-catalog] [-trailer] [-metadata] [-xrefs 1,5-7] [-pages 1,5-7]");
+ return 0;
+ }
+
+ string input = pos[0];
+ string? password = Get(opts, "password");
+ using var doc = OpenFile(input, password, true);
+
+ double size = new FileInfo(input).Length / 1024.0;
+ string flag = "KB";
+ if (size > 1000) { size /= 1024; flag = "MB"; }
+
+ var meta = doc.GetMetadata();
+ Console.WriteLine($"'{input}', pages: {doc.PageCount}, objects: {doc.XrefLength - 1}, {size:F1} {flag}, {meta.GetValueOrDefault("format", "")}, encryption: {meta.GetValueOrDefault("encryption", "None")}");
+
+ if (Flag(opts, "metadata"))
+ {
+ Console.WriteLine(Center("PDF metadata"));
+ foreach (var kv in doc.GetMetadata())
+ Console.WriteLine($" {kv.Key}: {kv.Value}");
+ Console.WriteLine();
+ }
+
+ if (Flag(opts, "catalog"))
+ {
+ Console.WriteLine(Center("PDF catalog"));
+ int xref = doc.PdfCatalog;
+ PrintXref(doc, xref);
+ Console.WriteLine();
+ }
+
+ if (Flag(opts, "trailer"))
+ {
+ Console.WriteLine(Center("PDF trailer"));
+ Console.WriteLine(doc.PdfTrailer());
+ Console.WriteLine();
+ }
+
+ if (opts.ContainsKey("xrefs"))
+ {
+ Console.WriteLine(Center("object information"));
+ var xrefs = ParsePageList(opts["xrefs"]!, doc.XrefLength);
+ foreach (int xref in xrefs)
+ {
+ PrintXref(doc, xref);
+ Console.WriteLine();
+ }
+ }
+
+ if (opts.ContainsKey("pages"))
+ {
+ Console.WriteLine(Center("page information"));
+ var pages = ParsePageList(opts["pages"]!, doc.PageCount);
+ foreach (int pno in pages)
+ {
+ int xref = doc.PageXref(pno - 1);
+ Console.WriteLine($"Page {pno}:");
+ PrintXref(doc, xref);
+ Console.WriteLine();
+ }
+ }
+
+ return 0;
+ }
+
+ static void PrintXref(Document doc, int xref)
+ {
+ Console.WriteLine($"{xref} 0 obj");
+ var keys = doc.XrefGetKeys(xref);
+ string xrefStr = keys.Count > 0 ? string.Join("\n", keys.Select(k => $" /{k} {doc.XrefGetKey(xref, k).value}")) : "";
+ Console.WriteLine(xrefStr);
+ if (doc.XrefIsStream(xref))
+ Console.WriteLine("stream\n...bytes\nendstream");
+ Console.WriteLine("endobj");
+ }
+
+ // ─── clean ──────────────────────────────────────────────────────
+
+ static int CommandClean(string[] args)
+ {
+ var (opts, pos) = ParseArgs(args, "compress", "ascii", "linear", "sanitize", "pretty", "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count < 2)
+ {
+ Console.WriteLine("Usage: clean [-password PW] [-pages 1,5-7] [-garbage 0-4] [-compress] [-linear] [-sanitize] [-pretty] [-ascii]");
+ return 0;
+ }
+
+ string input = pos[0], output = pos[1];
+ string? password = Get(opts, "password");
+ using var doc = OpenFile(input, password, true);
+
+ int garbage = int.Parse(Get(opts, "garbage", "0")!);
+ bool compress = Flag(opts, "compress");
+ bool clean = Flag(opts, "sanitize");
+ bool linear = Flag(opts, "linear");
+
+ string? pagesSpec = Get(opts, "pages");
+ if (pagesSpec == null)
+ {
+ doc.Save(output, garbage: garbage>0?1:0, clean: clean?1:0, deflate: compress?1:0);
+ }
+ else
+ {
+ var pages = ParsePageList(pagesSpec, doc.PageCount);
+ using var outDoc = new Document();
+ foreach (int pno in pages)
+ outDoc.InsertPdf(doc, fromPage: pno - 1, toPage: pno - 1);
+ outDoc.Save(output, garbage: garbage > 0 ? 1 : 0, clean: clean ? 1 : 0, deflate: compress ? 1 : 0);
+ }
+
+ Console.WriteLine($"Saved to '{output}'");
+ return 0;
+ }
+
+ // ─── join ───────────────────────────────────────────────────────
+
+ static int CommandJoin(string[] args)
+ {
+ var (opts, pos) = ParseArgs(args, "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count == 0 || !opts.ContainsKey("output"))
+ {
+ Console.WriteLine("Usage: join [input2.pdf ...] -output ");
+ return 0;
+ }
+
+ string output = opts["output"]!;
+ using var outDoc = new Document();
+
+ foreach (string srcItem in pos)
+ {
+ var parts = srcItem.Split(',');
+ string filename = parts[0];
+ string? password = parts.Length > 1 ? parts[1] : null;
+ using var src = OpenFile(filename, password, true);
+
+ List pageList;
+ if (parts.Length > 2)
+ pageList = ParsePageList(string.Join(",", parts.Skip(2)), src.PageCount);
+ else
+ pageList = Enumerable.Range(1, src.PageCount).ToList();
+
+ foreach (int pno in pageList)
+ outDoc.InsertPdf(src, fromPage: pno - 1, toPage: pno - 1);
+ }
+
+ outDoc.Save(output, garbage: 1, deflate: 1);
+ Console.WriteLine($"Joined to '{output}'");
+ return 0;
+ }
+
+ // ─── extract ────────────────────────────────────────────────────
+
+ static int CommandExtract(string[] args)
+ {
+ var (opts, pos) = ParseArgs(args, "images", "fonts", "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count == 0)
+ {
+ Console.WriteLine("Usage: extract [-images] [-fonts] [-output DIR] [-password PW] [-pages 1,5-7]");
+ return 0;
+ }
+
+ bool doImages = Flag(opts, "images");
+ bool doFonts = Flag(opts, "fonts");
+ if (!doImages && !doFonts)
+ return Error("Neither -images nor -fonts requested.");
+
+ string input = pos[0];
+ string? password = Get(opts, "password");
+ using var doc = OpenFile(input, password, true);
+
+ string outDir = Get(opts, "output") ?? Directory.GetCurrentDirectory();
+ if (!Directory.Exists(outDir))
+ return Error($"Output directory '{outDir}' does not exist.");
+
+ var pagesSpec = Get(opts, "pages");
+ var pages = pagesSpec != null
+ ? ParsePageList(pagesSpec, doc.PageCount)
+ : Enumerable.Range(1, doc.PageCount).ToList();
+
+ var fontXrefs = new HashSet();
+ var imageXrefs = new HashSet();
+
+ foreach (int pno in pages)
+ {
+ if (doFonts)
+ {
+ var fonts = doc.GetPageFonts(pno - 1);
+ foreach (var item in fonts)
+ {
+ int xref = item.Item1;
+ if (fontXrefs.Add(xref))
+ {
+ var (fontname, ext, _, buffer) = doc.ExtractFont(xref);
+ if (ext == "n/a" || buffer == null || buffer.Length == 0) continue;
+ string outname = Path.Combine(outDir, $"{fontname.Replace(' ', '-')}-{xref}.{ext}");
+ File.WriteAllBytes(outname, buffer);
+ }
+ }
+ }
+
+ if (doImages)
+ {
+ var images = doc.GetPageImages(pno - 1);
+ foreach (var item in images)
+ {
+ int xref = item.Item1;
+ if (imageXrefs.Add(xref))
+ {
+ var imgData = doc.ExtractImage(xref);
+ if (imgData != null && imgData.ContainsKey("image"))
+ {
+ string ext = imgData.ContainsKey("ext") ? imgData["ext"].ToString()! : "png";
+ string outname = Path.Combine(outDir, $"img-{xref}.{ext}");
+ File.WriteAllBytes(outname, (byte[])imgData["image"]);
+ }
+ }
+ }
+ }
+ }
+
+ if (doFonts) Console.WriteLine($"Saved {fontXrefs.Count} fonts to '{outDir}'");
+ if (doImages) Console.WriteLine($"Saved {imageXrefs.Count} images to '{outDir}'");
+ return 0;
+ }
+
+ // ─── gettext ────────────────────────────────────────────────────
+
+ static int CommandGetText(string[] args)
+ {
+ var (opts, pos) = ParseArgs(args, "noligatures", "convert-white", "extra-spaces",
+ "noformfeed", "skip-empty", "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count == 0)
+ {
+ Console.WriteLine("Usage: gettext [-password PW] [-mode simple|blocks|layout] [-pages 1,5-7,N] [-output FILE] [-grid N] [-fontsize N] [-noformfeed] [-skip-empty]");
+ return 0;
+ }
+
+ string input = pos[0];
+ string? password = Get(opts, "password");
+ using var doc = OpenFile(input, password, requirePdf: false);
+
+ string pagesSpec = Get(opts, "pages", "1-N")!;
+ var pages = ParsePageList(pagesSpec, doc.PageCount);
+ string mode = Get(opts, "mode", "layout")!;
+
+ string? outputFile = Get(opts, "output");
+ if (outputFile == null)
+ outputFile = Path.ChangeExtension(input, ".txt");
+
+ using var outStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write);
+ byte eop = Flag(opts, "noformfeed") ? (byte)'\n' : (byte)12;
+ bool skipEmpty = Flag(opts, "skip-empty");
+
+ foreach (int pno in pages)
+ {
+ var page = doc[pno - 1];
+ string text;
+ if (mode == "blocks")
+ {
+ var blocks = page.GetTextBlocks();
+ if (blocks == null || blocks.Count == 0)
+ {
+ if (!skipEmpty) outStream.WriteByte(eop);
+ continue;
+ }
+ text = string.Join("", blocks.Select(b => b.Item5));
+ }
+ else
+ {
+ text = page.GetText("text");
+ }
+
+ if (string.IsNullOrEmpty(text))
+ {
+ if (!skipEmpty) outStream.WriteByte(eop);
+ continue;
+ }
+ var bytes = System.Text.Encoding.UTF8.GetBytes(text);
+ outStream.Write(bytes, 0, bytes.Length);
+ outStream.WriteByte(eop);
+ }
+
+ Console.WriteLine($"Text saved to '{outputFile}'");
+ return 0;
+ }
+
+ // ─── embed-info ─────────────────────────────────────────────────
+
+ static int CommandEmbedInfo(string[] args)
{
- SampleMenu.Run(args);
+ var (opts, pos) = ParseArgs(args, "detail", "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count == 0)
+ {
+ Console.WriteLine("Usage: embed-info [-name NAME] [-detail] [-password PW]");
+ return 0;
+ }
+
+ using var doc = OpenFile(pos[0], Get(opts, "password"), true);
+ var names = doc.EmbfileNames();
+ if (names.Count == 0)
+ {
+ Console.WriteLine($"'{doc.Name}' contains no embedded files.");
+ return 0;
+ }
+
+ string? filterName = Get(opts, "name");
+ if (filterName != null)
+ {
+ if (!names.Contains(filterName))
+ return Error($"No such embedded file '{filterName}'");
+ Console.WriteLine($"Printing 1 of {names.Count} embedded file(s):");
+ Console.WriteLine($" {filterName}");
+ return 0;
+ }
+
+ Console.WriteLine($"'{doc.Name}' contains {names.Count} embedded file(s):");
+ foreach (var name in names)
+ Console.WriteLine($" {name}");
+ return 0;
+ }
+
+ // ─── embed-add ──────────────────────────────────────────────────
+
+ static int CommandEmbedAdd(string[] args)
+ {
+ var (opts, pos) = ParseArgs(args, "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count == 0
+ || !opts.ContainsKey("name") || !opts.ContainsKey("path"))
+ {
+ Console.WriteLine("Usage: embed-add -name NAME -path FILE [-desc TEXT] [-output OUT.pdf] [-password PW]");
+ return 0;
+ }
+
+ string input = pos[0];
+ using var doc = OpenFile(input, Get(opts, "password"), true);
+ string name = opts["name"]!;
+ string path = opts["path"]!;
+ if (!File.Exists(path))
+ return Error($"No such file '{path}'");
+
+ byte[] data = File.ReadAllBytes(path);
+ string desc = Get(opts, "desc", path)!;
+ doc.EmbfileAdd(name, data, filename: path, desc: desc);
+
+ string? output = Get(opts, "output");
+ if (output != null && output != input)
+ doc.Save(output);
+ else
+ doc.SaveIncr();
+
+ Console.WriteLine($"Added embedded file '{name}'");
+ return 0;
+ }
+
+ // ─── embed-del ──────────────────────────────────────────────────
+
+ static int CommandEmbedDel(string[] args)
+ {
+ var (opts, pos) = ParseArgs(args, "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count == 0 || !opts.ContainsKey("name"))
+ {
+ Console.WriteLine("Usage: embed-del -name NAME [-output OUT.pdf] [-password PW]");
+ return 0;
+ }
+
+ using var doc = OpenFile(pos[0], Get(opts, "password"), true);
+ doc.EmbfileDel(opts["name"]!);
+
+ string? output = Get(opts, "output");
+ if (output != null && output != pos[0])
+ doc.Save(output, garbage: 1);
+ else
+ doc.SaveIncr();
+
+ Console.WriteLine($"Deleted embedded file '{opts["name"]}'");
+ return 0;
+ }
+
+ // ─── embed-extract ──────────────────────────────────────────────
+
+ static int CommandEmbedExtract(string[] args)
+ {
+ var (opts, pos) = ParseArgs(args, "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count == 0 || !opts.ContainsKey("name"))
+ {
+ Console.WriteLine("Usage: embed-extract -name NAME [-output FILE] [-password PW]");
+ return 0;
+ }
+
+ using var doc = OpenFile(pos[0], Get(opts, "password"), true);
+ string name = opts["name"]!;
+ byte[] data = doc.EmbfileGet(name);
+ string? outputFile = Get(opts, "output") ?? name;
+ File.WriteAllBytes(outputFile, data);
+ Console.WriteLine($"Saved embedded file '{name}' as '{outputFile}'");
+ return 0;
+ }
+
+ // ─── embed-copy ─────────────────────────────────────────────────
+
+ static int CommandEmbedCopy(string[] args)
+ {
+ var (opts, pos) = ParseArgs(args, "help", "h");
+ if (Flag(opts, "help") || Flag(opts, "h") || pos.Count == 0 || !opts.ContainsKey("source"))
+ {
+ Console.WriteLine("Usage: embed-copy -source [-name NAME ...] [-output OUT.pdf] [-password PW] [-pwdsource PW]");
+ return 0;
+ }
+
+ using var doc = OpenFile(pos[0], Get(opts, "password"), true);
+ using var src = OpenFile(opts["source"]!, Get(opts, "pwdsource"), true);
+
+ var srcNames = new HashSet(src.EmbfileNames());
+ var filterName = Get(opts, "name");
+ var names = filterName != null ? new HashSet { filterName } : srcNames;
+
+ foreach (var name in names)
+ {
+ byte[] data = src.EmbfileGet(name);
+ doc.EmbfileAdd(name, data);
+ Console.WriteLine($"Copied entry '{name}'");
+ }
+
+ string? output = Get(opts, "output");
+ if (output != null && output != pos[0])
+ doc.Save(output, garbage: 1);
+ else
+ doc.SaveIncr();
+
+ return 0;
}
}
}
diff --git a/Demo/SampleMenu.cs b/Demo/SampleMenu.cs
deleted file mode 100644
index 3a4b1af4..00000000
--- a/Demo/SampleMenu.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-namespace Demo
-{
- ///
- /// Demo samples grouped by MuPDF.NET / PDF4LLM 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; PDF4LLM 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()),
-
- // —— PDF4LLM —— Samples/Llm
- new("PDF4LLM", "rag-markdown", "Legacy RAG: PDF4LLM.ToMarkdown with UseLayout=false (Magazine.pdf)", _ => Program.TestMuPdfRagToMarkdown()),
- new("PDF4LLM", "table", "Detect tables and export markdown", _ => Program.TestTable()),
- new("PDF4LLM", "table-extract-1", "Dump detected tables by page to console", _ => Program.TestTableExtract1()),
- new("PDF4LLM", "table-extract-2", "Export detected tables to tables.csv", _ => Program.TestTableExtract2()),
- new("PDF4LLM", "table-extract-3", "Merge continued table pages by column count", _ => Program.TestTableExtract3()),
- new("PDF4LLM", "table-ocr", "Extract OCR text from Ocr.pdf", _ => Program.TestOcr()),
- new("PDF4LLM", "llm-reader-save-pages", "Load markdown chunks and save per-page .md files", _ => Program.TestLLM2()),
- new("PDF4LLM", "markdown-reader", "LlamaIndex PDFMarkdownReader", _ => Program.TestMarkdownReader()),
- new("PDF4LLM", "llm-to-markdown-fixture-370", "ToMarkdown vs tests/test_370_expected.md (needs tests/test_370.pdf)", a => Program.Test4LlmToMarkdownCompareExpected370(a)),
- new("PDF4LLM", "llm-to-markdown-ocr-1", "ToMarkdown + U+FFFD fixture (tests/test_ocr_loremipsum_FFFD.pdf)", a => Program.Test4LlmToMarkdownOcrFixture1(a)),
- new("PDF4LLM", "llm-to-markdown-ocr-2", "ToMarkdown useOcr=false on FFFD fixture", a => Program.Test4LlmToMarkdownOcrFixture2(a)),
- new("PDF4LLM", "llm-to-markdown-ocr-3", "ToMarkdown OCR on/off on SVG fixture", a => Program.Test4LlmToMarkdownOcrFixture3(a)),
- new("PDF4LLM", "llm-pdf-reader-empty", "PDFMarkdownReader: new PDF, one blank page", a => Program.Test4LlmPdfMarkdownReaderEmptyPage(a)),
- new("PDF4LLM", "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", "pixmap-parallel", "Repro: parallel Pixmap.ToBytes rendering", _ => Program.TestPixmapParallel()),
- 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/Samples/Annotations/NewAnnots.cs b/Demo/Samples/Annotations/NewAnnots.cs
deleted file mode 100644
index c0cfcd66..00000000
--- a/Demo/Samples/Annotations/NewAnnots.cs
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * -------------------------------------------------------------------------------
- * Demo script showing how annotations can be added to a PDF using MuPDF.NET.
- *
- * It contains the following annotation types:
- * Caret, Text, FreeText, text markers (underline, strike-out, highlight,
- * squiggle), Circle, Square, Line, PolyLine, Polygon, FileAttachment, Stamp
- * and Redaction.
- * There is some effort to vary appearances by adding colors, line ends,
- * opacity, rotation, dashed lines, etc.
- *
--------------------------------------------------------------------------------
-*/
-
-using MuPDF.NET;
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Demo
-{
- public static class NewAnnots
- {
- private static void print_descr(Annot annot)
- {
- // Print a short description to the right of each annot rect.
- string description = annot.Type.Item2 + " annotation";
- annot.GetParent().InsertText(
- annot.Rect.BottomRight + new Point(10, -5), description, color: Constants.red);
- }
-
- // Use rich text for FreeText annotations
- public static void Run(string[] args)
- {
- Console.WriteLine("\n=== NewAnnots =======================");
- Rect r = Constants.r; // use the rectangle defined in Constants.cs
-
- Document doc = new Document();
- Page page = doc.NewPage();
-
- page.SetRotation(0); // no rotation
-
- Annot annot = page.AddCaretAnnot(r.TopLeft);
- print_descr(annot);
-
- r = r + Constants.displ;
- annot = page.AddFreeTextAnnot(
- r,
- Constants.t1,
- fontSize: 10,
- rotate: 90,
- textColor: Constants.blue,
- fillColor: Constants.gold,
- align: (int)TextAlign.TEXT_ALIGN_CENTER
- );
- annot.SetBorder(width: 0.3f, dashes: new int[] { 2 });
- annot.Update(textColor: Constants.blue, fillColor: Constants.gold);
- print_descr(annot);
-
- r = r + Constants.displ;
- annot = page.AddTextAnnot(
- r.TopLeft,
- Constants.t1
- );
- print_descr(annot);
-
- // Adding text marker annotations:
- // first insert a unique text, then search for it, then mark it
- Point pos = annot.Rect.TopLeft + Constants.displ.TopLeft;
- page.InsertText(
- pos, // insertion point
- Constants.highlight, // inserted text
- morph: new Morph(pos, new Matrix(-5)) // rotate around insertion point
- );
- List rl = page.SearchFor(Constants.highlight, quads: true); // need a quad b/o tilted text
- annot = page.AddHighlightAnnot(rl[0]);
- print_descr(annot);
-
- pos = annot.Rect.BottomLeft; // next insertion point
- page.InsertText(pos, Constants.underline, morph: new Morph(pos, new Matrix(-10)));
- rl = page.SearchFor(Constants.underline, quads: true);
- annot = page.AddUnderlineAnnot(rl[0]);
- print_descr(annot);
-
- pos = annot.Rect.BottomLeft;
- page.InsertText(pos, Constants.strikeout, morph: new Morph(pos, new Matrix(-15)));
- rl = page.SearchFor(Constants.strikeout, quads: true);
- annot = page.AddStrikeoutAnnot(rl[0]);
- print_descr(annot);
-
- pos = annot.Rect.BottomLeft;
- page.InsertText(pos, Constants.squiggled, morph: new Morph(pos, new Matrix(-20)));
- rl = page.SearchFor(Constants.squiggled, quads: true);
- annot = page.AddSquigglyAnnot(rl[0]);
- print_descr(annot);
-
- pos = annot.Rect.BottomLeft;
- r = new Rect(pos, pos.X + 75, pos.Y + 35) + new Rect(0, 20, 0, 20);
- annot = page.AddPolylineAnnot(new List { r.BottomLeft, r.TopRight, r.BottomRight, r.TopLeft }); // 'Polyline'
- annot.SetBorder(width: 0.3f, dashes: new int[] { 2 });
- annot.SetColors(stroke: Constants.blue, fill: Constants.green);
- annot.SetLineEnds(PdfLineEnding.PDF_ANNOT_LE_CLOSED_ARROW, PdfLineEnding.PDF_ANNOT_LE_R_CLOSED_ARROW);
- annot.Update(fillColor: new float[] { 1, 1, 0 });
- print_descr(annot);
-
- r += Constants.displ;
- annot = page.AddPolygonAnnot(new List { r.BottomLeft, r.TopRight, r.BottomRight, r.TopLeft }); // 'Polygon'
- annot.SetBorder(width: 0.3f, dashes: new int[] { 2 });
- annot.SetColors(stroke: Constants.blue, fill: Constants.gold);
- annot.SetLineEnds(PdfLineEnding.PDF_ANNOT_LE_DIAMOND, PdfLineEnding.PDF_ANNOT_LE_CIRCLE);
- annot.Update();
- print_descr(annot);
-
- r += Constants.displ;
- annot = page.AddLineAnnot(r.TopRight, r.BottomLeft); // 'Line'
- annot.SetBorder(width: 0.3f, dashes: new int[] { 2 });
- annot.SetColors(stroke: Constants.blue, fill: Constants.gold);
- annot.SetLineEnds(PdfLineEnding.PDF_ANNOT_LE_DIAMOND, PdfLineEnding.PDF_ANNOT_LE_CIRCLE);
- annot.Update();
- print_descr(annot);
-
- r += Constants.displ;
- 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);
- print_descr(annot);
-
- r += Constants.displ;
- annot = page.AddCircleAnnot(r); // 'Circle'
- annot.SetBorder(width: 0.3f, dashes: new int[] { 2 });
- annot.SetColors(stroke: Constants.blue, fill: Constants.gold);
- annot.Update();
- print_descr(annot);
-
- r += Constants.displ;
- annot = page.AddFileAnnot(
- r.TopLeft, Encoding.UTF8.GetBytes("just anything for testing"), "testdata.txt"); // 'FileAttachment'
- print_descr(annot); // annot.rect
-
- r += Constants.displ;
- annot = page.AddStampAnnot(r, stamp: 10); // 'Stamp'
- annot.SetColors(stroke: Constants.green);
- annot.Update();
- print_descr(annot);
-
- r += Constants.displ + new Rect(0, 0, 50, 10);
- float rc = page.InsertTextbox(
- r,
- "This content will be removed upon applying the redaction.",
- color: Constants.blue,
- align: (int)TextAlign.TEXT_ALIGN_CENTER
- );
- annot = page.AddRedactAnnot(r.Quad);
- print_descr(annot);
-
- doc.Save(typeof(NewAnnots).Name + ".pdf", deflate:1);
-
- doc.Close();
-
- Console.WriteLine("Saved to " + typeof(NewAnnots).Name + ".pdf");
- }
- }
-}
diff --git a/Demo/Samples/Annotations/Program.Annotations.FreeText.cs b/Demo/Samples/Annotations/Program.Annotations.FreeText.cs
deleted file mode 100644
index 4f398e5f..00000000
--- a/Demo/Samples/Annotations/Program.Annotations.FreeText.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-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}.
-
";
- (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/Support/Constants.cs b/Demo/Support/Constants.cs
deleted file mode 100644
index 7c354a14..00000000
--- a/Demo/Support/Constants.cs
+++ /dev/null
@@ -1,39 +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 Constants
- {
- // some colors
- public static float[] red = new float[] { 1, 0, 0 };
- public static float[] blue = new float[] { 0, 0, 1 };
- public static float[] gold = new float[] { 1, 1, 0 };
- public static float[] green = new float[] { 0, 1, 0 };
- public static float[] white = new float[] { 1, 1, 1 };
- public static float[] black = new float[] { 0, 0, 0 };
-
- // rectangles and points
- public static Rect displ = new Rect(0, 50, 0, 50);
- public static Rect r = new Rect(72, 72, 220, 100);
- public static Rect rect = new Rect(100, 100, 200, 200);
-
- // string
- public static string t1 = "têxt üsès Lätiñ charß,\nEUR: €, mu: µ, super scripts: ²³!";
- public static string highlight = "this text is highlighted";
- public static string underline = "this text is underlined";
- public static string strikeout = "this text is striked out";
- public static string squiggled = "this text is zigzag-underlined";
-
- public static Func FILENAME = () =>
- {
- return System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
- };
- }
-}
diff --git a/Demo/Support/Units.cs b/Demo/Support/Units.cs
deleted file mode 100644
index 730f7d3e..00000000
--- a/Demo/Support/Units.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-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/TestDocuments/Annot.pdf b/Demo/TestDocuments/Annot.pdf
deleted file mode 100644
index f3897e58..00000000
Binary files a/Demo/TestDocuments/Annot.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/2.png b/Demo/TestDocuments/Barcodes/2.png
deleted file mode 100644
index e23d2c42..00000000
Binary files a/Demo/TestDocuments/Barcodes/2.png and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/Input.pdf b/Demo/TestDocuments/Barcodes/Input.pdf
deleted file mode 100644
index 32620a1c..00000000
Binary files a/Demo/TestDocuments/Barcodes/Input.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/Low/drawing.pdf b/Demo/TestDocuments/Barcodes/Low/drawing.pdf
deleted file mode 100644
index befc3242..00000000
Binary files a/Demo/TestDocuments/Barcodes/Low/drawing.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/Low/fiamm.pdf b/Demo/TestDocuments/Barcodes/Low/fiamm.pdf
deleted file mode 100644
index e1277543..00000000
Binary files a/Demo/TestDocuments/Barcodes/Low/fiamm.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/Low/ocr-me.pdf b/Demo/TestDocuments/Barcodes/Low/ocr-me.pdf
deleted file mode 100644
index 6da6f056..00000000
Binary files a/Demo/TestDocuments/Barcodes/Low/ocr-me.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/Sample1.jpg b/Demo/TestDocuments/Barcodes/Sample1.jpg
deleted file mode 100644
index 5249d2d3..00000000
Binary files a/Demo/TestDocuments/Barcodes/Sample1.jpg and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/Sample1.pdf b/Demo/TestDocuments/Barcodes/Sample1.pdf
deleted file mode 100644
index 542b6814..00000000
Binary files a/Demo/TestDocuments/Barcodes/Sample1.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/Samplepng.png b/Demo/TestDocuments/Barcodes/Samplepng.png
deleted file mode 100644
index 7f079d20..00000000
Binary files a/Demo/TestDocuments/Barcodes/Samplepng.png and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/Samples.pdf b/Demo/TestDocuments/Barcodes/Samples.pdf
deleted file mode 100644
index 321c62e2..00000000
Binary files a/Demo/TestDocuments/Barcodes/Samples.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/datamatrix.pdf b/Demo/TestDocuments/Barcodes/datamatrix.pdf
deleted file mode 100644
index 9ddfadca..00000000
Binary files a/Demo/TestDocuments/Barcodes/datamatrix.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/output.png b/Demo/TestDocuments/Barcodes/output.png
deleted file mode 100644
index 1aab7ebf..00000000
Binary files a/Demo/TestDocuments/Barcodes/output.png and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/qr.png b/Demo/TestDocuments/Barcodes/qr.png
deleted file mode 100644
index fe983667..00000000
Binary files a/Demo/TestDocuments/Barcodes/qr.png and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/rendered.bmp b/Demo/TestDocuments/Barcodes/rendered.bmp
deleted file mode 100644
index b19e28a9..00000000
Binary files a/Demo/TestDocuments/Barcodes/rendered.bmp and /dev/null differ
diff --git a/Demo/TestDocuments/Barcodes/samplejpg.jpg b/Demo/TestDocuments/Barcodes/samplejpg.jpg
deleted file mode 100644
index 947a0184..00000000
Binary files a/Demo/TestDocuments/Barcodes/samplejpg.jpg and /dev/null differ
diff --git a/Demo/TestDocuments/Blank.pdf b/Demo/TestDocuments/Blank.pdf
deleted file mode 100644
index 6baef93d..00000000
Binary files a/Demo/TestDocuments/Blank.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/CMYK_Recolor.pdf b/Demo/TestDocuments/CMYK_Recolor.pdf
deleted file mode 100644
index 1d427b8e..00000000
Binary files a/Demo/TestDocuments/CMYK_Recolor.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/CMYK_Recolor1.pdf b/Demo/TestDocuments/CMYK_Recolor1.pdf
deleted file mode 100644
index e7637642..00000000
Binary files a/Demo/TestDocuments/CMYK_Recolor1.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Color.pdf b/Demo/TestDocuments/Color.pdf
deleted file mode 100644
index 6086ec95..00000000
Binary files a/Demo/TestDocuments/Color.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Html/test.html b/Demo/TestDocuments/Html/test.html
deleted file mode 100644
index 9cdece9c..00000000
--- a/Demo/TestDocuments/Html/test.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- Lorem Ipsum
-
-
- Lorem Ipsum
-
-
- Lorem Ipsum
-
-
- Lorem Ipsum
-
-
- Lorem Ipsum
-
-
- Lorem Ipsum
-
-
- Lorem Ipsum
-
-
- Lorem Ipsum
-
-
- Lorem Ipsum
-
-
-
\ No newline at end of file
diff --git a/Demo/TestDocuments/Image/TestInsertImage.pdf b/Demo/TestDocuments/Image/TestInsertImage.pdf
deleted file mode 100644
index 23357d22..00000000
Binary files a/Demo/TestDocuments/Image/TestInsertImage.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Image/_apple.png b/Demo/TestDocuments/Image/_apple.png
deleted file mode 100644
index 6d81042d..00000000
Binary files a/Demo/TestDocuments/Image/_apple.png and /dev/null differ
diff --git a/Demo/TestDocuments/Image/_bb-logo.png b/Demo/TestDocuments/Image/_bb-logo.png
deleted file mode 100644
index 971607d7..00000000
Binary files a/Demo/TestDocuments/Image/_bb-logo.png and /dev/null differ
diff --git a/Demo/TestDocuments/Image/boxedpage.jpg b/Demo/TestDocuments/Image/boxedpage.jpg
deleted file mode 100644
index 1b7030f1..00000000
Binary files a/Demo/TestDocuments/Image/boxedpage.jpg and /dev/null differ
diff --git a/Demo/TestDocuments/Image/table.jpg b/Demo/TestDocuments/Image/table.jpg
deleted file mode 100644
index d8cad3cd..00000000
Binary files a/Demo/TestDocuments/Image/table.jpg and /dev/null differ
diff --git a/Demo/TestDocuments/Image/test.pdf b/Demo/TestDocuments/Image/test.pdf
deleted file mode 100644
index 5411b2a5..00000000
Binary files a/Demo/TestDocuments/Image/test.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Jbig2.pdf b/Demo/TestDocuments/Jbig2.pdf
deleted file mode 100644
index d8c9a130..00000000
Binary files a/Demo/TestDocuments/Jbig2.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Magazine.pdf b/Demo/TestDocuments/Magazine.pdf
deleted file mode 100644
index c8e166e0..00000000
Binary files a/Demo/TestDocuments/Magazine.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Morph.pdf b/Demo/TestDocuments/Morph.pdf
deleted file mode 100644
index a26cc1a3..00000000
Binary files a/Demo/TestDocuments/Morph.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/NewAnnots.pdf b/Demo/TestDocuments/NewAnnots.pdf
deleted file mode 100644
index 5a055ff1..00000000
Binary files a/Demo/TestDocuments/NewAnnots.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Ocr.pdf b/Demo/TestDocuments/Ocr.pdf
deleted file mode 100644
index 3f28b991..00000000
Binary files a/Demo/TestDocuments/Ocr.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/ReColor.pdf b/Demo/TestDocuments/ReColor.pdf
deleted file mode 100644
index fdf43744..00000000
Binary files a/Demo/TestDocuments/ReColor.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/SvgTest.pdf b/Demo/TestDocuments/SvgTest.pdf
deleted file mode 100644
index 822a211f..00000000
Binary files a/Demo/TestDocuments/SvgTest.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/TestPdf1.pdf b/Demo/TestDocuments/TestPdf1.pdf
deleted file mode 100644
index ae0f872e..00000000
Binary files a/Demo/TestDocuments/TestPdf1.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/TestResolveNames.pdf b/Demo/TestDocuments/TestResolveNames.pdf
deleted file mode 100644
index dd523186..00000000
Binary files a/Demo/TestDocuments/TestResolveNames.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/Widget.pdf b/Demo/TestDocuments/Widget.pdf
deleted file mode 100644
index f903200a..00000000
Binary files a/Demo/TestDocuments/Widget.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/columns.pdf b/Demo/TestDocuments/columns.pdf
deleted file mode 100644
index 18f5f159..00000000
Binary files a/Demo/TestDocuments/columns.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/err_table.pdf b/Demo/TestDocuments/err_table.pdf
deleted file mode 100644
index f7f19b3a..00000000
Binary files a/Demo/TestDocuments/err_table.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/issue_1880.pdf b/Demo/TestDocuments/issue_1880.pdf
deleted file mode 100644
index 306deab6..00000000
Binary files a/Demo/TestDocuments/issue_1880.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/issue_213.pdf b/Demo/TestDocuments/issue_213.pdf
deleted file mode 100644
index 5263abd3..00000000
Binary files a/Demo/TestDocuments/issue_213.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/kenpixel.ttf b/Demo/TestDocuments/kenpixel.ttf
deleted file mode 100644
index e3dc57ec..00000000
Binary files a/Demo/TestDocuments/kenpixel.ttf and /dev/null differ
diff --git a/Demo/TestDocuments/national-capitals.pdf b/Demo/TestDocuments/national-capitals.pdf
deleted file mode 100644
index d2b47219..00000000
Binary files a/Demo/TestDocuments/national-capitals.pdf and /dev/null differ
diff --git a/Demo/TestDocuments/test_widget_parse.pdf b/Demo/TestDocuments/test_widget_parse.pdf
deleted file mode 100644
index 70328c72..00000000
Binary files a/Demo/TestDocuments/test_widget_parse.pdf and /dev/null differ
diff --git "a/Demo/TestDocuments/\344\275\240\345\245\275.pdf" "b/Demo/TestDocuments/\344\275\240\345\245\275.pdf"
deleted file mode 100644
index 0b81fba8..00000000
Binary files "a/Demo/TestDocuments/\344\275\240\345\245\275.pdf" and /dev/null differ
diff --git a/MuPDF.NET.Test/AnnotTest.cs b/MuPDF.NET.Test/AnnotTest.cs
deleted file mode 100644
index 9573c38d..00000000
--- a/MuPDF.NET.Test/AnnotTest.cs
+++ /dev/null
@@ -1,506 +0,0 @@
-using ICSharpCode.SharpZipLib.Zip.Compression;
-using Newtonsoft.Json;
-using NUnit.Framework;
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using MuPDF.NET;
-
-namespace MuPDF.NET.Test
-{
- public class AnnotTest
- {
- [Test]
- public void Annot_CleanContents()
- {
- Document doc = new Document();
- Page page = doc.NewPage();
- Annot annot = page.AddHighlightAnnot(new Rect(10, 10, 20, 20));
-
- annot.CleanContents();
-
- Assert.That(Encoding.UTF8.GetString(annot.GetAP()).StartsWith("q"), Is.EqualTo(true));
- }
-
- [Test]
- public void Test_PdfString()
- {
- Utils.GetPdfNow();
- Utils.GetPdfString("Beijing, chinesisch 北京");
- Utils.GetTextLength("Beijing, chinesisch 北京", "null", fontName: "china-s");
- Utils.GetPdfString("Latin characters êßöäü");
- }
-
- [Test]
- public void TestCaret()
- {
- Document doc = new Document();
- Page page = doc.NewPage();
- Rect r = new Rect(72, 72, 220, 100);
- Annot annot = page.AddCaretAnnot(r.TopLeft);
-
- Assert.That(annot.Type.Item2, Is.EqualTo("Caret"));
- Assert.That((int)annot.Type.Item1, Is.EqualTo(14));
-
- annot.Update(rotate: 20);
-
- page.GetAnnotNames();
- page.GetAnnotXrefs();
- }
-
- [Test]
- public void TestFreeText1()
- {
- Document doc = new Document();
- Page page = doc.NewPage();
- Annot annot = page.AddFreeTextAnnot(
- Constants.r,
- Constants.t1,
- fontSize: 10,
- rotate: 90,
- textColor: new float[] { 0, 0, 1 },
- align: (int)TextAlign.TEXT_ALIGN_CENTER
- );
-
- annot.SetBorder(border: null, width: 0.3f, dashes: new int[] { 2 });
- annot.Update(textColor: new float[] { 0, 0, 1 }, fillColor: new float[] { 0, 1, 1 });
-
- Assert.That((int)annot.Type.Item1, Is.EqualTo(2));
- Assert.That(annot.Type.Item2, Is.EqualTo("FreeText"));
-
- page.Dispose();
- doc.Save(@"TestFreeText1.pdf");
- doc.Close();
- }
-
- [Test]
- public void TestFreeText2()
- {
- 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
- );
-
- Assert.That(annot.GetText(page).Length, Is.EqualTo(206));
-
- page.Dispose();
- doc.Save(@"TestFreeText2.pdf");
- doc.Close();
- }
-
- [Test]
- public void AddPolyLine()
- {
- Document doc = new Document();
- Page page = doc.NewPage();
- Rect r = new Rect(72, 72, 220, 100);
- Annot annot = page.AddFileAnnot(
- r.TopLeft,
- Encoding.UTF8.GetBytes("just anything for testing"),
- "testdata.txt"
- );
-
- Assert.That((int)annot.Type.Item1, Is.EqualTo(17));
- }
-
- [Test]
- public void Redact1()
- {
- Document doc = new Document();
- Page page = doc.NewPage();
- Annot annot = page.AddRedactAnnot(new Rect(72, 72, 200, 200).Quad, text: "Hello");
- annot.Update(rotate: -1);
- Assert.That((int)annot.Type.Item1, Is.EqualTo(12));
-
- annot.GetPixmap();
- AnnotInfo info = annot.Info;
- annot.SetInfo(info);
- Assert.That(annot.HasPopup, Is.False);
-
- annot.SetPopup(new Rect(72, 72, 100, 100));
- Rect s = annot.PopupRect;
-
- Assert.That(s.Abs(), Is.EqualTo(new Rect(72, 72, 100, 100).Abs()));
- page.ApplyRedactions();
- }
-
- [Test]
- public void Redact2()
- {
- Document doc = new Document("../../../resources/symbol-list.pdf");
- Page page = doc[0];
- List allText = page.GetText("words");
- page.AddRedactAnnot(page.Rect.Quad);
- page.ApplyRedactions(text: 0);
- List t = page.GetText("words");
-
- Assert.That(t.Count, Is.EqualTo(0));
- Assert.That(page.GetDrawings().Count, Is.Zero);
- }
-
- [Test]
- public void Redact3()
- {
- Document doc = new Document("../../../resources/symbol-list.pdf");
- Page page = doc[0];
- List arts = page.GetDrawings();
- page.AddRedactAnnot(page.Rect.Quad);
- page.ApplyRedactions(graphics: 0);
-
- Assert.That(page.GetText("words").Count, Is.Zero);
- Assert.That(arts.Count, Is.EqualTo(page.GetDrawings().Count));
- }
-
- [Test]
- public void FirstAnnot()
- {
- Document doc = new Document("../../../resources/annots.pdf");
- Page page = doc[0];
- Annot firstAnnot = (new List(page.GetAnnots()))[0];
- Annot next = firstAnnot.Next;
- }
-
- [Test]
- public void AddLineAnnot()
- {
- Document doc = new Document();
- Page page = doc.NewPage();
-
- page.AddLineAnnot(new Point(0, 0), new Point(100, 100));
- page.AddLineAnnot(new Point(100, 0), new Point(0, 100));
-
- IEnumerable annots = page.GetAnnots();
- foreach (Annot annot in annots)
- {
- annot.SetBorder(width: 8);
- annot.Update();
-
- Assert.That(annot.Type.Item1, Is.EqualTo(PdfAnnotType.PDF_ANNOT_LINE));
- }
- page.Dispose();
- doc.Save(@"AddLineAnnot.pdf"); // Save the modified document
- doc.Close();
- }
-
- /*
- * Test fix for #1645.
- * The expected output files assume annot_stem is 'jorj'. We need to always
- * restore this before returning (this is checked by conftest.py).
- */
- [Test]
- public void Test1645()
- {
- string annot_stem = Utils.ANNOT_ID_STEM;
- Utils.SetAnnotStem("jorj");
- try
- {
- string path_in = "../../../resources/symbol-list.pdf";
- string path_expected = "../../../resources/test_1645_expected.pdf";
- string path_out = "test_1645_out.pdf";
- Document doc = new Document(path_in);
- Page page = doc[0];
- Rect page_bounds = page.GetBound();
- Rect annot_loc = new Rect(page_bounds.X0, page_bounds.Y0, page_bounds.X0 + 75, page_bounds.Y0 + 15);
-
- page.AddFreeTextAnnot(
- annot_loc * page.DerotationMatrix,
- "TEST",
- fontSize: 18,
- fillColor: Utils.GetColor("FIREBRICK1"),
- rotate: page.Rotation
- );
-
- doc.Save(path_out, garbage: 1, deflate: 1, noNewId: 1);
- Console.WriteLine($@"Have created {path_out}. comparing with {path_expected}.");
-
- byte[] outBytes = File.ReadAllBytes(path_out);
- byte[] expectedBytes = File.ReadAllBytes(path_expected);
-
- Assert.IsTrue(outBytes.SequenceEqual(expectedBytes), "Byte arrays are not equal");
- }
- finally
- {
- Utils.SetAnnotStem(annot_stem);
- }
- }
-
- /*
- * Test fix for #4254.
- * Ensure that both annotations are fully created
- * We do this by asserting equal top-used colors in respective pixmaps.
- */
- [Test]
- public void Test4254()
- {
- int GetHashCode(byte[] obj)
- {
- if (obj == null) return 0;
- unchecked
- {
- int hash = 17;
- foreach (byte b in obj)
- hash = hash * 31 + b;
- return hash;
- }
- }
-
- Document doc = new Document();
- Page page = doc.NewPage();
-
- Rect rect = new Rect(100, 100, 200, 150);
- Annot annot = page.AddFreeTextAnnot(rect, "Test Annotation from minimal example");
- annot.SetBorder(width: 1, dashes: new int[] { 3, 3 });
- annot.SetOpacity(0.5f);
- try
- {
- annot.SetColors(stroke: new float[] { 1, 0, 0 });
- }
- catch (Exception e) {}
-
- annot.Update();
-
- rect = new Rect(200, 200, 400, 400);
- Annot annot2 = page.AddFreeTextAnnot(rect, "Test Annotation from minimal example pt 2");
- annot2.SetBorder(width: 1, dashes: new int[] { 3, 3 });
- annot2.SetOpacity(0.5f);
- try
- {
- annot.SetColors(stroke: new float[] { 1, 0, 0 });
- }
- catch (Exception e) { }
-
- annot.Update();
- annot2.Update();
-
- doc.Save("test_4254.pdf");
-
- // stores top color for each pixmap
- HashSet top_colors = new HashSet();
- foreach (var _annot in page.GetAnnots())
- {
- Pixmap pix = _annot.GetPixmap();
- top_colors.Add(GetHashCode(pix.ColorTopUsage().Item2));
- }
-
- // only one color must exist
- Assert.IsTrue(top_colors.Count == 1, "No colors found in annotations pixmaps.");
- }
-
- /*
- * Test creation of rich text FreeText annotations.
- * We create the same annotation on different pages in different ways,
- * with and without using Annotation.update(), and then assert equality
- * of the respective images.
- * We do this by asserting equal top-used colors in respective pixmaps.
- */
- [Test]
- public void TestRichText()
- {
- string ds = "font-size: 11pt; font-family: sans-serif;";
- string bullet = "\u2610\u2611\u2612"; // Output: ☐☑☒;
-
- string text = $@"
-MuPDF.NET འདི་ ཡིག་ཆ་བཀྲམ་སྤེལ་གྱི་དོན་ལུ་ པའི་ཐོན་ཐུམ་སྒྲིལ་དྲག་ཤོས་དང་མགྱོགས་ཤོས་ཅིག་ཨིན།
-Here is some bold and italic text, followed by bold-italic. Text-based check boxes: {bullet}.
-
";
-
- Document doc = new Document();
-
- // first page
- 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: 0.5f,
- rotate: 90,
- borderWidth: 1,
- dashes: null,
- richtext: true,
- callout: new Point[] { p3, p2, rect.TopRight }
- );
- Pixmap pix1 = page.GetPixmap();
-
- // # Second page.
- // the annotation is created with minimal parameters, which are supplied
- // in a separate call to the .update() method.
- page = doc.NewPage();
- annot = page.AddFreeTextAnnot(
- rect,
- text,
- borderWidth: 1,
- dashes: null,
- richtext: true,
- callout: new Point[] { p3, p2, rect.TopRight }
- );
- annot.Update(fillColor: Constants.gold, opacity: 0.5f, rotate: 90);
- Pixmap pix2 = page.GetPixmap();
-
- doc.Save("test_rich_text.pdf");
- doc.Close();
-
- Assert.That(pix1.SAMPLES, Is.EqualTo(pix2.SAMPLES));
-
- pix1.Dispose();
- pix2.Dispose();
- }
-
- /*
- * Test fix for #4447.
- */
- [Test]
- public void Test4447()
- {
- Document doc = new Document();
- Page page = doc.NewPage();
-
- float[] text_color = Constants.red;
- float[] fill_color = Constants.green;
- float[] border_color = Constants.blue;
-
- Rect annot_rect = new Rect(90.1f, 486.73f, 139.26f, 499.46f);
-
- try
- {
- Annot annot = page.AddFreeTextAnnot(
- annot_rect,
- "AETERM",
- fontName: "Arial",
- fontSize: 10,
- textColor: text_color,
- fillColor: fill_color,
- borderColor: border_color,
- borderWidth: 1
- );
- }
- catch (Exception e)
- {
- Assert.That(true, $@"cannot set border_color if rich_text is False {e.Message}");
- }
-
- try
- {
- Annot annot = page.AddFreeTextAnnot(
- new Rect(30, 400, 100, 450),
- "Two",
- fontName: "Arial",
- fontSize: 10,
- textColor: text_color,
- fillColor: fill_color,
- borderColor: border_color,
- borderWidth: 1
- );
- }
- catch (Exception e)
- {
- Assert.That(true, $@"cannot set border_color if rich_text is False {e.Message}");
- }
-
- {
- Annot annot = page.AddFreeTextAnnot(
- new Rect(30, 500, 100, 550),
- "Three",
- fontName: "Arial",
- fontSize: 10,
- textColor: text_color,
- borderWidth: 1
- );
- annot.Update(textColor: text_color, fillColor: fill_color);
- try
- {
- annot.Update(borderColor: border_color);
- }
- catch (Exception e)
- {
- Assert.That(true, e.Message, Does.Contain("cannot set border_color if rich_text is False"));
- }
- }
-
- doc.Save("test_4447.pdf");
- doc.Close();
- }
-
- /*
- * Test Stamp.
- */
- [Test]
- public void TestStamp()
- {
- Document doc = new Document();
- Page page = doc.NewPage();
-
- Rect r = new Rect(72, 72, 220, 100);
-
- Annot annot = page.AddStampAnnot(r, stamp: 0);
- Assert.That(annot.Type.Item1, Is.EqualTo(PdfAnnotType.PDF_ANNOT_STAMP));
- Assert.That(annot.Type.Item2, Is.EqualTo("Stamp"));
- Assert.That(annot.Info.Content, Is.EqualTo("Approved"));
- string annot_id = annot.Info.Id;
- int annot_xref = annot.Xref;
- Annot annot1 = page.LoadAnnot(annot_id);
- Annot annot2 = page.LoadAnnot(annot_xref);
- Assert.That(annot1.Xref, Is.EqualTo(annot2.Xref));
- page = doc.ReloadPage(page);
-
- doc.Save("test_stamp.pdf");
- doc.Close();
- }
-
- /*
- * Test Image Stamp.
- */
- [Test]
- public void TestImageStamp()
- {
- Document doc = new Document();
- Page page = doc.NewPage();
-
- Rect r = new Rect(72, 72, 220, 100);
-
- string filename = "../../../resources/nur-ruhig.jpg";
- Annot annot = page.AddStampAnnot(r, stamp: filename);
- Assert.That(annot.Info.Content, Is.EqualTo("Image Stamp"));
-
- doc.Save("test_image_stamp.pdf");
- doc.Close();
- }
- }
-}
diff --git a/MuPDF.NET.Test/AnnotTests.cs b/MuPDF.NET.Test/AnnotTests.cs
new file mode 100644
index 00000000..6cc6d05f
--- /dev/null
+++ b/MuPDF.NET.Test/AnnotTests.cs
@@ -0,0 +1,462 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Xunit;
+
+namespace MuPDF.NET.Test
+{
+ ///
+ /// Tests for the Annot class.
+ /// Ported from tests/test_annots.py.
+ ///
+ public class AnnotTests
+ {
+ private float[] red = { 1, 0, 0 };
+ private float[] blue = { 0, 0, 1 };
+ private float[] gold = { 1, 1, 0 };
+ private float[] green = { 0, 1, 0 };
+
+ private Rect displ = new Rect(0, 50, 0, 50);
+ private Rect r = new Rect(72, 72, 220, 100);
+ private string t1 = "têxt üsès Lätiñ charß,\nEUR: €, mu: µ, super scripts: ²³!";
+ private Rect rect = new Rect(100, 100, 200, 200);
+
+ private (Document doc, Page page) CreateDocWithPage()
+ {
+ var doc = new Document();
+ var page = doc.NewPage();
+ return (doc, page);
+ }
+
+ // ─── Add annotations ────────────────────────────────────────────
+
+ [Fact]
+ public void Annot_AddTextAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddTextAnnot(r.TL, t1);
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddTextAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Text", annot.TypeString);
+ Assert.True(annot.Xref > 0);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddFreeTextAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ // `borderWidth` exists only on the full AddFreeTextAnnot overload (disambiguates from compact).
+ var annot = page.AddFreeTextAnnot(
+ rect,
+ t1,
+ fontsize: 10,
+ rotate: 90,
+ textColor: blue,
+ fillColor: gold,
+ align: Constants.TEXT_ALIGN_CENTER
+ );
+ annot.SetBorder(width: 0.3f, dashes: new float[] { 2 });
+ annot.Update(textColor: blue, fillColor: gold);
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddFreeTextAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("FreeText", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddHighlightAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddHighlightAnnot(new[] { new Quad(rect) });
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddHighlightAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Highlight", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddUnderlineAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddUnderlineAnnot(new[] { new Quad(rect) });
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddUnderlineAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Underline", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddStrikeoutAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddStrikeoutAnnot(new[] { new Quad(rect) });
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddStrikeoutAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("StrikeOut", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddSquigglyAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddSquigglyAnnot(new[] { new Quad(rect) });
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddSquigglyAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Squiggly", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddLineAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddLineAnnot(new Point(50, 50), new Point(200, 200));
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddLineAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Line", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddRectAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddRectAnnot(new Rect(50, 50, 200, 200));
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddRectAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Square", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddCircleAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddCircleAnnot(new Rect(50, 50, 200, 200));
+ annot.Update(fillColor: new float[] { 1, 0, 0 }, rotate: 20, opacity: 0.1f);
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddCircleAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Circle", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddPolylineAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ Rect rect = page.Rect + new Rect(100, 36, -100, -36);
+
+ var cell = Utils.MakeTable(rect, rows: 10);
+
+ for (int i = 0; i < 10; i++)
+ {
+ // (cell[i][0].bl, cell[i][0].br)
+ var points = new[]
+ {
+ cell[i][0].BottomLeft,
+ cell[i][0].BottomRight
+ };
+
+ var annot_ = page.AddPolylineAnnot(points);
+
+ annot_.SetLineEnds(i, i);
+ annot_.Update();
+ }
+
+ int index = 0;
+
+ foreach (var annot__ in page.annots())
+ {
+ var lineEnds = annot__.line_ends;
+
+ //Debug.Assert(lineEnds.Item1 == index);
+ //Debug.Assert(lineEnds.Item2 == index);
+
+ index++;
+ }
+
+ // last annot from enumeration
+ var lastAnnot = page.annots().Last();
+
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddPolylineAnnot.pdf");
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddPolygonAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var points = new List
+ {
+ rect.BL, rect.TR, rect.BR, rect.TL
+ };
+ var annot = page.AddPolygonAnnot(points.ToArray());
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddPolygonAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Polygon", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddInkAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var strokes = new List>
+ {
+ new List
+ {
+ new Point(50, 50), new Point(100, 100), new Point(150, 50)
+ }
+ };
+ var annot = page.AddInkAnnot(strokes.Select(s => s.ToArray()).ToArray());
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddInkAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Ink", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ [Fact]
+ public void Annot_AddCaretAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddCaretAnnot(new Point(100, 100));
+ annot.Update(rotate: 20);
+ Assert.NotNull(annot);
+ Assert.Equal("Caret", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ List names = page.AnnotNames();
+ var xrefs = page.AnnotXrefs();
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddCaretAnnot.pdf");
+ }
+ }
+
+ [Fact]
+ public void Annot_AddStampAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddStampAnnot(new Rect(100, 100, 300, 200));
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddStampAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Stamp", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ }
+ }
+
+ [Fact]
+ public void Annot_AddRedactAnnot()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddRedactAnnot(new Quad(new Rect(50, 50, 200, 100)));
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_AddRedactAnnot.pdf");
+ Assert.NotNull(annot);
+ Assert.Equal("Redact", annot.TypeString);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on
+ }
+ }
+
+ // ─── Annotation properties ──────────────────────────────────────
+
+ [Fact]
+ public void Annot_RectProperty()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddRectAnnot(new Rect(50, 50, 200, 200));
+ var rect = annot.Rect;
+ Assert.True(rect.Width > 0);
+ Assert.True(rect.Height > 0);
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_RectProperty.pdf");
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ }
+ }
+
+ [Fact]
+ public void Annot_Contents()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddTextAnnot(new Point(100, 100), "My Contents");
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_Contents.pdf");
+ Assert.Equal("My Contents", annot.Contents);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ }
+ }
+
+ [Fact]
+ public void Annot_Flags()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddTextAnnot(new Point(100, 100), "Flags");
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_Flags.pdf");
+ Assert.True(annot.Flags >= 0);
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ }
+ }
+
+ [Fact]
+ public void Annot_Opacity()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot1 = page.AddTextAnnot(new Point(100, 100), "Flags");
+ //var annot = page.AddRectAnnot(new Rect(50, 50, 200, 200));
+ var annot = page.AddRedactAnnot(new Quad(new Rect(50, 50, 200, 200)));
+ //annot.SetOpacity(0.5f);
+ Assert.False(TestHelper.IsClose(0.5, annot.Opacity, 0.01));
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_Opacity.pdf");
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ }
+ }
+
+ // ─── Iteration ──────────────────────────────────────────────────
+
+ [Fact]
+ public void Page_AnnotsEnumeration()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ page.AddTextAnnot(new Point(50, 50), "A1");
+ page.AddRectAnnot(new Rect(100, 100, 200, 200));
+ page.AddCircleAnnot(new Rect(200, 200, 300, 300));
+ int count = page.Annots().Count();
+ Assert.Equal(3, count);
+ doc.Save(@"E:\Pdf\Tmp\Test\Page_AnnotsEnumeration.pdf");
+ page.Dispose();
+ }
+ }
+
+ [Fact]
+ public void Page_NoAnnots()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ int count = page.Annots().Count();
+ Assert.Equal(0, count);
+ }
+ }
+
+ // ─── Delete ─────────────────────────────────────────────────────
+
+ [Fact]
+ public void Annot_Delete()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddTextAnnot(new Point(50, 50), "Delete Me");
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_Delete_Before.pdf");
+ Assert.NotNull(page.FirstAnnot);
+ page.DeleteAnnot(annot);
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_Delete.pdf");
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on save.
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ }
+ }
+
+ // ─── Update ─────────────────────────────────────────────────────
+
+ [Fact]
+ public void Annot_Update()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddRectAnnot(new Rect(50, 50, 200, 200));
+ annot.SetColors(new float[] { 0, 1, 1 });
+ //annot.Update(default(float[]), opacity: 0);
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_Update.pdf");
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ }
+ }
+
+ // ─── SetInfo / GetInfo ──────────────────────────────────────────
+
+ [Fact]
+ public void Annot_SetAndGetInfo()
+ {
+ var (doc, page) = CreateDocWithPage();
+ using (doc)
+ {
+ var annot = page.AddTextAnnot(new Point(50, 50), "Info Test");
+ annot.SetInfo(new Dictionary { ["title"] = "Test Author" });
+ var result = annot.GetInfo();
+ Assert.Contains("title", result.Keys);
+ Assert.Equal("Test Author", result["title"]);
+ doc.Save(@"E:\Pdf\Tmp\Test\Annot_SetAndGetInfo.pdf");
+ annot.Dispose(); // Dispose annot before page to avoid "Page still in use" error on
+ page.Dispose(); // Dispose page before doc to avoid "Page still in use" error on save.
+ }
+ }
+ }
+}
diff --git a/MuPDF.NET.Test/ArchiveTest.cs b/MuPDF.NET.Test/ArchiveTest.cs
deleted file mode 100644
index 25825704..00000000
--- a/MuPDF.NET.Test/ArchiveTest.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace MuPDF.NET.Test
-{
- public class ArchiveTest
- {
-
- }
-}
diff --git a/MuPDF.NET.Test/BarcodeTest.cs b/MuPDF.NET.Test/BarcodeTest.cs
deleted file mode 100644
index 99336642..00000000
--- a/MuPDF.NET.Test/BarcodeTest.cs
+++ /dev/null
@@ -1,274 +0,0 @@
-using NUnit.Framework;
-using mupdf;
-
-namespace MuPDF.NET.Test
-{
- public class BarcodeTest
- {
- [Test]
- public void ReadBarcodeFromImage()
- {
- string imageFilePath = Path.GetFullPath("../../../resources/Barcodes/rendered.bmp");
-
- // Code 128
- List code128s = Utils.ReadBarcodes(imageFilePath, barcodeFormat:BarcodeFormat.CODE128);
- Assert.IsTrue(code128s.Count == 3);
- Assert.IsTrue(code128s[0].Text == "Hello World!");
- Assert.IsTrue(code128s[1].Text == "0123456789");
- Assert.IsTrue(code128s[2].Text == "010123456789012810012345678915041220");
-
- // I2OF5
- List i2of5s = Utils.ReadBarcodes(imageFilePath, clip: new Rect(1245, 690, 2200, 1235), barcodeFormat: BarcodeFormat.I2OF5);
- Assert.IsTrue(i2of5s.Count == 2);
- Assert.IsTrue(i2of5s[0].Text == "0123456789");
- Assert.IsTrue(i2of5s[1].Text == "15400141288763");
-
- // Code 39
- List code39s = Utils.ReadBarcodes(imageFilePath, clip: new Rect(1245, 1295, 2200, 1520), barcodeFormat: BarcodeFormat.CODE39);
- Assert.IsTrue(code39s.Count == 1);
- Assert.IsTrue(code39s[0].Text == "0123456789");
-
- // UPC-A
- List upcAs = Utils.ReadBarcodes(imageFilePath, clip: new Rect(1245, 2490, 2200, 2720), barcodeFormat: BarcodeFormat.UPC_A);
- Assert.IsTrue(upcAs.Count == 1);
- Assert.IsTrue(upcAs[0].Text == "001234567895");
-
- // UPC-E
- List upcEs = Utils.ReadBarcodes(imageFilePath, clip: new Rect(1245, 2780, 2200, 3020), barcodeFormat: BarcodeFormat.UPC_E);
- Assert.IsTrue(upcEs.Count == 1);
- Assert.IsTrue(upcEs[0].Text == "01234133");
- }
-
- [Test]
- public void ReadBarcodeFromPdf()
- {
- string pdfFilePath = Path.GetFullPath("../../../resources/Barcodes/Samples.pdf");
-
- Document doc = new Document(pdfFilePath);
-
- Page page = doc[0];
-
- // EAN 8
- List ean8s = page.ReadBarcodes(barcodeFormat: BarcodeFormat.EAN8);
- Assert.IsTrue(ean8s.Count == 1);
- Assert.IsTrue(ean8s[0].Text == "40123455");
-
- // EAN 13
- List ean13s = page.ReadBarcodes(clip: new Rect(212, 243, 510, 575), barcodeFormat: BarcodeFormat.EAN13);
- Assert.IsTrue(ean13s.Count == 3);
- Assert.IsTrue(ean13s[0].Text == "4012345678901");
- Assert.IsTrue(ean13s[1].Text == "9783161484100");
- Assert.IsTrue(ean13s[2].Text == "9771234567058");
-
- // UPC-A
- List upcAs = page.ReadBarcodes(clip: new Rect(212, 575, 510, 690), barcodeFormat: BarcodeFormat.UPC_A);
- Assert.IsTrue(upcAs.Count == 1);
- Assert.IsTrue(upcAs[0].Text == "042287061527");
-
- // UPC-E
- List upcEs = page.ReadBarcodes(clip: new Rect(212, 690, 510, 790), barcodeFormat: BarcodeFormat.UPC_E);
- Assert.IsTrue(upcEs.Count == 1);
- Assert.IsTrue(upcEs[0].Text == "12345670");
-
- // 2/5 Interleaved
- List i2of5s = page.ReadBarcodes(clip: new Rect(800, 160, 1100, 370), barcodeFormat: BarcodeFormat.I2OF5);
- Assert.IsTrue(i2of5s.Count == 2);
- Assert.IsTrue(i2of5s[0].Text == "012345678905");
- Assert.IsTrue(i2of5s[1].Text == "40123456789010");
-
- // PHARMA
- List pharmas = page.ReadBarcodes(clip: new Rect(800, 230, 1100, 295), barcodeFormat: BarcodeFormat.PHARMA);
- Assert.IsTrue(pharmas.Count == 1);
- Assert.IsTrue(pharmas[0].Text == "-1043773788");
-
- // Code 39
- List code39s = page.ReadBarcodes(clip: new Rect(800, 370, 1100, 435), barcodeFormat: BarcodeFormat.CODE39);
- Assert.IsTrue(code39s.Count == 1);
- Assert.IsTrue(code39s[0].Text == "123ABC$");
-
- // Code 39 Extended
- List code39Exs = page.ReadBarcodes(clip: new Rect(800, 435, 1100, 505), barcodeFormat: BarcodeFormat.CODE39_EX);
- Assert.IsTrue(code39Exs.Count == 1);
- Assert.IsTrue(code39Exs[0].Text == "123+A+B+CX");
-
- // Code 128
- List code128s = page.ReadBarcodes(clip: new Rect(800, 505, 1100, 645), barcodeFormat: BarcodeFormat.CODE128);
- Assert.IsTrue(code128s.Count == 2);
- Assert.IsTrue(code128s[0].Text == "1234567890");
- Assert.IsTrue(code128s[1].Text == "014012345678901");
-
- // PHARMA
- pharmas = page.ReadBarcodes(clip: new Rect(800, 645, 1100, 775), barcodeFormat: BarcodeFormat.PHARMA);
- Assert.IsTrue(pharmas.Count == 2);
- Assert.IsTrue(pharmas[0].Text == "1319299");
- Assert.IsTrue(pharmas[1].Text == "12345");
-
- // TRIOPTIC
- List trioptics = page.ReadBarcodes(clip: new Rect(800, 775, 1100, 845), barcodeFormat: BarcodeFormat.TRIOPTIC);
- Assert.IsTrue(trioptics.Count == 1);
- Assert.IsTrue(trioptics[0].Text == "-1234567");
-
- // Datamatrix
- List datamatrixes = page.ReadBarcodes(clip: new Rect(1400, 150, 1660, 357), barcodeFormat: BarcodeFormat.DM);
- //Assert.IsTrue(datamatrixes.Count == 1);
- //Assert.IsTrue(datamatrixes[0].Text == "-1234567");
-
- // QR
- List qrs = page.ReadBarcodes(clip: new Rect(1400, 357, 1660, 500), barcodeFormat: BarcodeFormat.QR);
- Assert.IsTrue(qrs.Count == 1);
- Assert.IsTrue(qrs[0].Text == "https://softmatic.com");
-
- // PDF417
- List pdf417s = page.ReadBarcodes(clip: new Rect(1400, 500, 1660, 580), barcodeFormat: BarcodeFormat.PDF417);
- Assert.IsTrue(pdf417s.Count == 1);
- Assert.IsTrue(pdf417s[0].Text == "1234567890ABCDEF");
-
- // Aztec
- List aztecs = page.ReadBarcodes(clip: new Rect(1400, 580, 1660, 720), barcodeFormat: BarcodeFormat.AZTEC);
- Assert.IsTrue(aztecs.Count == 1);
- Assert.IsTrue(aztecs[0].Text == "1234567890ABCDEF");
-
- page.Dispose();
- doc.Close();
- }
-
- [Test]
- public void ReadDatamatrixFromPdf()
- {
- string pdfFilePath = Path.GetFullPath("../../../resources/Barcodes/datamatrix.pdf");
-
- Document doc = new Document(pdfFilePath);
-
- Page page = doc[0];
-
- List barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.DM);
- Assert.IsTrue(barcodes.Count == 2);
- Assert.IsTrue(barcodes[0].Text == "01020000110435177000");
- Assert.IsTrue(barcodes[1].Text == "1100630047057533");
-
- page.Dispose();
- doc.Close();
- }
-
- [Test]
- public void ReadQrFromPdf()
- {
- string pdfFilePath = Path.GetFullPath("../../../resources/Barcodes/qr.pdf");
-
- Document doc = new Document(pdfFilePath);
-
- Page page = doc[0];
-
- List barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.QR);
- Assert.IsTrue(barcodes.Count == 1);
- Assert.IsTrue(barcodes[0].Text == "$001-52-20250520#MRC");
-
- page.Dispose();
- doc.Close();
- }
-
- [Test]
- public void TestBarcode()
- {
- string testFilePath = @"TestBarcode.pdf";
-
- Document doc = new Document();
- Page page = doc.NewPage();
-
- 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!", 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: true, 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);
-
- page.Dispose();
- doc.Save(testFilePath);
- doc.Close();
-
- // read barcodes in the new pdf document
- doc = new Document(testFilePath);
- page = doc[0];
-
- List barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.QR);
- Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "Hello World!");
-
- barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.EAN8);
- Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "12345670");
-
- barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.EAN13);
- Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "0123456789012");
-
- barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.UPC_A);
- Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "123456789012");
-
- barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.CODE39);
- Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "H+E+L+L+O/A");
-
- barcodes = page.ReadBarcodes(clip: new Rect(100, 320, 400, 355), barcodeFormat: BarcodeFormat.CODE128);
- Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "Hello World!");
-
- barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.I2OF5);
- Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "12345678901234567890");
-
- barcodes = page.ReadBarcodes(barcodeFormat: BarcodeFormat.PDF417);
- Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "Hello World!");
-
- barcodes = page.ReadBarcodes(clip: new Rect(80, 500, 450, 700), barcodeFormat: BarcodeFormat.CODABAR);
- //Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "12345678901234567890");
-
- barcodes = page.ReadBarcodes(clip: new Rect(100, 620, 140, 660), barcodeFormat: BarcodeFormat.DM);
- //Assert.IsTrue(barcodes.Count == 1 && barcodes[0].Text == "01100000110419257000");
-
- page.Dispose();
- doc.Close();
- }
- }
-}
diff --git a/MuPDF.NET.Test/ColorspaceTests.cs b/MuPDF.NET.Test/ColorspaceTests.cs
new file mode 100644
index 00000000..d8e81877
--- /dev/null
+++ b/MuPDF.NET.Test/ColorspaceTests.cs
@@ -0,0 +1,39 @@
+using Xunit;
+
+namespace MuPDF.NET.Test
+{
+ ///
+ /// Tests for the Colorspace class.
+ ///
+ public class ColorspaceTests
+ {
+ [Fact]
+ public void Colorspace_RGB()
+ {
+ var cs = Colorspace.CsRGB;
+ Assert.Equal(3, cs.N);
+ Assert.Contains("RGB", cs.Name, System.StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Colorspace_Gray()
+ {
+ var cs = Colorspace.CsGRAY;
+ Assert.Equal(1, cs.N);
+ }
+
+ [Fact]
+ public void Colorspace_CMYK()
+ {
+ var cs = Colorspace.CsCMYK;
+ Assert.Equal(4, cs.N);
+ }
+
+ [Fact]
+ public void Colorspace_NameNotEmpty()
+ {
+ var cs = Colorspace.CsRGB;
+ Assert.False(string.IsNullOrEmpty(cs.Name));
+ }
+ }
+}
diff --git a/MuPDF.NET.Test/DisposePatternTest.cs b/MuPDF.NET.Test/DisposePatternTest.cs
deleted file mode 100644
index 80fdc4b6..00000000
--- a/MuPDF.NET.Test/DisposePatternTest.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System;
-using System.IO;
-using NUnit.Framework;
-using MuPDF.NET;
-
-namespace MuPDF.NET.Test
-{
- public class DisposePatternTest
- {
- private const string TocPath = "../../../resources/toc.pdf";
-
- [Test]
- public void Document_Dispose_MultipleTimes_DoesNotThrow()
- {
- var doc = new Document(TocPath);
-
- doc.Dispose();
-
- Assert.DoesNotThrow(() => doc.Dispose());
- }
-
- [Test]
- public void Page_Dispose_MultipleTimes_DoesNotThrow()
- {
- var doc = new Document(TocPath);
- var page = doc[0];
-
- page.Dispose();
-
- Assert.DoesNotThrow(() => page.Dispose());
-
- doc.Dispose();
- }
-
- [Test]
- public void TextPage_Dispose_MultipleTimes_DoesNotThrow()
- {
- var doc = new Document(TocPath);
- var page = doc[0];
- var textPage = page.GetTextPage();
-
- textPage.Dispose();
-
- Assert.DoesNotThrow(() => textPage.Dispose());
-
- page.Dispose();
- doc.Dispose();
- }
-
- [Test]
- public void Story_Dispose_MultipleTimes_DoesNotThrow()
- {
- var story = new Story("
Hello
");
-
- story.Dispose();
-
- Assert.DoesNotThrow(() => story.Dispose());
- }
-
- [Test]
- public void DisplayList_Dispose_MultipleTimes_DoesNotThrow()
- {
- var rect = new Rect(0, 0, 100, 100);
- var dl = new DisplayList(rect);
-
- dl.Dispose();
-
- Assert.DoesNotThrow(() => dl.Dispose());
- }
-
- [Test]
- public void DocumentWriter_Dispose_MultipleTimes_DoesNotThrow()
- {
- string path = Path.GetTempFileName();
-
- try
- {
- var writer = new DocumentWriter(path);
-
- writer.Dispose();
-
- Assert.DoesNotThrow(() => writer.Dispose());
- }
- finally
- {
- if (File.Exists(path))
- File.Delete(path);
- }
- }
-
- [Test]
- public void Font_Dispose_MultipleTimes_DoesNotThrow()
- {
- var font = new Font();
-
- font.Dispose();
-
- Assert.DoesNotThrow(() => font.Dispose());
- }
-
- [Test]
- public void GraftMap_Dispose_MultipleTimes_DoesNotThrow()
- {
- var doc = new Document(TocPath);
- var map = new GraftMap(doc);
-
- map.Dispose();
-
- Assert.DoesNotThrow(() => map.Dispose());
-
- doc.Dispose();
- }
-
- // Outline is constructed internally from native MuPDF outline structures and
- // not exposed as a public constructor. Its disposal semantics are exercised
- // indirectly via Document/Document.GetToc tests, so we skip a direct idempotency test.
- }
-}
-
diff --git a/MuPDF.NET.Test/DocumentTest.cs b/MuPDF.NET.Test/DocumentTest.cs
deleted file mode 100644
index 7141ce01..00000000
--- a/MuPDF.NET.Test/DocumentTest.cs
+++ /dev/null
@@ -1,245 +0,0 @@
-using mupdf;
-using System;
-using System.IO;
-using System.Collections.Generic;
-using System.Data.Common;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using NUnit.Framework;
-
-namespace MuPDF.NET.Test
-{
- public class DocumentTest : PdfTestBase
- {
- [Test]
- public void CopyFullPage()
- {
- Document doc = new Document("../../../resources/toc.pdf");
- int oldLen = doc.PageCount;
- doc.CopyFullPage(0);
-
- Assert.AreEqual(doc.PageCount, oldLen + 1);
- doc.Close();
- }
-
- [Test]
- public void CopyPage()
- {
- Document doc = new Document("../../../resources/toc.pdf");
- int oldLen = doc.PageCount;
- doc.CopyPage(0);
-
- Assert.AreEqual(doc.PageCount, oldLen + 1);
- doc.Close();
- }
-
- [Test]
- public void ColorTest()
- {
- string testFilePath = Path.GetFullPath("../../../resources/DocumentTest/Color.pdf");
- Document doc = new Document(testFilePath);
- List images = doc.GetPageImages(0);
- Assert.IsTrue(images[0].CsName == "DeviceRGB");
-
- doc.Recolor(0, 4);
- images = doc.GetPageImages(0);
- Assert.IsTrue(images[0].CsName == "ICCBased");
-
- doc.Close();
- }
-
- [Test]
- public void DeletePage()
- {
- Document doc = new Document("../../../resources/toc.pdf");
- int oldLen = doc.PageCount;
- doc.DeletePage(0);
- Assert.AreEqual(doc.PageCount, oldLen - 1);
- doc.Close();
- }
-
- [Test]
- public void DeletePages()
- {
- Document doc = new Document("../../../resources/toc.pdf");
- int oldLen = doc.PageCount;
-
- doc.DeletePages(0); // delete one page
-
- doc.DeletePages(new int[] { 0, }); // delete 2 pages
-
- Assert.AreEqual(doc.PageCount, oldLen - 1);
- doc.Close();
- }
-
- [Test]
- public void XmlMetadata()
- {
- Document doc = new Document("../../../resources/toc.pdf");
- doc.DeleteXmlMetadata();
-
- Assert.That(doc.GetXmlMetadata(), Is.EqualTo(""));
- doc.Close();
- }
-
- [Test]
- public void GetXrefLen()
- {
- Document doc = new Document("../../../resources/toc.pdf");
- doc.GetXrefLength();
- //Assert.Pass();
- doc.Close();
- }
-
- [Test]
- public void GetPageImage_ExtractImage()
- {
- Document doc = new Document("../../../resources/toc.pdf");
- int n = doc.GetPageImages(0).Count;
-
- Assert.That(n, Is.EqualTo(15)); // in case of current input pdf, if other file, real count should be fixed
-
- n = doc.ExtractImage(doc.GetPageImages(0)[0].Xref).Image.Length;
-
- //Assert.Pass();
- doc.Close();
- }
-
- [Test]
- public void GetToc()
- {
- Document doc = new Document("../../../resources/toc.pdf");
- doc.GetToc(true);
- }
-
- [Test]
- public void EraseToc()
- {
- Document doc = new Document("../../../resources/toc.pdf");
- doc.SetToc(null);
- Assert.That(doc.GetToc().Count, Is.EqualTo(0));
- doc.Close();
- }
-
- [Test]
- public void Embedded()
- {
- Document doc = new Document();
- byte[] buffer = Encoding.UTF8.GetBytes("123456678790qwexcvnmhofbnmfsdg4589754uiofjkb-");
- doc.AddEmbfile("file1", buffer, "testfile.txt", "testfile-u.txt", "Description of some sort");
- }
-
- [Test]
- public void Test_IsNoPDF()
- {
- Document doc = new Document("../../../resources/DocumentTest/Bezier.epub");
- Assert.That(doc.IsPDF, Is.False);
- }
-
- [Test]
- public void Test_PageIds()
- {
- Document doc = new Document("../../../resources/DocumentTest/Bezier.epub");
-
- Assert.That(doc.ChapterCount, Is.EqualTo(7));
- Assert.That(doc.LastLocation.Item1, Is.EqualTo(6));
- }
-
- [Test]
- public void OC1()
- {
- Document doc = new Document();
- int ocg1 = doc.AddOcg("ocg1");
- int ocg2 = doc.AddOcg("ocg2");
- doc.SetOCMD(xref: 0, ocgs: new int[] { ocg1, ocg2 });
- doc.SetLayer(-1);
- doc.AddLayer("layer1");
-
- doc.GetLayer();
- doc.GetLayers();
- doc.GetOcgs();
- doc.LayerUIConfigs();
- doc.SwitchLayer(0);
- }
-
- [Test]
- public void OpenUnicodeDocument()
- {
- Document doc = new Document("../../../resources/DocumentTest/你好.pdf");
- Assert.That(doc.PageCount, Is.EqualTo(1));
- doc.Close();
- }
-
- [Test]
- public void TestRewriteImages()
- {
- // Example for decreasing file size by more than 30%.
- string filePath = "../../../resources/DocumentTest/test-rewrite-images.pdf";
- Document doc = new Document(filePath);
- int size0 = File.ReadAllBytes(filePath).Length;
- doc.RewriteImage(dpiThreshold: 100, dpiTarget: 72, quality: 33);
- byte[] data = doc.Write(garbage: true, deflate: true);
- int size1 = data.Length;
-
- Assert.That((1-(size1/size0)) > 0.3);
- }
-
- [Test]
- public void TestJoinPdfPages()
- {
- string testFilePath1 = Path.GetFullPath(@"../../../resources/DocumentTest/Widget.pdf");
- Document doc1 = new Document(testFilePath1);
- string testFilePath2 = Path.GetFullPath(@"../../../resources/DocumentTest/Color.pdf");
- Document doc2 = new Document(testFilePath2);
-
- doc1.InsertPdf(doc2, 0, 0, 2);
-
- doc1.Save("Joined.pdf", pretty: 1);
-
- Assert.IsTrue(doc1.PageCount == 7);
-
- doc2.Close();
- doc1.Close();
- }
-
- [Test]
- public void TestMoveFile()
- {
- string testFilePath1 = Path.GetFullPath(@"../../../resources/DocumentTest/Widget.pdf");
- string testFilePath2 = Path.GetFullPath(@"TestMoveOrig.pdf");
- string testFilePath3 = Path.GetFullPath(@"TestMoveNew.pdf");
-
- File.Copy(testFilePath1, testFilePath2, true);
-
- Document doc = new Document(testFilePath2);
- Page page = doc[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();
-
- doc.Save(tmp, garbage: 3, deflateFonts: 1, deflate: 1);
- doc.Close();
-
- File.WriteAllBytes(testFilePath2, tmp.ToArray());
-
- tmp.Dispose();
-
- File.Move(testFilePath2, testFilePath3, true);
-
- Document newDoc = new Document(testFilePath3);
- Assert.IsTrue(newDoc.PageCount == 6);
- newDoc.Close();
- }
- }
-}
diff --git a/MuPDF.NET.Test/DocumentTests.cs b/MuPDF.NET.Test/DocumentTests.cs
new file mode 100644
index 00000000..fa6c8e61
--- /dev/null
+++ b/MuPDF.NET.Test/DocumentTests.cs
@@ -0,0 +1,918 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using Xunit;
+
+namespace MuPDF.NET.Test
+{
+ ///
+ /// Tests for the Document class.
+ /// Ported from tests/test_general.py, tests/test_metadata.py.
+ ///
+ public class DocumentTests
+ {
+ // ─── Construction ───────────────────────────────────────────────
+
+ [Fact]
+ public void Document_NewEmptyPdf()
+ {
+ using var doc = new Document();
+ Assert.True(doc.IsPdf);
+ Assert.Equal(0, doc.PageCount);
+ Assert.False(doc.IsClosed);
+ }
+
+ [Fact]
+ public void Document_init_doc_OpenPdf_NoThrow()
+ {
+ using var doc = new Document();
+ doc.init_doc();
+ Assert.False(doc.IsEncrypted);
+ }
+
+ [Fact]
+ public void Document_OpenNonExistent_Throws()
+ {
+ Assert.Throws(() => new Document("nonexistent.pdf"));
+ }
+
+ [Fact]
+ public void Document_OpenEmptyFile_Throws()
+ {
+ var path = Path.GetTempFileName();
+ try
+ {
+ Assert.Throws(() => new Document(path));
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+
+ [Fact]
+ public void Document_OpenEmptyStream_Throws()
+ {
+ Assert.Throws(() => new Document(Array.Empty()));
+ }
+
+ [Fact]
+ public void Document_Close()
+ {
+ var doc = new Document();
+ Assert.False(doc.IsClosed);
+ doc.Close();
+ Assert.True(doc.IsClosed);
+ }
+
+ [Fact]
+ public void Document_CloseTwiceNoError()
+ {
+ var doc = new Document();
+ doc.Close();
+ doc.Close();
+ Assert.True(doc.IsClosed);
+ }
+
+ [Fact]
+ public void Document_AccessAfterClose_Throws()
+ {
+ var doc = new Document();
+ doc.Close();
+ var ex = Assert.Throws(() => _ = doc.PageCount);
+ Assert.Equal("document closed", ex.Message);
+ }
+
+ [Fact]
+ public void Document_DisposeClosesDocument()
+ {
+ var doc = new Document();
+ doc.Dispose();
+ Assert.True(doc.IsClosed);
+ }
+
+ // ─── New page creation ──────────────────────────────────────────
+
+ [Fact]
+ public void Document_NewPage()
+ {
+ using var doc = new Document();
+ var page = doc.NewPage();
+ Assert.Equal(1, doc.PageCount);
+ Assert.Equal(595, page.Width);
+ Assert.Equal(842, page.Height);
+ }
+
+ [Fact]
+ public void Document_NewPageCustomSize()
+ {
+ using var doc = new Document();
+ var page = doc.NewPage(width: 200, height: 300);
+ Assert.True(TestHelper.IsClose(200, page.Width));
+ Assert.True(TestHelper.IsClose(300, page.Height));
+ }
+
+ [Fact]
+ public void Document_NewPage_WhenClosed_ThrowsLikePython()
+ {
+ var doc = new Document();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.NewPage());
+ Assert.Equal("document closed or encrypted", ex.Message);
+ }
+
+ [Fact]
+ public void Document_NewPage_InsertBeforeLessThanMinusOne_ThrowsLikePython()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ Assert.Throws(() => doc.NewPage(-2));
+ }
+
+ [Fact]
+ public void Document_MultiplePages()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ doc.NewPage();
+ doc.NewPage();
+ Assert.Equal(3, doc.PageCount);
+ }
+
+ // ─── Page loading ───────────────────────────────────────────────
+
+ [Fact]
+ public void Document_LoadPage()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var page = doc.LoadPage(0);
+ Assert.NotNull(page);
+ Assert.Equal(0, page.Number);
+ }
+
+ [Fact]
+ public void Document_LoadPageNegativeIndex()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ doc.NewPage();
+ var page = doc.LoadPage(-1);
+ Assert.Equal(1, page.Number);
+ }
+
+ [Fact]
+ public void Document_LoadPageOutOfRange_Throws()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var ex = Assert.Throws(() => doc.LoadPage(5));
+ Assert.Equal("page not in document", ex.Message);
+ }
+
+ [Fact]
+ public void Document_LoadPage_WhenClosed_ThrowsLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.LoadPage(0));
+ Assert.Equal("document closed or encrypted", ex.Message);
+ }
+
+ [Fact]
+ public void Document_Indexer()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var page = doc[0];
+ Assert.NotNull(page);
+ }
+
+ [Fact]
+ public void Document_Indexer_OutOfRange_ThrowsIndexErrorLikePython()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var ex = Assert.Throws(() => _ = doc[5]);
+ Assert.Contains("page 5", ex.Message, StringComparison.Ordinal);
+ Assert.Contains("not in document", ex.Message, StringComparison.Ordinal);
+ }
+
+ // ─── Page enumeration ───────────────────────────────────────────
+
+ [Fact]
+ public void Document_Enumerate()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ doc.NewPage();
+ int count = 0;
+ foreach (var page in doc)
+ count++;
+ Assert.Equal(2, count);
+ }
+
+ [Fact]
+ public void Document_PagesGenerator()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ doc.NewPage();
+ doc.NewPage();
+ int count = 0;
+ foreach (var page in doc.Pages(start: 0, stop: 2))
+ count++;
+ Assert.Equal(2, count);
+ }
+
+ // ─── Properties ─────────────────────────────────────────────────
+
+ [Fact]
+ public void Document_IsPdf()
+ {
+ using var doc = new Document();
+ Assert.True(doc.IsPdf);
+ }
+
+ /// Matches PyMuPDF tests/test_general.py::test_isdirty (opened file, not a fresh in-memory PDF).
+ [Fact]
+ public void Document_IsDirty_FalseOnOpenPdf()
+ {
+ var path = TestHelper.GetResource("test_4043.pdf");
+ Assert.True(File.Exists(path), $"missing test PDF: {path}");
+ using var doc = new Document(path);
+ Assert.False(doc.IsDirty);
+ }
+
+ [Fact]
+ public void Document_NeedsPass_FalseOnNew()
+ {
+ using var doc = new Document();
+ Assert.False(doc.NeedsPass);
+ }
+
+ [Fact]
+ public void Document_IsReflowable_FalseForPdf()
+ {
+ using var doc = new Document();
+ Assert.False(doc.IsReflowable);
+ }
+
+ [Fact]
+ public void Document_ChapterCount()
+ {
+ using var doc = new Document();
+ Assert.True(doc.ChapterCount >= 1);
+ }
+
+ [Fact]
+ public void Document_ContainsPage()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ Assert.True(doc.ContainsPage(0));
+ Assert.False(doc.ContainsPage(5));
+ }
+
+ // ─── Metadata ───────────────────────────────────────────────────
+
+ [Fact]
+ public void Document_GetMetadata()
+ {
+ using var doc = new Document();
+ var meta = doc.GetMetadata();
+ Assert.NotNull(meta);
+ Assert.True(meta.ContainsKey("format"));
+ }
+
+ [Fact]
+ public void Document_SetMetadata()
+ {
+ using var doc = new Document();
+ doc.SetMetadata(new Dictionary
+ {
+ ["title"] = "Test Title",
+ ["author"] = "Test Author"
+ });
+ var meta = doc.GetMetadata();
+ Assert.Contains("Test Title", meta.GetValueOrDefault("title", ""));
+ }
+
+ [Fact]
+ public void Document_SetMetadata_WhenClosed_ThrowsLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() =>
+ doc.SetMetadata(new Dictionary { ["title"] = "x" }));
+ Assert.Equal("document closed or encrypted", ex.Message);
+ }
+
+ // ─── Table of Contents ──────────────────────────────────────────
+
+ [Fact]
+ public void Document_GetToc_EmptyOnNew()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var toc = doc.GetToc();
+ Assert.Empty(toc);
+ }
+
+ // ─── Page Operations ────────────────────────────────────────────
+
+ [Fact]
+ public void Document_DeletePagesBySlice_WhenClosed_ThrowsLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.DeletePagesBySlice(0, 1));
+ Assert.Equal("document closed", ex.Message);
+ }
+
+ [Fact]
+ public void Document_DeletePage()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ doc.NewPage();
+ Assert.Equal(2, doc.PageCount);
+ doc.DeletePage(0);
+ Assert.Equal(1, doc.PageCount);
+ }
+
+ [Fact]
+ public void Document_DeletePage_WhenClosed_ThrowsDocumentClosedLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.DeletePage(0));
+ Assert.Equal("document closed", ex.Message);
+ }
+
+ [Fact]
+ public void Document_FullcopyPage_WhenClosed_ThrowsDocumentClosedLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.FullcopyPage(0));
+ Assert.Equal("document closed", ex.Message);
+ }
+
+ [Fact]
+ public void Document_CopyPage()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ Assert.Equal(1, doc.PageCount);
+ doc.CopyPage(0);
+ Assert.Equal(2, doc.PageCount);
+ }
+
+ [Fact]
+ public void Document_MovePage()
+ {
+ using var doc = new Document();
+ doc.NewPage(width: 100, height: 100);
+ doc.NewPage(width: 200, height: 200);
+ doc.MovePage(0, 2);
+ Assert.Equal(2, doc.PageCount);
+ }
+
+ [Fact]
+ public void Document_CopyPage_WhenClosed_ThrowsDocumentClosedLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.CopyPage(0));
+ Assert.Equal("document closed", ex.Message);
+ }
+
+ [Fact]
+ public void Document_MovePage_WhenClosed_ThrowsDocumentClosedLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.MovePage(0, -1));
+ Assert.Equal("document closed", ex.Message);
+ }
+
+ // ─── Write / ToBytes ────────────────────────────────────────────
+
+ [Fact]
+ public void Document_Write()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var bytes = doc.Write();
+ Assert.NotNull(bytes);
+ Assert.True(bytes.Length > 0);
+ }
+
+ [Fact]
+ public void Document_ToBytes()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var bytes = doc.ToBytes();
+ Assert.True(bytes.Length > 0);
+ }
+
+ [Fact]
+ public void Document_Save()
+ {
+ var tmpDir = Path.GetTempPath();
+ var path = Path.Combine(tmpDir, $"mupdfnet_test_{Guid.NewGuid()}.pdf");
+ try
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ doc.Save(path);
+ Assert.True(File.Exists(path));
+ Assert.True(new FileInfo(path).Length > 0);
+ }
+ finally
+ {
+ if (File.Exists(path)) File.Delete(path);
+ }
+ }
+
+ // ─── Xref ───────────────────────────────────────────────────────
+
+ [Fact]
+ public void Document_XrefLength()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ Assert.True(doc.XrefLength > 0);
+ }
+
+ [Fact]
+ public void Document_PageXref()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ int xref = doc.PageXref(0);
+ Assert.True(xref > 0);
+ }
+
+ /// PyMuPDF xref_object uses pdf_print_obj; binding pdf_to_str_buf was empty for many dicts.
+ [Fact]
+ public void Document_XrefObject_PageDictNonEmpty()
+ {
+ using var doc = new Document();
+ doc.NewPage(595, 842);
+ int xref = doc.PageXref(0);
+ var text = doc.XrefObject(xref);
+ Assert.False(string.IsNullOrWhiteSpace(text));
+ Assert.Contains("/Type", text, StringComparison.Ordinal);
+ Assert.Contains("/Page", text, StringComparison.Ordinal);
+ Assert.Contains("MediaBox", text, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Document_XrefObject_ContainsCropBoxAfterSet()
+ {
+ using var doc = new Document();
+ doc.NewPage(595, 842);
+ doc[0].SetCropBox(new Rect(100, 200, 400, 700));
+ int xref = doc.PageXref(0);
+ var text = doc.XrefObject(xref);
+ Assert.Contains("/CropBox", text, StringComparison.Ordinal);
+ // pdf_print_obj (compress=false) may add spaces, e.g. "[ 100 200 400 700 ]".
+ var compact = Regex.Replace(text, @"\s+", "");
+ Assert.Contains("100200400700", compact);
+ }
+
+ [Fact]
+ public void Document_PdfTrailer_NonEmpty()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var t = doc.PdfTrailer();
+ Assert.False(string.IsNullOrWhiteSpace(t));
+ Assert.Contains("Root", t, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Document_XrefObject_AfterClose_Throws()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ int xref = doc.PageXref(0);
+ doc.Close();
+ var ex = Assert.Throws(() => _ = doc.XrefObject(xref));
+ Assert.Equal("document closed", ex.Message);
+ }
+
+ [Fact]
+ public void Document_XrefGetKey_XrefZero_Throws()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ Assert.Throws(() => doc.XrefGetKey(0, "Type"));
+ }
+
+ [Fact]
+ public void Document_XrefGetKey_OutOfRange_Throws()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ int bad = doc.XrefLength + 50;
+ Assert.Throws(() => doc.XrefGetKey(bad, "Type"));
+ }
+
+ [Fact]
+ public void Document_XrefGetKeys_TrailerMinusOne()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var keys = doc.XrefGetKeys(-1);
+ Assert.NotNull(keys);
+ Assert.Contains("Root", keys);
+ }
+
+ [Fact]
+ public void Document_XrefStream_XrefZero_Throws()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ Assert.Throws(() => doc.XrefStream(0));
+ }
+
+ [Fact]
+ public void Document_XrefStream_TrailerMinusOne_NoThrow()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ // Trailer is usually not a stream; PyMuPDF returns None.
+ _ = doc.XrefStream(-1);
+ }
+
+ [Fact]
+ public void Document_XrefStreamRaw_XrefZero_Throws()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ Assert.Throws(() => doc.XrefStreamRaw(0));
+ }
+
+ [Fact]
+ public void Document_UpdateObject_XrefZero_Throws()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ Assert.Throws(() => doc.UpdateObject(0, "<<>>"));
+ }
+
+ [Fact]
+ public void Document_UpdateStream_XrefZero_Throws()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ Assert.Throws(() => doc.UpdateStream(0, new byte[] { 1, 2, 3 }));
+ }
+
+ [Fact]
+ public void Document_UpdateStream_NonDictXref_Throws()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ int xref = doc.GetNewXref();
+ doc.UpdateObject(xref, "42");
+ var ex = Assert.Throws(() => doc.UpdateStream(xref, new byte[] { 1, 2, 3 }));
+ Assert.Contains("dict", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Document_update_stream_FourArgPythonOverload_IgnoresNewUsesCompressInt()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ int xref = doc.GetNewXref();
+ doc.UpdateObject(xref, "<<>>");
+ doc.update_stream(xref, new byte[] { 0xAB, 0xCD, 0xEF }, new_: 999, compress: 0);
+ var raw = doc.XrefStreamRaw(xref);
+ Assert.NotNull(raw);
+ }
+
+ [Fact]
+ public void Document_XrefSetKey_NullKeyword_RemovesDictEntry()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ int xref = doc.GetNewXref();
+ doc.UpdateObject(xref, "<< /Y 1 >>");
+ doc.XrefSetKey(xref, "Y", "null");
+ Assert.DoesNotContain("Y", doc.XrefGetKeys(xref));
+ var y = doc.XrefGetKey(xref, "Y");
+ Assert.Equal("null", y.type);
+ Assert.Equal("null", y.value);
+ }
+
+ [Fact]
+ public void Document_XrefCopy_CopiesDictKeysAndRespectsKeep()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ int a = doc.GetNewXref();
+ int b = doc.GetNewXref();
+ doc.UpdateObject(a, "<< /K1 1 /K2 2 >>");
+ doc.UpdateObject(b, "<< /K2 99 /K3 3 >>");
+ doc.XrefCopy(a, b, new[] { "K3" });
+ Assert.Equal("int", doc.XrefGetKey(b, "K1").type);
+ Assert.Equal("1", doc.XrefGetKey(b, "K1").value);
+ Assert.Equal("2", doc.XrefGetKey(b, "K2").value);
+ Assert.Equal("3", doc.XrefGetKey(b, "K3").value);
+ }
+
+ [Fact]
+ public void Document_XrefCopy_StaticOverload_MatchesInstance()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ int a = doc.GetNewXref();
+ int b = doc.GetNewXref();
+ doc.UpdateObject(a, "<< /X 42 >>");
+ doc.UpdateObject(b, "<< /Y 1 >>");
+ Document.XrefCopy(doc, a, b);
+ Assert.Equal("42", doc.XrefGetKey(b, "X").value);
+ Assert.Equal("null", doc.XrefGetKey(b, "Y").type);
+ }
+
+ [Fact]
+ public void Document_xref_copy_PythonCompat()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ int a = doc.GetNewXref();
+ int b = doc.GetNewXref();
+ doc.UpdateObject(a, "<< /P 7 >>");
+ doc.UpdateObject(b, "<< /Q 8 >>");
+ Document.xref_copy(doc, a, b, new List { "Q" });
+ Assert.Equal("7", doc.XrefGetKey(b, "P").value);
+ Assert.Equal("8", doc.XrefGetKey(b, "Q").value);
+ }
+
+ [Fact]
+ public void Document_GetNewXref()
+ {
+ using var doc = new Document();
+ int xref = doc.GetNewXref();
+ Assert.True(xref > 0);
+ }
+
+ // ─── ConvertToPdf ───────────────────────────────────────────────
+
+ [Fact]
+ public void Document_ConvertToPdf()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var pdfBytes = doc.ConvertToPdf();
+ Assert.True(pdfBytes.Length > 0);
+ }
+
+ // ─── Journal ────────────────────────────────────────────────────
+
+ [Fact]
+ public void Document_JournalEnable()
+ {
+ using var doc = new Document();
+ doc.JournalEnable();
+ Assert.True(doc.JournalIsEnabled);
+ }
+
+ [Fact]
+ public void Document_Journal_WhenClosed_ThrowsValueErrorLikePython()
+ {
+ var doc = new Document();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.JournalEnable());
+ Assert.Equal("document closed or encrypted", ex.Message);
+ var ex2 = Assert.Throws(() => doc.JournalCanDo());
+ Assert.Equal("document closed or encrypted", ex2.Message);
+ }
+
+ [Fact]
+ public void Document_MakeBookmark_WhenClosed_ThrowsValueErrorLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.MakeBookmark((0, 0)));
+ Assert.Equal("document closed or encrypted", ex.Message);
+ }
+
+ [Fact]
+ public void Document_FindBookmark_WhenClosed_ThrowsValueErrorLikePython()
+ {
+ var doc = new Document();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.FindBookmark(0UL));
+ Assert.Equal("document closed or encrypted", ex.Message);
+ }
+
+ // ─── Select / scrub (Python ValueError messages) ────────────────
+
+ [Fact]
+ public void Document_Select_WhenClosed_ThrowsCombinedMessageLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.Select(new[] { 0 }));
+ Assert.Equal("document closed or encrypted", ex.Message);
+ }
+
+ [Fact]
+ public void Document_Select_Null_ThrowsSequenceRequiredLikePython()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var ex = Assert.Throws(() => doc.Select(null!));
+ Assert.Equal("sequence required", ex.Message);
+ }
+
+ [Fact]
+ public void Document_Select_Empty_ThrowsBadPageNumbersLikePython()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var ex = Assert.Throws(() => doc.Select(Array.Empty()));
+ Assert.Equal("bad page number(s)", ex.Message);
+ }
+
+ [Fact]
+ public void Document_Select_OutOfRange_ThrowsBadPageNumbersLikePython()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var ex = Assert.Throws(() => doc.Select(new[] { 5 }));
+ Assert.Equal("bad page number(s)", ex.Message);
+ }
+
+ [Fact]
+ public void Document_Select_ReordersPages()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ doc.NewPage();
+ doc.NewPage();
+ doc.Select(new[] { 2, 0, 1 });
+ Assert.Equal(3, doc.PageCount);
+ }
+
+ [Fact]
+ public void Document_Scrub_WhenClosed_ThrowsValueErrorLikePython()
+ {
+ var doc = new Document();
+ doc.NewPage();
+ doc.Close();
+ var ex = Assert.Throws(() => doc.Scrub());
+ Assert.Equal("closed or encrypted doc", ex.Message);
+ }
+
+ [Fact]
+ public void Document_Scrub_MinimalPdf_NoThrow()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ doc.Scrub();
+ Assert.Equal(1, doc.PageCount);
+ }
+
+ // ─── TOC (set_toc / set_toc_item ValueError parity) ─────────────
+
+ [Fact]
+ public void Document_SetToc_FirstLevelNot1_ThrowsValueErrorLikePython()
+ {
+ using var doc = new Document();
+ doc.NewPage();
+ var toc = new List