- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc. Fixes edited values staying recoverable by text extraction. - Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode. - Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon. - Remove the upstream author credit and the in-app install button.
92 lines
3.6 KiB
C#
92 lines
3.6 KiB
C#
using UglyToad.PdfPig;
|
|
|
|
namespace MmdPdf.Services
|
|
{
|
|
internal sealed class SearchResult
|
|
{
|
|
public Dictionary<int, List<(double Left, double Bottom, double Right, double Top)>> PageRects { get; } = [];
|
|
public List<int> ResultPages { get; } = [];
|
|
public int TotalHits { get; set; }
|
|
}
|
|
|
|
internal sealed class SearchService
|
|
{
|
|
/// <summary>
|
|
/// Scans every page of <paramref name="filePath"/> for <paramref name="query"/> (case-insensitive).
|
|
/// Returns an empty result when query is blank or the file cannot be opened.
|
|
/// </summary>
|
|
public SearchResult Search(string filePath, string query)
|
|
{
|
|
var result = new SearchResult();
|
|
if (string.IsNullOrWhiteSpace(query) || string.IsNullOrWhiteSpace(filePath))
|
|
return result;
|
|
|
|
try
|
|
{
|
|
using var doc = PdfDocument.Open(filePath);
|
|
for (int pi = 0; pi < doc.NumberOfPages; pi++)
|
|
{
|
|
var page = doc.GetPage(pi + 1);
|
|
var hits = FindMatchesOnPage(page, query);
|
|
if (hits.Count > 0)
|
|
{
|
|
result.PageRects[pi] = hits;
|
|
result.ResultPages.Add(pi);
|
|
result.TotalHits += hits.Count;
|
|
}
|
|
}
|
|
}
|
|
catch { /* return whatever was collected so far */ }
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static List<(double Left, double Bottom, double Right, double Top)> FindMatchesOnPage(
|
|
UglyToad.PdfPig.Content.Page page, string query)
|
|
{
|
|
var result = new List<(double, double, double, double)>();
|
|
var words = page.GetWords().ToList();
|
|
|
|
// A multi-word phrase can span adjacent words; a single token never does, so for single-token
|
|
// queries we only do the per-word substring match (the cross-word union box below would
|
|
// otherwise highlight whole runs of words leading up to the matching one).
|
|
bool isPhrase = query.Trim().IndexOf(' ') >= 0;
|
|
|
|
for (int i = 0; i < words.Count; i++)
|
|
{
|
|
if (words[i].Text.IndexOf(query, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
var bb = words[i].BoundingBox;
|
|
result.Add((bb.Left, bb.Bottom, bb.Right, bb.Top));
|
|
continue;
|
|
}
|
|
|
|
if (!isPhrase) continue;
|
|
|
|
// Multi-word match
|
|
string combined = words[i].Text;
|
|
for (int j = i + 1; j < words.Count && combined.Length < query.Length + 20; j++)
|
|
{
|
|
combined += " " + words[j].Text;
|
|
if (combined.IndexOf(query, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
double minX = double.MaxValue, minY = double.MaxValue;
|
|
double maxX = double.MinValue, maxY = double.MinValue;
|
|
for (int k = i; k <= j; k++)
|
|
{
|
|
var wbb = words[k].BoundingBox;
|
|
minX = Math.Min(minX, wbb.Left);
|
|
minY = Math.Min(minY, wbb.Bottom);
|
|
maxX = Math.Max(maxX, wbb.Right);
|
|
maxY = Math.Max(maxY, wbb.Top);
|
|
}
|
|
result.Add((minX, minY, maxX, maxY));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|