TL;DR
WinMerge's image compare decodes JPEG files to slightly different pixel values than other image software does, so it reports differences between images that are actually identical.
Open the two attached files in WinMerge's image compare — repro.jpg, and repro-libjpeg-decode.png which is a lossless copy of that JPEG's own correct decode. They should have zero differences. Instead there's 5000+, with some faintly visible at 100% zoom, and many visible at 200%.
The same JPEG renders identically in 7 image viewers and 2 image editors (listed below), and differently only in WinMerge.
Proposed fix
Presumed and untested — I did not build WinIMerge to verify it. If it is wrong or incomplete, the sections below give the measurements, the reproduction, and the reasoning.
Decode for accuracy on the image-compare path. Either pass JPEG_ACCURATE to FreeImage_Load, or set dct_method = JDCT_ISLOW and do_fancy_upsampling = TRUE in the bundled FreeImage JPEG plugin.
Or add as an option: Compare ▸ Image ▸ accurate JPEG decoding.
Where to look: FreeImage's JPEG loader offers JPEG_ACCURATE ("best quality, sacrificing some speed") as an opt-in flag, so its default path is not the most accurate available. Check whether WinIMerge passes it, and what dct_method and do_fancy_upsampling end up as in the forked PluginJPEG.cpp.
Environment
WinMerge 2.16.58.0 · Windows 11 Pro 26200 · x64
Reproduction
Two attached files:
| File |
What it is |
repro.jpg |
a 949×949 4:2:0 JPEG, quality 95 |
repro-libjpeg-decode.png |
that same JPEG decoded with libjpeg at its defaults, saved losslessly |
Two independent decodes of repro.jpg are bit-identical (0 pixels differ), so the reference is deterministic.
- Open both files in WinMerge's image compare.
- Make sure no unpacker plugin is active.
Expected: no differences. The PNG holds exactly the pixels a conforming decoder produces from the JPEG.
Actual: differences reported across the image.
What it looks like
At 1:1 the difference is faint. At 2× zoom it is clear on saturated colour edges — a red hair strand in this image — and faint in several other areas.
Where the difference is
Not uniform. It concentrates on edges, line art, hatching and screentone, and is exactly zero across flat areas.
repro-difference-map.png is attached: the difference between a default decode and a fast-IDCT decode of repro.jpg, amplified ×14 with a 2.2 gamma lift. The error follows the drawing; flat regions are black.
Measured on the same pair, 64-pixel tiles:
|
mean abs. difference (0–255 levels) |
| whole frame |
1.096 |
| red hair strand region (200×200 at 170,195) |
1.411 |
| quietest tile of 225 (flat background) |
0.000 |
| median tile |
1.160 |
| worst tile |
2.263 |
The worst 10% of tiles hold 17.3% of the summed error, where a uniform distribution would hold 10%. Five of the eight worst tiles fall inside the red-strand region, which is where the difference is visible by eye.
A flat 8×8 block carries only a DC coefficient, so an approximate inverse transform has nothing to get wrong — hence the true zeros. A detailed block carries many AC coefficients and the error accumulates across them.
Presumed Cause
FreeImage bundles and configures its own libjpeg, so its output need not match libjpeg's defaults.
Decoding the same file under each combination of IDCT method and chroma upsampling, against a libjpeg-default decode as baseline:
| Decode variant |
mean abs. difference |
islow + fancy upsampling — the default; control |
0.000 |
float + fancy upsampling — control |
0.026 |
islow + simple upsampling |
0.182 |
ifast + fancy upsampling |
1.432 |
ifast + simple upsampling |
1.476 |
| WinMerge, measured against paint.NET's render |
1.537 |
Both controls return ~0. A fast IDCT reproduces the observed difference within 4%; chroma upsampling alone is roughly eight times too small to account for it.
The error grows with file quality
| Source |
est. quality |
chroma |
ifast + fancy |
| quality-50 original |
50 |
4:2:0 |
0.554 |
| near-lossless re-encode of it |
100 |
4:2:0 |
1.432 |
| same, 4:4:4 |
100 |
4:4:4 |
1.317 |
Same reason as the spatial pattern: a quality-50 file has most high-frequency coefficients quantized away, a near-lossless file keeps them. The error is 2.6× larger on the high-quality file.
The 4:4:4 row is a control on the mechanism: there, islow + simple scores 0.000 and ifast + fancy equals ifast + simple exactly (both 1.317). With no chroma to upsample that setting has nothing to do, while the IDCT term is unchanged.
Effects
A fast IDCT is conforming: JPEG defines the transform mathematically and permits approximation. In image comparison the decoded pixels are also the input to the difference count.
- A JPEG and a lossless copy of its own decode are reported as different.
- Difference counts carry a floor of decode noise, so they do not measure how much two images actually differ.
- Tested on the same file: IrfanView, FastStone Image Viewer, FSPViewer, ImageGlass, Honeyview, NeeView, qView (7 viewers) and Paint, paint.NET (2 editors) agree with each other and with the reference PNG. WinMerge is the only outlier.
Nothing is corrupted and no data is at risk.
Residual uncertainty
On a 4:4:4 near-lossless variant, WinMerge measured 2.061 against ifast's 1.317 — a wider gap than the 4:2:0 case, and unexplained. Candidates: FreeImage linking classic IJG libjpeg, whose jpeg_idct_ifast is less accurate than the libjpeg-turbo used for the figures above; rounding in the YCbCr→RGB conversion; an 8-bit intermediate where libjpeg keeps more precision. All checkable in the build, none from outside it.
Workaround for users
Convert both sides to a PNG before comparing:
magick in.jpg PNG:out.png
A script can make the temporary PNGs, open them in WinMerge, and delete them afterwards.
TL;DR
WinMerge's image compare decodes JPEG files to slightly different pixel values than other image software does, so it reports differences between images that are actually identical.
Open the two attached files in WinMerge's image compare —
repro.jpg, andrepro-libjpeg-decode.pngwhich is a lossless copy of that JPEG's own correct decode. They should have zero differences. Instead there's 5000+, with some faintly visible at 100% zoom, and many visible at 200%.The same JPEG renders identically in 7 image viewers and 2 image editors (listed below), and differently only in WinMerge.
Proposed fix
Decode for accuracy on the image-compare path. Either pass
JPEG_ACCURATEtoFreeImage_Load, or setdct_method = JDCT_ISLOWanddo_fancy_upsampling = TRUEin the bundled FreeImage JPEG plugin.Or add as an option: Compare ▸ Image ▸ accurate JPEG decoding.
Where to look: FreeImage's JPEG loader offers
JPEG_ACCURATE("best quality, sacrificing some speed") as an opt-in flag, so its default path is not the most accurate available. Check whether WinIMerge passes it, and whatdct_methodanddo_fancy_upsamplingend up as in the forkedPluginJPEG.cpp.Environment
WinMerge 2.16.58.0 · Windows 11 Pro 26200 · x64
Reproduction
Two attached files:
repro.jpgrepro-libjpeg-decode.pngTwo independent decodes of
repro.jpgare bit-identical (0 pixels differ), so the reference is deterministic.Expected: no differences. The PNG holds exactly the pixels a conforming decoder produces from the JPEG.
Actual: differences reported across the image.
What it looks like
At 1:1 the difference is faint. At 2× zoom it is clear on saturated colour edges — a red hair strand in this image — and faint in several other areas.
Where the difference is
Not uniform. It concentrates on edges, line art, hatching and screentone, and is exactly zero across flat areas.
repro-difference-map.pngis attached: the difference between a default decode and a fast-IDCT decode ofrepro.jpg, amplified ×14 with a 2.2 gamma lift. The error follows the drawing; flat regions are black.Measured on the same pair, 64-pixel tiles:
The worst 10% of tiles hold 17.3% of the summed error, where a uniform distribution would hold 10%. Five of the eight worst tiles fall inside the red-strand region, which is where the difference is visible by eye.
A flat 8×8 block carries only a DC coefficient, so an approximate inverse transform has nothing to get wrong — hence the true zeros. A detailed block carries many AC coefficients and the error accumulates across them.
Presumed Cause
FreeImage bundles and configures its own libjpeg, so its output need not match libjpeg's defaults.
Decoding the same file under each combination of IDCT method and chroma upsampling, against a libjpeg-default decode as baseline:
islow+ fancy upsampling — the default; controlfloat+ fancy upsampling — controlislow+ simple upsamplingifast+ fancy upsamplingifast+ simple upsamplingBoth controls return ~0. A fast IDCT reproduces the observed difference within 4%; chroma upsampling alone is roughly eight times too small to account for it.
The error grows with file quality
ifast+ fancySame reason as the spatial pattern: a quality-50 file has most high-frequency coefficients quantized away, a near-lossless file keeps them. The error is 2.6× larger on the high-quality file.
The 4:4:4 row is a control on the mechanism: there,
islow+ simple scores 0.000 andifast+ fancy equalsifast+ simple exactly (both 1.317). With no chroma to upsample that setting has nothing to do, while the IDCT term is unchanged.Effects
A fast IDCT is conforming: JPEG defines the transform mathematically and permits approximation. In image comparison the decoded pixels are also the input to the difference count.
Nothing is corrupted and no data is at risk.
Residual uncertainty
On a 4:4:4 near-lossless variant, WinMerge measured 2.061 against
ifast's 1.317 — a wider gap than the 4:2:0 case, and unexplained. Candidates: FreeImage linking classic IJG libjpeg, whosejpeg_idct_ifastis less accurate than the libjpeg-turbo used for the figures above; rounding in the YCbCr→RGB conversion; an 8-bit intermediate where libjpeg keeps more precision. All checkable in the build, none from outside it.Workaround for users
Convert both sides to a PNG before comparing:
A script can make the temporary PNGs, open them in WinMerge, and delete them afterwards.