Skip to content

Commit f3d2a2d

Browse files
committed
Comparer: pink-dot grid overlay for pixel-level diffs (#13)
The image area in each pane is divided into a fixed-size grid in display pixels. Every cell that contains at least one pixel that differs from the reference pane is highlighted with a translucent pink rectangle outline plus a center dot, drawn with GDI+ alpha blending so the underlying image stays visible. Because the cell size is fixed in display pixels, zooming in implicitly subdivides the source region each cell covers — at maximum zoom each dot resolves to a single differing source pixel. The overlay is hidden automatically once zoom is high enough to render per-pixel value labels, since the labels already convey the diff. 'D' toggles the overlay on and off (default on); '?' shows or hides the new Comparer shortcut panel. docs/USER_GUIDE.md gains a Comparer controls table and a mention of the diff overlay; the Viewer table now also lists the 'M' mute shortcut. Per-row red diff lines in PosInfoView are removed (the grid overlay covers the same information). The timeline is hidden when neither pane is a video; frame picking is still available for video sources.
1 parent ff90ffa commit f3d2a2d

8 files changed

Lines changed: 223 additions & 28 deletions

File tree

Comparer/Comparer.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
#include "ComparerDoc.h"
1111

12+
#include <gdiplus.h>
13+
#pragma comment(lib, "gdiplus.lib")
14+
1215
#ifdef _DEBUG
1316
#define new DEBUG_NEW
1417
#endif
@@ -62,6 +65,9 @@ BOOL CComparerApp::InitInstance()
6265
{
6366
CWinApp::InitInstance();
6467

68+
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
69+
Gdiplus::GdiplusStartup(&mGdiplusToken, &gdiplusStartupInput, NULL);
70+
6571
SetRegistryKey(_T("Chammoru"));
6672
LoadStdProfileSettings(4); // Load standard INI file options (including MRU)
6773

@@ -108,6 +114,12 @@ BOOL CComparerApp::InitInstance()
108114
return TRUE;
109115
}
110116

117+
int CComparerApp::ExitInstance()
118+
{
119+
Gdiplus::GdiplusShutdown(mGdiplusToken);
120+
return CWinApp::ExitInstance();
121+
}
122+
111123

112124

113125
// CAboutDlg dialog used for App About

Comparer/Comparer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@ class CComparerApp : public CWinApp
2424
// Overrides
2525
public:
2626
virtual BOOL InitInstance();
27+
virtual int ExitInstance();
2728

2829
// Implementation
2930
afx_msg void OnAppAbout();
3031
DECLARE_MESSAGE_MAP()
3132
afx_msg void OnFileOpen();
33+
34+
private:
35+
ULONG_PTR mGdiplusToken;
3236
};
3337

3438
extern CComparerApp theApp;

Comparer/ComparerDoc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ CComparerDoc::CComparerDoc()
6464
, mFps(COMPARER_DEF_FPS)
6565
, mInterpol(false)
6666
, mDiffRes(false)
67+
, mDiffOverlay(true)
6768
{
6869
BITMAPINFOHEADER &bmiHeader = mBmi.bmiHeader;
6970
bmiHeader.biSize = (DWORD)sizeof(BITMAPINFOHEADER);

Comparer/ComparerDoc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class CComparerDoc : public CDocument
7979
double mFps;
8080
bool mInterpol;
8181
bool mDiffRes;
82+
bool mDiffOverlay;
8283

8384
// Operations
8485
public:

Comparer/ComparerView.cpp

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
#include <QViewerCmn.h>
1717
#include <QImageStr.h>
1818

19+
#include <gdiplus.h>
20+
1921
// CComparerView
2022

2123
CComparerView::CComparerView()
@@ -27,6 +29,7 @@ CComparerView::CComparerView()
2729
, mHCanvas(0)
2830
, mIsClicked(false)
2931
, mProcessing(false)
32+
, mShowHelp(false)
3033
, mRgbBufSize(0)
3134
, mRgbBuf(NULL)
3235
{
@@ -242,11 +245,161 @@ void CComparerView::OnDraw(CDC *pDC)
242245
}
243246
}
244247

