Files
kua-agent 6610acfa44 feat: rebrand to MMD PDF, single corporate theme, and remove original text under an edit
- 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.
2026-08-27 07:37:09 +02:00

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;
}
}
}