NodeBB icon indicating copy to clipboard operation
NodeBB copied to clipboard

Saved JPEG quality is severly degraded

Open sadaszewski opened this issue 2 years ago • 30 comments

I upload JPEGs with quality 97 looking wonderfully. Stripped-EXIF JPEGs saved by NodeBB contain a TON more artifacts and look really poorly (quality 85 I would say). I think the way image processing / EXIF stripping is done needs to be rethought. The image data should not be touched at all. All tags apart from orientation should be stripped. Degradation of quality can be avoided if the imaging data is not touched.

Independently, I believe that NodeBB is using a default JPEG quality level to save processed images. In case image resizing is enabled, it should take care that the same quality level is used for the resized output as was used in the uploaded JPEG.

It is really sad to upload nice quality images and see the result completely garbled - sorry for the rant ;)

sadaszewski avatar Mar 31 '22 13:03 sadaszewski

Wouldn't PR #10447 be the solution for the EXIF part ?

sadaszewski avatar Mar 31 '22 15:03 sadaszewski

Not sure if EXIF is replaced though...

https://github.com/lovell/sharp/issues/2910

sadaszewski avatar Mar 31 '22 15:03 sadaszewski

Added the workaround from the above ticket. I think it should actually work ...

sadaszewski avatar Mar 31 '22 15:03 sadaszewski

No I think it will not work, we need to use a command line tool like exiv2.

sadaszewski avatar Mar 31 '22 15:03 sadaszewski

I have found the correct command using exiftool. The PR should work provided that exiftool is available on the system.

sadaszewski avatar Mar 31 '22 15:03 sadaszewski

How should exiftool be added to the continuous integration Docker image?

sadaszewski avatar Mar 31 '22 16:03 sadaszewski

The default jpeg output quality is 80: https://sharp.pixelplumbing.com/api-output#jpeg

That might be something we want to change or make configurable.

pitaj avatar Mar 31 '22 18:03 pitaj

Your PR is never going to fly as is. You probably only saw a quality improvement because you're no longer going through sharp at all.

pitaj avatar Mar 31 '22 18:03 pitaj

Ok it's terribly low but regardless of the quality, every re-compression with JPEG degrades quality. Especially that with Exif-Stripping + resizing those operations are currently chained !!! This could be worked around to re-compress only once but even so, for a photography forum such as mine, one recompression is already one too many. I strongly advocate in favor of exiftool for EXIF stripping. As implemented in PR #10447. It works perfectly - WAY better than sharp. And only requires a well established tool to be present on the system.

sadaszewski avatar Mar 31 '22 18:03 sadaszewski

Sharp will probably never be able to achieve what exiftool is already doing without any problem whatsoever. Please let me know how the PR should be modified in order to "fly" ;)

sadaszewski avatar Mar 31 '22 18:03 sadaszewski

You can implement just exif stripping in a plugin like https://github.com/NodeBB/nodebb-plugin-imagemagick if that's what you really want.

But I think there's some weird stuff going on in our image library. It does appear that we might be running images through sharp multiple times.

@baris do you mind taking a look?

pitaj avatar Mar 31 '22 18:03 pitaj

As for what to do about your PR: I suggest changing it to just set the quality to 100 for jpegs and pngs when stripping exif data. See resize for an example of detecting the image type.

pitaj avatar Mar 31 '22 18:03 pitaj

Wouldn't it be better to detect if exiftool is available and use it if possible?

sadaszewski avatar Mar 31 '22 18:03 sadaszewski

Fixing quality to 100 will cause quite a lot of bloat when uploading other images with intentionally lower quality. It will get ugly fast. The problem is really that stripEXIF is not doing as advertised and not just stripping EXIF but recompressing the image.

sadaszewski avatar Mar 31 '22 18:03 sadaszewski

Also stripEXIF does not seem to support a hook, so a plugin currently cannot be implemented.

sadaszewski avatar Mar 31 '22 18:03 sadaszewski

You'd turn off exif stripping in the ACP and do it in your plugin instead.

I doubt that setting quality to 100 will bloat the images, but I can't tell you for sure without experimenting.

pitaj avatar Mar 31 '22 18:03 pitaj

But which hook should I use then in the plugin?

Setting quality to 100 does blow up images. An example image I just tried saved at 97 has 600 KB. That image saved then at 80 has 200 KB and then the 80-image saved back as 100 has 500KB. So agreed it does not blow back even to the level of original 97 but it does blow up at least 2.5 times and that's for a relatively small image - 1400x1080.

sadaszewski avatar Mar 31 '22 18:03 sadaszewski

I think we can make some improvements. Adding a hook into stripExif is one of them.

Also it seems right now if stripExif is turned on and the image is too big it goes through sharp twice. This can be changed so that it only goes through sharp once. Because it uses the same code.

stripExif does sharp(buffer).rotate().toFile() and then resize does sharp(buffer).rotate().resize().jpeg().toFile()

So it seems we don't need the call to stripExif here https://github.com/NodeBB/NodeBB/blob/master/src/controllers/uploads.js#L67 if image is already going to be resized by sharp.

I think a processImage function would be better to combine the two. Something like

const sharpImage = sharp(buffer, { failOnError: true});
sharpImage.rotate(); // auto-orients based on exif data
if (shouldResize) {
    sharpImage.resize(width, height));
}
sharpImage.jpeg()
await sharpImage.toFile(path)

barisusakli avatar Mar 31 '22 18:03 barisusakli

@sadaszewski https://github.com/NodeBB/NodeBB/commit/b8765df5f48b638517daac041964c7443dd52fde

barisusakli avatar Mar 31 '22 18:03 barisusakli

The stripEXIF hook is welcome. If a plugin is indeed the only way to go...

sadaszewski avatar Mar 31 '22 18:03 sadaszewski

Another alternative - although unmaintained - could be to use piexifjs. In this kind of code I would not expect many changes to be necessary over time - if any at all. What do you think?

sadaszewski avatar Mar 31 '22 19:03 sadaszewski

Detecting the quality of an input jpeg is a hard problem: https://gist.github.com/FranckFreiburger/d8e7445245221c5cf38e69a88f22eeeb

But we could do something like this to try to output a jpeg at the same quality of the input jpeg.

pitaj avatar Mar 31 '22 19:03 pitaj

You could "just strip exif data but orientation" but that won't work if you need to resize the image anyways, and support for exif orientation in browsers isn't great.

pitaj avatar Mar 31 '22 19:03 pitaj

You could "just strip exif data but orientation" but that won't work if you need to resize the image anyways, and support for exif orientation in browsers isn't great.

I haven't seen any browser where it would not be supported in the last years...

sadaszewski avatar Mar 31 '22 19:03 sadaszewski

Resizing is not something I am interested in but I agree that the correct way to do this would be to try to estimate similar image quality to the original.

sadaszewski avatar Mar 31 '22 19:03 sadaszewski

Did you try disabling resizing? Nodebb stores the original image and then resizes and saves that to filename-resized.jpg. The resized image is displayed in the browser and when clicked should open the original. I tried with resizing disable and got the same image even with stripExif enabled.

barisusakli avatar Mar 31 '22 22:03 barisusakli

I never had resizing enabled, only stripEXIF. And JPG images definitely come out garbled every single time. You can see the artifacts from low quality compression / recompression with but a slight zoom. It would be wrong to get the same image with stripEXIF enabled. After all the EXIF is supposed to be stripped.

sadaszewski avatar Apr 01 '22 05:04 sadaszewski

I have created the plugin : https://github.com/sadaszewski/nodebb-plugin-exiftool

sadaszewski avatar Apr 01 '22 12:04 sadaszewski