248+
DrawDiffOverlay(&memDC, pDoc, pane);
249+
250+
if (mShowHelp)
251+
DrawHelpMenu(&memDC);
252+
245253
pDC->BitBlt(0, mRcControls.bottom, mWCanvas, mHCanvas, &memDC, 0, 0, SRCCOPY);
246254

247255
mProcessing = false;
248256
}
249257

258+
void CComparerView::ToggleHelp()
259+
{
260+
mShowHelp = !mShowHelp;
261+
Invalidate(FALSE);
262+
}
263+
264+
void CComparerView::DrawHelpMenu(CDC *pDC)
265+
{
266+
const int W_HELP = 460;
267+
const int H_HELP = 280;
268+
const int W_MARGIN = 18;
269+
const int H_MARGIN = 14;
270+
const int X_HELP = (mWCanvas - W_HELP) / 2;
271+
const int Y_HELP = (mHCanvas - H_HELP) / 2;
272+
CRect bgRect(X_HELP, Y_HELP, X_HELP + W_HELP, Y_HELP + H_HELP);
273+
pDC->FillSolidRect(bgRect, Q1UI_COLOR_SURFACE);
274+
CPen borderPen(PS_SOLID, 1, Q1UI_COLOR_BORDER);
275+
CPen *prevPen = pDC->SelectObject(&borderPen);
276+
pDC->SelectStockObject(NULL_BRUSH);
277+
pDC->Rectangle(bgRect);
278+
pDC->SelectObject(prevPen);
279+
280+
CRect manualRect(bgRect.left + W_MARGIN, bgRect.top + H_MARGIN,
281+
bgRect.right - W_MARGIN, bgRect.bottom - H_MARGIN);
282+
LOGFONT lf;
283+
CFont manualFont;
284+
mDefPixelTextFont.GetLogFont(&lf);
285+
lf.lfHeight = 14;
286+
lf.lfWeight = FW_NORMAL;
287+
manualFont.CreateFontIndirect(&lf);
288+
pDC->SetBkMode(TRANSPARENT);
289+
CFont *prevFont = pDC->SelectObject(&manualFont);
290+
pDC->SetTextColor(Q1UI_COLOR_TEXT);
291+
CString manual(
292+
"Comparer shortcuts\n"
293+
"\n"
294+
"? Show or hide this panel\n"
295+
"Drag && Drop Open a source into a pane\n"
296+
"Mouse Wheel Zoom in or out; high zoom shows pixel values\n"
297+
"Left/Right Previous or next video frame\n"
298+
"Space Play or pause\n"
299+
"H Toggle hex pixel values\n"
300+
"I Interpolate pixels\n"
301+
"D Toggle pink diff overlay (grid + dots)\n"
302+
"Click timeline Pick a video frame (left/right pane)\n"
303+
);
304+
pDC->DrawText(manual, &manualRect, DT_LEFT | DT_TOP);
305+
pDC->SelectObject(prevFont);
306+
}
307+
308+
// Pilot: draw a semi-transparent pink cell outline + center dot in every
309+
// grid cell that contains at least one pixel that differs from the reference
310+
// pane. Cell size is fixed in display pixels, so zooming in implicitly
311+
// subdivides the source region each cell covers — at maximum zoom each dot
312+
// resolves to a single differing source pixel.
313+
void CComparerView::DrawDiffOverlay(CDC *pDC, CComparerDoc *pDoc, ComparerPane *pane)
314+
{
315+
if (!pDoc->mDiffOverlay)
316+
return;
317+
// At high zoom the per-pixel value labels already convey the diff
318+
// information; the grid and dots would just clutter the view.
319+
if (pDoc->mN > ZOOM_TEXT_START)
320+
return;
321+
if (!pane || !pane->isAvail() || !pane->rgbBuf)
322+
return;
323+
324+
// Find any other available pane to compare against (pilot assumes 2 panes).
325+
ComparerPane *other = nullptr;
326+
for (int i = 0; i < CComparerDoc::IMG_VIEW_MAX; i++) {
327+
ComparerPane *p = &pDoc->mPane[i];
328+
if (p == pane)
329+
continue;
330+
if (p->isAvail() && p->rgbBuf) {
331+
other = p;
332+
break;
333+
}
334+
}
335+
if (!other || pDoc->mW <= 0 || pDoc->mH <= 0)
336+
return;
337+
338+
const int rowBytes = ROUNDUP_DWORD(pDoc->mW) * QIMG_DST_RGB_BYTES;
339+
const BYTE *bufA = pane->rgbBuf;
340+
const BYTE *bufB = other->rgbBuf;
341+
342+
const int cellPx = 48; // grid cell size in display pixels
343+
const int dotR = 3; // center dot radius
344+
345+
Gdiplus::Graphics g(pDC->m_hDC);
346+
g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
347+
g.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
348+
349+
const Gdiplus::Color cellColor(200, 0xff, 0x3d, 0x8a); // ~78% alpha pink
350+
const Gdiplus::Color dotColor (235, 0xff, 0x3d, 0x8a); // ~92% alpha pink
351+
Gdiplus::Pen cellPen(cellColor, 1.0f);
352+
cellPen.SetAlignment(Gdiplus::PenAlignmentInset);
353+
Gdiplus::SolidBrush dotBrush(dotColor);
354+
355+
const float invN = (pDoc->mN > 0.0f) ? (1.0f / pDoc->mN) : 1.0f;
356+
357+
for (int cy = 0; cy < mHCanvas; cy += cellPx) {
358+
int cellBot = cy + cellPx;
359+
if (cellBot > mHCanvas) cellBot = mHCanvas;
360+
361+
int sy0 = (int)floorf((cy - mYDst) * invN);
362+
int sy1 = (int)ceilf ((cellBot - mYDst) * invN);
363+
if (sy0 < 0) sy0 = 0;
364+
if (sy1 > pDoc->mH) sy1 = pDoc->mH;
365+
if (sy0 >= sy1) continue;
366+
367+
for (int cx = 0; cx < mWCanvas; cx += cellPx) {
368+
int cellRight = cx + cellPx;
369+
if (cellRight > mWCanvas) cellRight = mWCanvas;
370+
371+
int sx0 = (int)floorf((cx - mXDst) * invN);
372+
int sx1 = (int)ceilf ((cellRight - mXDst) * invN);
373+
if (sx0 < 0) sx0 = 0;
374+
if (sx1 > pDoc->mW) sx1 = pDoc->mW;
375+
if (sx0 >= sx1) continue;
376+
377+
bool hasDiff = false;
378+
const int byteStart = sx0 * QIMG_DST_RGB_BYTES;
379+
const int byteLen = (sx1 - sx0) * QIMG_DST_RGB_BYTES;
380+
for (int sy = sy0; sy < sy1; sy++) {
381+
const BYTE *ra = bufA + sy * rowBytes + byteStart;
382+
const BYTE *rb = bufB + sy * rowBytes + byteStart;
383+
if (memcmp(ra, rb, byteLen) != 0) {
384+
hasDiff = true;
385+
break;
386+
}
387+
}
388+
389+
if (hasDiff) {
390+
g.DrawRectangle(&cellPen,
391+
(float)cx, (float)cy,
392+
(float)(cellRight - cx - 1), (float)(cellBot - cy - 1));
393+
394+
float dotX = (cx + cellRight) * 0.5f;
395+
float dotY = (cy + cellBot) * 0.5f;
396+
g.FillEllipse(&dotBrush,
397+
dotX - dotR, dotY - dotR, 2.0f * dotR, 2.0f * dotR);
398+
}
399+
}
400+
}
401+
}
402+
250403
void CComparerView::DrawEmptyPane(CDC *pDC, CComparerDoc *pDoc)
251404
{
252405
CRect canvas(0, mRcControls.bottom, mWClient, mHClient);
@@ -678,6 +831,12 @@ void CComparerView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
678831
pDoc->mInterpol = !pDoc->mInterpol;
679832
Invalidate(FALSE);
680833
break;
834+
case 'D':
835+
pDoc->mDiffOverlay = !pDoc->mDiffOverlay;
836+
break;
837+
case VK_OEM_2: // '?' / '/' on US keyboards.
838+
ToggleHelp();
839+
break;
681840
}
682841

