namespace Poe2Trade.Screen; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using Serilog; using Region = Poe2Trade.Core.Region; class DiffCropHandler { private Bitmap? _referenceFrame; private Region? _referenceRegion; private readonly object _refLock = new(); public void HandleSnapshot(string? file = null, Region? region = null) { var newFrame = ScreenCapture.CaptureOrLoad(file, region); lock (_refLock) { _referenceFrame?.Dispose(); _referenceFrame = newFrame; _referenceRegion = region; } } public void HandleScreenshot(string path, Region? region = null) { Bitmap? refCopy; lock (_refLock) { refCopy = _referenceFrame != null ? (Bitmap)_referenceFrame.Clone() : null; } var bitmap = refCopy ?? ScreenCapture.CaptureOrLoad(null, region); var format = ImageUtils.GetImageFormat(path); var dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); bitmap.Save(path, format); bitmap.Dispose(); } public byte[] HandleCapture(Region? region = null) { using var bitmap = ScreenCapture.CaptureOrLoad(null, region); using var ms = new MemoryStream(); bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png); return ms.ToArray(); } /// /// Diff detection + crop only. Returns the raw tooltip crop bitmap and region, /// or null if no tooltip detected. Caller is responsible for disposing the bitmaps. /// public (Bitmap cropped, Bitmap refCropped, Bitmap current, Region region)? DiffCrop( DiffCropParams c, string? file = null, Region? region = null) { Bitmap refSnapshot; Region? refRegion; lock (_refLock) { if (_referenceFrame == null) return null; refSnapshot = (Bitmap)_referenceFrame.Clone(); refRegion = _referenceRegion; } var diffRegion = region ?? refRegion; int baseX = diffRegion?.X ?? 0; int baseY = diffRegion?.Y ?? 0; var current = ScreenCapture.CaptureOrLoad(file, diffRegion); Bitmap refForDiff = refSnapshot; if (diffRegion != null) { if (refRegion == null) { var croppedRef = CropBitmap(refSnapshot, diffRegion); if (croppedRef == null) { current.Dispose(); refSnapshot.Dispose(); return null; } refForDiff = croppedRef; } else if (!RegionsEqual(diffRegion, refRegion)) { int offX = diffRegion.X - refRegion.X; int offY = diffRegion.Y - refRegion.Y; if (offX < 0 || offY < 0 || offX + diffRegion.Width > refSnapshot.Width || offY + diffRegion.Height > refSnapshot.Height) { current.Dispose(); refSnapshot.Dispose(); return null; } var croppedRef = CropBitmap(refSnapshot, new Region(offX, offY, diffRegion.Width, diffRegion.Height)); if (croppedRef == null) { current.Dispose(); refSnapshot.Dispose(); return null; } refForDiff = croppedRef; } } int w = Math.Min(refForDiff.Width, current.Width); int h = Math.Min(refForDiff.Height, current.Height); var refData = refForDiff.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); byte[] refPx = new byte[refData.Stride * h]; Marshal.Copy(refData.Scan0, refPx, 0, refPx.Length); refForDiff.UnlockBits(refData); int stride = refData.Stride; var curData = current.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); byte[] curPx = new byte[curData.Stride * h]; Marshal.Copy(curData.Scan0, curPx, 0, curPx.Length); current.UnlockBits(curData); int diffThresh = c.DiffThresh; // Pass 1: parallel row diff int[] rowCounts = new int[h]; Parallel.For(0, h, y => { int count = 0; int rowOffset = y * stride; for (int x = 0; x < w; x++) { int i = rowOffset + x * 4; int darker = (refPx[i] - curPx[i]) + (refPx[i + 1] - curPx[i + 1]) + (refPx[i + 2] - curPx[i + 2]); if (darker > diffThresh) count++; } rowCounts[y] = count; }); int totalChanged = 0; for (int y = 0; y < h; y++) totalChanged += rowCounts[y]; if (totalChanged == 0) { current.Dispose(); if (refForDiff != refSnapshot) refForDiff.Dispose(); refSnapshot.Dispose(); return null; } int maxGap = c.MaxGap; int rowThresh = w / c.RowThreshDiv; int bestRowStart = 0, bestRowEnd = 0, bestRowLen = 0; int curRowStart = -1, lastActiveRow = -1; for (int y = 0; y < h; y++) { if (rowCounts[y] >= rowThresh) { if (curRowStart < 0) curRowStart = y; lastActiveRow = y; } else if (curRowStart >= 0 && y - lastActiveRow > maxGap) { int len = lastActiveRow - curRowStart + 1; if (len > bestRowLen) { bestRowStart = curRowStart; bestRowEnd = lastActiveRow; bestRowLen = len; } curRowStart = -1; } } if (curRowStart >= 0) { int len = lastActiveRow - curRowStart + 1; if (len > bestRowLen) { bestRowStart = curRowStart; bestRowEnd = lastActiveRow; bestRowLen = len; } } // Pass 2: parallel column diff int[] colCounts = new int[w]; int rowRangeLen = bestRowEnd - bestRowStart + 1; if (rowRangeLen <= 200) { for (int y = bestRowStart; y <= bestRowEnd; y++) { int rowOffset = y * stride; for (int x = 0; x < w; x++) { int i = rowOffset + x * 4; int darker = (refPx[i] - curPx[i]) + (refPx[i + 1] - curPx[i + 1]) + (refPx[i + 2] - curPx[i + 2]); if (darker > diffThresh) colCounts[x]++; } } } else { Parallel.For(bestRowStart, bestRowEnd + 1, () => new int[w], (y, _, localCols) => { int rowOffset = y * stride; for (int x = 0; x < w; x++) { int i = rowOffset + x * 4; int darker = (refPx[i] - curPx[i]) + (refPx[i + 1] - curPx[i + 1]) + (refPx[i + 2] - curPx[i + 2]); if (darker > diffThresh) localCols[x]++; } return localCols; }, localCols => { for (int x = 0; x < w; x++) Interlocked.Add(ref colCounts[x], localCols[x]); }); } int tooltipHeight = bestRowEnd - bestRowStart + 1; int colThresh = tooltipHeight / c.ColThreshDiv; int bestColStart = 0, bestColEnd = 0, bestColLen = 0; int curColStart = -1, lastActiveCol = -1; for (int x = 0; x < w; x++) { if (colCounts[x] >= colThresh) { if (curColStart < 0) curColStart = x; lastActiveCol = x; } else if (curColStart >= 0 && x - lastActiveCol > maxGap) { int len = lastActiveCol - curColStart + 1; if (len > bestColLen) { bestColStart = curColStart; bestColEnd = lastActiveCol; bestColLen = len; } curColStart = -1; } } if (curColStart >= 0) { int len = lastActiveCol - curColStart + 1; if (len > bestColLen) { bestColStart = curColStart; bestColEnd = lastActiveCol; bestColLen = len; } } Log.Debug("diff-crop: changed={Changed} rows={RowStart}-{RowEnd}({RowLen}) cols={ColStart}-{ColEnd}({ColLen})", totalChanged, bestRowStart, bestRowEnd, bestRowLen, bestColStart, bestColEnd, bestColLen); if (bestRowLen < 50 || bestColLen < 50) { Log.Debug("diff-crop: no tooltip-sized region found"); current.Dispose(); if (refForDiff != refSnapshot) refForDiff.Dispose(); refSnapshot.Dispose(); return null; } int minX = bestColStart; int minY = bestRowStart; int maxX = Math.Min(bestColEnd, w - 1); int maxY = Math.Min(bestRowEnd, h - 1); // Boundary extension int extRowThresh = Math.Max(1, rowThresh / 4); int extColThresh = Math.Max(1, colThresh / 4); int extTop = Math.Max(0, minY - maxGap); for (int y = minY - 1; y >= extTop; y--) { if (rowCounts[y] >= extRowThresh) minY = y; else break; } int extBottom = Math.Min(h - 1, maxY + maxGap); for (int y = maxY + 1; y <= extBottom; y++) { if (rowCounts[y] >= extRowThresh) maxY = y; else break; } int extLeft = Math.Max(0, minX - maxGap); for (int x = minX - 1; x >= extLeft; x--) { if (colCounts[x] >= extColThresh) minX = x; else break; } int extRight = Math.Min(w - 1, maxX + maxGap); for (int x = maxX + 1; x <= extRight; x++) { if (colCounts[x] >= extColThresh) maxX = x; else break; } // Trim low-density edges int colSpan = maxX - minX + 1; if (colSpan > 50) { int q1 = minX + colSpan / 4; int q3 = minX + colSpan * 3 / 4; long midSum = 0; int midCount = 0; for (int x = q1; x <= q3; x++) { midSum += colCounts[x]; midCount++; } double avgMidDensity = (double)midSum / Math.Max(1, midCount); double cutoff = avgMidDensity * c.TrimCutoff; while (minX < maxX - 50 && colCounts[minX] < cutoff) minX++; while (maxX > minX + 50 && colCounts[maxX] < cutoff) maxX--; } int rowSpan = maxY - minY + 1; if (rowSpan > 50) { int q1 = minY + rowSpan / 4; int q3 = minY + rowSpan * 3 / 4; long midSum = 0; int midCount = 0; for (int y = q1; y <= q3; y++) { midSum += rowCounts[y]; midCount++; } double avgMidDensity = (double)midSum / Math.Max(1, midCount); double cutoff = avgMidDensity * c.TrimCutoff; while (minY < maxY - 50 && rowCounts[minY] < cutoff) minY++; while (maxY > minY + 50 && rowCounts[maxY] < cutoff) maxY--; } int rw = maxX - minX + 1; int rh = maxY - minY + 1; var cropped = CropFromBytes(curPx, stride, minX, minY, rw, rh); var refCropped = CropFromBytes(refPx, stride, minX, minY, rw, rh); var resultRegion = new Region(baseX + minX, baseY + minY, rw, rh); Log.Debug("diff-crop: tooltip region ({X},{Y}) {W}x{H}", minX, minY, rw, rh); if (refForDiff != refSnapshot) refForDiff.Dispose(); refSnapshot.Dispose(); return (cropped, refCropped, current, resultRegion); } private static bool RegionsEqual(Region a, Region b) => a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height; private static Bitmap? CropBitmap(Bitmap src, Region region) { int cx = Math.Max(0, region.X); int cy = Math.Max(0, region.Y); int cw = Math.Min(region.Width, src.Width - cx); int ch = Math.Min(region.Height, src.Height - cy); if (cw <= 0 || ch <= 0) return null; return src.Clone(new Rectangle(cx, cy, cw, ch), PixelFormat.Format32bppArgb); } /// /// Fast crop from raw pixel bytes. /// private static Bitmap CropFromBytes(byte[] px, int srcStride, int cropX, int cropY, int cropW, int cropH) { var bmp = new Bitmap(cropW, cropH, PixelFormat.Format32bppArgb); var data = bmp.LockBits(new Rectangle(0, 0, cropW, cropH), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); int dstStride = data.Stride; int rowBytes = cropW * 4; for (int y = 0; y < cropH; y++) { int srcOffset = (cropY + y) * srcStride + cropX * 4; Marshal.Copy(px, srcOffset, data.Scan0 + y * dstStride, rowBytes); } bmp.UnlockBits(data); return bmp; } public static double LevenshteinSimilarity(string a, string b) { a = a.ToLowerInvariant(); b = b.ToLowerInvariant(); if (a == b) return 1.0; int la = a.Length, lb = b.Length; if (la == 0 || lb == 0) return 0.0; var d = new int[la + 1, lb + 1]; for (int i = 0; i <= la; i++) d[i, 0] = i; for (int j = 0; j <= lb; j++) d[0, j] = j; for (int i = 1; i <= la; i++) for (int j = 1; j <= lb; j++) { int cost = a[i - 1] == b[j - 1] ? 0 : 1; d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } return 1.0 - (double)d[la, lb] / Math.Max(la, lb); } }