using UglyToad.PdfPig; namespace MmdPdf.Services { internal sealed class SearchResult { public Dictionary> PageRects { get; } = []; public List ResultPages { get; } = []; public int TotalHits { get; set; } } internal sealed class SearchService { /// /// Scans every page of for (case-insensitive). /// Returns an empty result when query is blank or the file cannot be opened. /// 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; } } }