683842
// Most shortcuts affect shared document/view state, so refresh once at the end.

Comparer/ComparerView.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class CComparerView : public CScrollView
4343
CQMenuItem mNameQMenu;
4444
CQMenuItem mCsQMenu;
4545
bool mProcessing;
46+
bool mShowHelp;
4647
int mRgbBufSize;
4748
BYTE *mRgbBuf;
4849
CFont mDefPixelTextFont;
@@ -57,6 +58,9 @@ class CComparerView : public CScrollView
5758
void ScaleNearestNeighbor(CComparerDoc *pDoc, BYTE *src, BYTE *dst, int sDst,
5859
q1::GridInfo &gi);
5960
void DrawEmptyPane(CDC *pDC, CComparerDoc *pDoc);
61+
void DrawDiffOverlay(CDC *pDC, CComparerDoc *pDoc, ComparerPane *pane);
62+
void DrawHelpMenu(CDC *pDC);
63+
void ToggleHelp();
6064

6165
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
6266
afx_msg void OnDropFiles(HDROP hDropInfo);

Comparer/PosInfoView.cpp

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,6 @@ void CPosInfoView::OnInitialUpdate()
5959
CScrollView::OnInitialUpdate();
6060
}
6161

62-
static void DrawDiffPosLines(CDC *pDC, CRect *frameRect, bool *flags, int n)
63-
{
64-
int top = frameRect->top + 1;
65-
int left = frameRect->left + 1;
66-
int right = frameRect->right - 1;
67-
68-
for (int i = 0; i < n; i++) {
69-
if (flags[i] == false)
70-
continue;
71-
72-
pDC->MoveTo(left, top + i);
73-
pDC->LineTo(right, top + i);
74-
}
75-
}
76-
7762
void CPosInfoView::DrawEachRect(CDC* pDC,
7863
ComparerPane *pane,
7964
CRect *frameRect,
@@ -98,17 +83,9 @@ void CPosInfoView::DrawEachRect(CDC* pDC,
9883
pDC->FillSolidRect(frameRect, frameColor);
9984
pDC->Draw3dRect(frameRect, Q1UI_COLOR_SURFACE, Q1UI_COLOR_SURFACE);
10085

101-
if (parseDone) {
102-
CPen *prev = pDC->SelectObject(mDiffPen);
103-
IFrmCmpStrategy *frmCmpStrategy = pDoc->mFrmCmpStrategy;
104-
list<RLC> diffRLC[QPLANES];
105-
106-
if (mFileScanThread->copyDiffRLC(i, diffRLC)) {
107-
frmCmpStrategy->FlagTotalDiffLine(diffRLC, mDiffFlags, mPosLinesPerFrame);
108-
DrawDiffPosLines(pDC, frameRect, mDiffFlags, mPosLinesPerFrame);
109-
}
110-
pDC->SelectObject(prev);
111-
}
86+
// Per-row red diff lines have been replaced by the pink grid overlay in
87+
// ComparerView. Pilot keeps the frame tiles for video-frame selection but
88+
// no longer draws the per-row indicators.
11289

11390
const COLORREF curIdColor = Q1UI_COLOR_ACCENT;
11491
COLORREF preColor;
@@ -183,7 +160,13 @@ void CPosInfoView::OnDraw(CDC* pDC)
183160

184161
memDC.FillSolidRect(CRect(0, 0, mWClient, h), Q1UI_COLOR_APP_BG);
185162

186-
if (!paneL->isAvail() && !paneR->isAvail()) {
163+
// Pilot: hide the timeline entirely when neither pane is a video. The pink
164+
// grid overlay in ComparerView already conveys diff information; the
165+
// timeline is only useful for picking a frame in a video source.
166+
bool hasVideo = (paneL->isAvail() && paneL->frames > 1)
167+
|| (paneR->isAvail() && paneR->frames > 1);
168+
169+
if (!paneL->isAvail() && !paneR->isAvail() || !hasVideo) {
187170
LOGFONT lf;
188171
mPosNumFont.GetLogFont(&lf);
189172
::lstrcpy(lf.lfFaceName, Q1UI_FONT_TEXT);
@@ -195,7 +178,10 @@ void CPosInfoView::OnDraw(CDC* pDC)
195178
memDC.SetBkMode(TRANSPARENT);
196179
memDC.SetTextColor(Q1UI_COLOR_TEXT_MUTED);
197180
CRect msgRect(0, 0, mWClient, h);
198-
memDC.DrawText(_T("Timeline"), &msgRect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
181+
LPCTSTR msg = (paneL->isAvail() || paneR->isAvail())
182+
? _T("Timeline\n(video only)") : _T("Timeline");
183+
memDC.DrawText(msg, &msgRect,
184+
DT_CENTER | DT_VCENTER | DT_WORDBREAK | DT_NOCLIP);
199185
memDC.SelectObject(prevFont);
200186
goto OnDrawExit;
201187
}

docs/USER_GUIDE.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Viewer includes a built-in control panel, opened with `?`.
5656
| Toggle box info display | `B` |
5757
| Toggle pixel interpolation | `I` |
5858
| Next color space | `N` |
59+
| Mute or unmute video audio | `M` |
5960
| Toggle selection mode | `S` |
6061
| Capture view or selected region | `Ctrl+C` |
6162

@@ -88,6 +89,33 @@ The same reference and encoded image below are shown with SSIM selected:
8889

8990
![Comparer showing SSIM for the same sources](images/comparer-ssim.webp)
9091

92+
### Pixel-Level Diff Overlay
93+
94+
When two sources are loaded, Comparer highlights every region where the pixels
95+
differ from the reference pane. The image area is divided into a fixed-size
96+
grid in display pixels; each cell that contains any differing pixel gets a
97+
translucent pink rectangle outline plus a center dot. Because the cell size is
98+
fixed on screen, zooming in implicitly subdivides the source area each cell
99+
covers — at maximum zoom each dot resolves to a single differing source pixel.
100+
The overlay is hidden automatically when zoom is high enough to show per-pixel
101+
values, since the pixel labels already convey the diff.
102+
103+
### Controls
104+
105+
Comparer also includes a built-in shortcut panel, opened with `?`.
106+
107+
| Action | Control |
108+
| --- | --- |
109+
| Show or hide the shortcut panel | `?` |
110+
| Open a source into a pane | Drag and drop |
111+
| Zoom | Mouse wheel |
112+
| Previous or next video frame | Left / Right |
113+
| Play or pause | `Space` |
114+
| Toggle hex pixel values | `H` |
115+
| Toggle pixel interpolation | `I` |
116+
| Toggle pink diff overlay (grid + dots) | `D` |
117+
| Pick a frame in the timeline (video) | Click left or right side |
118+
91119
## Input Notes
92120

93121
- HEIF/HEIC/HIF and AVIF still images are supported by both applications.

0 commit comments

Comments
 (0)