FEATURE: introduce lossy color optimization on resized pngs

FEATURE: introduce lossy color optimization on resized pngs

This feature ensures optimized images run via pngquant, this results extreme amounts of savings for resized images. Effectively the only impact is that the color palette on small resized images is reduced to 256.

To ensure safety we only apply this optimisation to images smaller than 500k.

This commit also makes a bunch of image specs less fragile.

diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb
index 01f870b..5e5e8e4 100644
--- a/app/models/optimized_image.rb
+++ b/app/models/optimized_image.rb
@@ -318,9 +318,13 @@ class OptimizedImage < ActiveRecord::Base
     convert_with(instructions, to, opts)
   end
 
+  MAX_PNGQUANT_SIZE = 500_000
+
   def self.convert_with(instructions, to, opts = {})
     Discourse::Utils.execute_command(*instructions)
-    FileHelper.optimize_image!(to)
+
+    allow_pngquant = to.downcase.ends_with?(".png") && File.size(to) < MAX_PNGQUANT_SIZE
+    FileHelper.optimize_image!(to, allow_pngquant: allow_pngquant)
     true
   rescue => e
     if opts[:raise_on_error]
diff --git a/lib/file_helper.rb b/lib/file_helper.rb
index f146b8b..3f148fc 100644
--- a/lib/file_helper.rb
+++ b/lib/file_helper.rb
@@ -82,7 +82,12 @@ class FileHelper
     tmp
   end
 
-  def self.optimize_image!(filename)
+  def self.optimize_image!(filename, allow_pngquant: false)
+    pngquant_options = false
+    if allow_pngquant
+      pngquant_options = { allow_lossy: true }
+    end
+
     ImageOptim.new(
       # GLOBAL
       timeout: 15,
@@ -92,7 +97,7 @@ class FileHelper
       advpng: false,
       pngcrush: false,
       pngout: false,
-      pngquant: false,
+      pngquant: pngquant_options,
       # JPG
       jpegoptim: { strip: SiteSetting.strip_image_metadata ? "all" : "none" },
       jpegtran: false,
diff --git a/spec/fixtures/images/pngquant.png b/spec/fixtures/images/pngquant.png
new file mode 100644
index 0000000..2ffb274
Binary files /dev/null and b/spec/fixtures/images/pngquant.png differ
diff --git a/spec/lib/upload_creator_spec.rb b/spec/lib/upload_creator_spec.rb
index 04ac72f..897c946 100644
--- a/spec/lib/upload_creator_spec.rb
+++ b/spec/lib/upload_creator_spec.rb
@@ -98,6 +98,27 @@ RSpec.describe UploadCreator do
       end
     end
 
+    describe 'pngquant' do
+      let(:filename) { "pngquant.png" }
+      let(:file) { file_from_fixtures(filename) }
+
+      it 'should apply pngquant to optimized images' do
+        upload = UploadCreator.new(file, filename,
+          pasted: true,
+          force_optimize: true
+        ).create_for(user.id)
+
+        # no optimisation possible without losing details
+        expect(upload.filesize).to eq(9558)
+
+        thumbnail_size = upload.get_optimized_image(upload.width, upload.height, {}).filesize
+
+        # pngquant will lose some colors causing some extra size reduction
+        expect(thumbnail_size).to be < 7500
+      end
+
+    end
+
     describe 'converting to jpeg' do
       let(:filename) { "should_be_jpeg.png" }
       let(:file) { file_from_fixtures(filename) }
diff --git a/spec/models/optimized_image_spec.rb b/spec/models/optimized_image_spec.rb
index 45c96ab..9aaa864 100644
--- a/spec/models/optimized_image_spec.rb
+++ b/spec/models/optimized_image_spec.rb
@@ -6,7 +6,7 @@ describe OptimizedImage do
 
   unless ENV["TRAVIS"]
     describe '.crop' do
-      it 'should work correctly (requires correct version of image optim)' do
+      it 'should produce cropped images' do
         tmp_path = "/tmp/cropped.png"
 
         begin
@@ -17,12 +17,15 @@ describe OptimizedImage do
             5
           )
 
-          fixture_path = "#{Rails.root}/spec/fixtures/images/cropped.png"
-          fixture_hex = Digest::MD5.hexdigest(File.read(fixture_path))
+          # we don't want to deal with something new here every time image magick
+          # is upgraded or pngquant is upgraded, lets just test the basics ...
+          # cropped image should be less than 120 bytes
 
-          cropped_hex = Digest::MD5.hexdigest(File.read(tmp_path))
+          cropped_size = File.size(tmp_path)
+
+          expect(cropped_size).to be < 120
+          expect(cropped_size).to be > 50
 
-          expect(cropped_hex).to eq(fixture_hex)
         ensure
           File.delete(tmp_path) if File.exists?(tmp_path)
         end
@@ -128,7 +131,7 @@ describe OptimizedImage do
     end
 
     describe '.downsize' do
-      it 'should work correctly (requires correct version of image optim)' do
+      it 'should downsize logo' do
         tmp_path = "/tmp/downsized.png"
 
         begin
@@ -138,12 +141,10 @@ describe OptimizedImage do
             "100x100\>"
           )
 
-          fixture_path = "#{Rails.root}/spec/fixtures/images/downsized.png"
-          fixture_hex = Digest::MD5.hexdigest(File.read(fixture_path))
-
-          downsized_hex = Digest::MD5.hexdigest(File.read(tmp_path))
+          info = FastImage.new(tmp_path)
+          expect(info.size).to eq([100, 27])
+          expect(File.size(tmp_path)).to be < 2300
 
-          expect(downsized_hex).to eq(fixture_hex)
         ensure
           File.delete(tmp_path) if File.exists?(tmp_path)
         end

GitHub
sha: 766e67ce

2 Likes

would it make sense to add an option for this? on things like photography forums this might not be desired.

Do photography website usually posts < 500 KB PNG images?

we sometimes do for screenshots for bugs and enlarged details to discuss issues. and then color accuracy is important.

Not relevant, this only affects thumbnails.

On Wed, Jan 2, 2019 at 7:58 AM darix notifications@github.com wrote:

we sometimes do for screenshots for bugs and enlarged details to discuss
issues. and then color accuracy is important.


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/discourse/discourse/commit/766e67ce575aaa6f817c842d591948c7c02b84cc#commitcomment-31820279,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABc7VQUJOBe8-Q1Sk2ixIA0csLPVaQrGks5u_Nc6gaJpZM4Zmsyi
.

Note, color accuracy is not impacted on thumbnails, only fidelity (which is reduced to 256 for thumbs). Original image is never touched.

I see no reason to make this configurable as this would be an enormous slippery slope, cause next up you would want control over unsharp algorithm used for image thumbs and the journey would continue from there to horrible places.

1 Like