I went ahead and repeated the experiment to see if I could figure out what's going on.
I generated a random 256-by-256 pixel RGB image using the "Solid Noise" filter in GIMP (Filters > Render > Clouds > Solid Noise...) using the default settings (shown below):
And the result:
Then I saved the image as a JPEG using the default settings:
Then I transferred the image to Windows and opened the image with Windows Photo Viewer by right-clicking on the image in the File Explorer and choosing Preview from the menu. Then I rotated the image using the buttons in the bottom, and saved the image by navigating to the next image using the arrow keys.
For each of the tests below I started with a copy of the original image, and rotated (clicked on the rotate button) the corresponding number of times before saving. Here are the reslting sizes (
ls -l -r):
size in bytes last-modified date
-rwxrwx--- 1 root vboxsf 6258 Nov 8 11:24 original.jpg
-rwxrwx--- 1 root vboxsf 23645 Nov 8 11:30 cw.jpg
-rwxrwx--- 1 root vboxsf 23636 Nov 8 11:30 cw-cw.jpg
-rwxrwx--- 1 root vboxsf 23649 Nov 8 11:30 cw-cw-cw.jpg
-rwxrwx--- 1 root vboxsf 6258 Nov 8 11:27 cw-cw-cw-cw.jpg
-rwxrwx--- 1 root vboxsf 23649 Nov 8 11:31 cw-cw-cw-cw-cw.jpg
-rwxrwx--- 1 root vboxsf 23649 Nov 8 11:29 ccw.jpg
-rwxrwx--- 1 root vboxsf 23636 Nov 8 11:29 ccw-ccw.jpg
-rwxrwx--- 1 root vboxsf 23645 Nov 8 11:29 ccw-ccw-ccw.jpg
-rwxrwx--- 1 root vboxsf 6258 Nov 8 11:27 ccw-ccw-ccw-ccw.jpg
-rwxrwx--- 1 root vboxsf 23649 Nov 8 11:30 ccw-ccw-ccw-ccw-ccw.jpg
- Windows Photo Viewer (WPV) does increase the size dramatically; the amount of increase is around four times in this test!
- All the new images increase to around the same size, but they are not identical.
- WPV does not re-encode or even re-save the image when it is rotated by a multiple of 360 degrees. (The timestamp, 11:27, is when the files were first copied.)
cmp -l on files that should have identical content allows us to see where the files differ.
robert@unity ../jpeg-rotate-test % cmp -l cw.jpg ccw-ccw-ccw.jpg
2223 63 62
2224 60 71
2226 60 64
2227 60 66
robert@unity ../jpeg-rotate-test % cmp -l cw-cw.jpg ccw-ccw.jpg
2223 63 62
2224 60 71
2226 60 64
2227 62 64
robert@unity ..jpeg-rotate-test % cmp -l ccw.jpg cw-cw-cw.jpg
2223 62 63
2224 71 60
2226 64 60
2227 61 64
robert@unity ../jpeg-rotate-test % cmp -l cw.jpg cw-cw-cw-cw-cw.jpg
2221 60 61
2223 63 61
2224 60 66
2226 60 61
2227 60 61
robert@unity ../jpeg-rotate-test % cmp -l ccw.jpg ccw-ccw-ccw-ccw-ccw.jpg
2223 62 63
2224 71 60
2226 64 65
2227 61 64
These files differ in only four bytes (actually in a timestamp), meaning that WPV is doing the same thing every time; now we only need to figure out what that is.
For this I used JPEGsnoop to see what exactly was is in the images.
Since the outputs are fairly long I've linked to them as a gist. Here's a summary of the differences:
GIMP uses only an
APP0 (JFIF) and
COM (comment) segment for metadata. WPV leaves the
APP0 segment untouched, but curiously adds a null byte to the comment (so that it is null-terminated).
WPV adds two
APP1 segments, which are Exif and XMP metadata. These segments are 4286 and 12726 bytes, respectively. Together they account for almost the entire increase in filesize.
GIMP produces a progressive JPEG, while WPV produces a baseline (non-progressive) JPEG. For this reason GIMP's image has multiple scan segments, while the WPV image has only one. In my experience the progressive image is sometimes slightly smaller.
GIMP used 1×1 chroma subsampling, while WPV used 2×2 subsampling. This leads me to believe that WPV is not using "true" lossless rotation, unless it is somehow able to detect this is a black-and-white image.
To resolve these issues, I ran a second test.
I followed similar steps to the first test. I created a random 256×256 RGB image using the RGB Noise filter (Filters > Nose > RGB Nose...) with the following settings:
Here is the result:
I exported the file as JPEG using the following settings:
Progressive has been turned off, but Subsampling is still set to 4:4:4 (which is another name for 1×1 subsampling). The quality is increased to 98.
I copied the image and rotated the copy clockwise; then copied the rotated version and rotated that copy counterclockwise, so that we can directly compare the quality between the original and the WPV processed copy.
-rwxrwx--- 1 root vboxsf 159774 Nov 8 16:21 original-random.jpg
-rwxrwx--- 1 root vboxsf 222404 Nov 8 16:24 cw-random.jpg
-rwxrwx--- 1 root vboxsf 222467 Nov 8 16:24 cw-ccw-random.jpg
Although the increase this time is smaller in relative terms (around 40%) the absolute increase is even greater—around 62 kB. This suggests that WMV is using a less efficient encoding.
I'll use ImageMagick to compare the two images:
robert@unity ../jpeg-rotate-test % compare -verbose -metric AE original-random.jpg cw-ccw-random.jpg null:
original-random.jpg JPEG 256x256 256x256+0+0 8-bit sRGB 160KB 0.000u 0:00.009
cw-ccw-random.jpg JPEG 256x256 256x256+0+0 8-bit sRGB 222KB 0.010u 0:00.010
Channel distortion: AE
original-random.jpg=> JPEG 256x256 256x256+0+0 8-bit sRGB 0.050u 0:00.020
There are zero pixels different between the original and rotated copy. So, even if WPV is not using "true" lossless rotation, it is doing a good enough job. I suspect I know what is going on, and to explain I'll divert a little bit into the math behind JPEG compression.
The JPEG compression algorithm breaks an image into 8×8-pixel blocks. Each one of these blocks is then subjected to a Discrete Cosine Transform (DCT). The resulting DCT coefficients describe the block as a sum of different-frequency waves. The algorithm then "throws away" some information in the high-frequency waves that correspond to noise and very small detail. The decoding process reverses the DCT, adding the stored waves together to get back the block.
It is possible to rotate the DCT "waves" without actually undoing and redoing the transform (basically you turn all the horizontal waves into vertical waves and vice-versa). What I think happens in WPV is that the image is actually decoded, rotated, and then re-encoded. During the re-encoding process, since our image's size is a multiple of 8 in both dimensions, each of the new blocks corresponds to one of the original blocks. Importantly, since each block has no high-frequency components, the algorithm doesn't throw away any information, and finds exactly the right DCT components that a "true" lossless rotation would have.
Lastly, I'll look at the components of the JPEG files again. The results are again linked as gists. Comparing the two:
The WPV image contains an extra 4286+2 bytes of Exif metadata, 1 extra byte in the comment, and 12,726+2 bytes of XMP metadata. This is a total of 17,017 bytes of additional metadata. What is all that data used for? I peered into the file with my trusty hex editor and a copy of the relevant standards:
Exif metadata is structured like a TIFF image, which contains a number of tags (there's way more complexity, but I'll skip right over it). Most of the bytes in the Exif segment are contained in two identical tags with tag number
EA1C (59,932 decimal). That tag number is not documented anywhere I could find. Both tags contain 2060 bytes of "undefined" type, which are all null bytes except for the first six (
1C EA 00 00 00 08). I have no idea what these tags are, why there are two of them, and why they need to be 2 kB each.
The XMP metadata is actually an entire embedded XML document with namespacing and long UUIDs, that just contains the WPV version string (that was already in the Exif metadata). However, that only accounts for around 400 bytes. The remainder of the segment is 122 repetitions of 100 spaces followed by a newline. That's over 12,000 bytes of totally wasted space.
Like the previous test, both GIMP and WPV use the same DCT quantization tables. This means that they should be calculating the exact same DCT coefficients, which is why the images are exactly the same. I'm not sure if WPV just happens to be using the same quantization tables or if it copies the tables from the input.
Unlike the previous test, this time WPV uses 1×1 subsampling, so it may actually be detecting that this is a color image (or at least that higher samples are necessary to losslessly re-encode the image).
GIMP and WPV use different Huffman tables (part of the entropy coding step). The tables for WPV are larger by a total of 279 bytes, and in one case contain 7 times as many codes.
Looking at JPEGsnoop's statistics, we can see that some of these codes are rarely used. For example, in the
ID: 1, Class: AC table, of the 119 16-bit codes defined, only 23 are actually used. Overall, the actual scan segment is 28.5% larger in the WPV version.
WPV may not be doing "true" lossless rotations, but the rotations seem to be practically lossless.
The extra size is partially due to a fixed amount of added metadata, and partly due to less efficient entropy coding.
OS (Linux) (
Linux unity 3.16.0-4-amd64 #1 SMP Debian 3.16.36-1+deb8u1 (2016-09-03) x86_64 GNU/Linux
GIMP (Linux): 2.8.14 (from package
Window Photo Viewer (according to image metadata):
Microsoft Windows Photo Viewer 10.0.10586.0