Skip to content

Commit

Permalink
Add ImageCompressionBear
Browse files Browse the repository at this point in the history
ImageCompressionBear is a bear which uses optimage, jpegoptim, jpegtran,
pngcrush, optipng to see if the image can be compressed and how much
bytes will be reduced if it's compressed.

Closes #1259
  • Loading branch information
yukiisbored committed Jan 13, 2017
1 parent 9dc6ef7 commit e8d5aea
Show file tree
Hide file tree
Showing 13 changed files with 182 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .ci/deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ esac
# apt-get commands
export DEBIAN_FRONTEND=noninteractive

deps="libclang1-3.4 indent mono-mcs chktex r-base julia golang luarocks verilator cppcheck flawfinder"
deps="libclang1-3.4 indent mono-mcs chktex r-base julia golang luarocks verilator cppcheck flawfinder libjpeg-progs jpegoptim pngcrush optipng"
deps_infer="m4 opam"

case $CIRCLE_BUILD_IMAGE in
Expand Down
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ addons:
- opam
- php-codesniffer
- verilator
- jpegoptim
- libjpeg-progs
- pngcrush
- optipng

cache:
pip: true
Expand Down
1 change: 1 addition & 0 deletions bear-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ munkres3~=1.0
mypy-lang~=0.4.6
nbformat~=4.1
nltk~=3.2
optimage~=0.0.1
proselint~=0.7.0
pycodestyle~=2.2
pydocstyle~=1.1
Expand Down
86 changes: 86 additions & 0 deletions bears/image/ImageCompressionBear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import os
import subprocess
import shutil
import optimage

from coalib.bears.GlobalBear import GlobalBear
from coalib.results.Result import Result
from dependency_management.requirements.PipRequirement import PipRequirement
from dependency_management.requirements.DistributionRequirement \
import DistributionRequirement
from coala_utils.FileUtils import create_tempfile


class ImageCompressionBear(GlobalBear):
LANGUAGES = {'Image'}
REQUIREMENTS = {PipRequirement('optimage', '0.0.1'),
DistributionRequirement(apt_get='jpegoptim'),
DistributionRequirement(apt_get='libjpeg-progs',
binary='jpegtran'),
DistributionRequirement(apt_get='pngcrush'),
DistributionRequirement(apt_get='optipng')}
AUTHORS = {'The coala developers'}
AUTHORS_EMAILS = {'The coala developers'}
LICENSE = 'AGPL-3.0'
CAN_DETECT = {'Compression', 'Useless metadata'}

@classmethod
def check_prerequisites(cls):
for requirement in cls.REQUIREMENTS:
if isinstance(requirement, DistributionRequirement):
executable = requirement.package['binary'] or \
requirement.package['apt_get']
if shutil.which(executable) is None:
return '{} is not installed'.format(executable)
elif isinstance(requirement, PipRequirement):
if not requirement.is_installed():
return '{} is not installed'.format(
requirement.package)
return True

def run(self, image_files: list=()):
"""
Check for how much the image file size can be optimized
:param image_files: The image files that this bear will use
"""

if isinstance(image_files, str):
image_files = list(image_files)

for filename in image_files:
_, extension = os.path.splitext(filename)
extension = extension.lower()
compressor = optimage._EXTENSION_MAPPING.get(extension)

if compressor is None:
return

output_filename = create_tempfile(suffix=extension)

try:
compressor(filename, output_filename)
except optimage.InvalidExtension:
return
except optimage.MissingBinary:
return
except subprocess.CalledProcessError:
return

original_size = os.path.getsize(filename)
new_size = os.path.getsize(output_filename)
reduction = original_size - new_size
reduction_percentage = reduction * 100 / original_size
savings = 'savings: {} bytes = {:.2f}%'.format(
reduction, reduction_percentage)

if new_size < original_size:
yield Result.from_values(origin=self,
message=('This Image can be '
'losslessly compressed '
'to {} bytes ({})'
.format(new_size,
savings)),
file=filename)

os.remove(output_filename)
90 changes: 90 additions & 0 deletions tests/image/ImageCompressionBearTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import os
import unittest
import shutil

from bears.image.ImageCompressionBear import ImageCompressionBear
from coalib.settings.Section import Section
from queue import Queue


def get_absolute_path(filename):
return os.path.join(os.path.dirname(__file__),
'test_files',
filename)


class ImageCompressionBearTest(unittest.TestCase):
"""
Tests for ImageCompressionBear
"""

MESSAGE_RE = ('This Image can be losslessly compressed to .* bytes '
'\(savings: .* .*%\)')

NOT_INSTALLED_RE = ('(?:jpegoptim|jpegtran|pngcrush|optipng|optimage) '
'is not installed')

def setUp(self):
self.section = Section('')
self.queue = Queue()
self.icb = ImageCompressionBear(None, self.section, self.queue)

def test_check_prerequisites(self):
_shutil_which = shutil.which
try:
shutil.which = lambda *args, **kwargs: None
self.assertRegex(ImageCompressionBear.check_prerequisites(),
self.NOT_INSTALLED_RE)
finally:
shutil.which = _shutil_which

def test_clean_jpeg(self):
self.assertEqual([], list(self.icb.run(
image_files=[get_absolute_path('gradient_clean.jpg')])))

def test_clean_png(self):
self.assertEqual([], list(self.icb.run(
image_files=[get_absolute_path('gradient_clean.png')])))

def test_bloat_jpeg(self):
results = list(self.icb.run(
image_files=[get_absolute_path('gradient_bloat.jpg')]))

self.assertNotEqual([], results)
self.assertRegex(results[0].message, self.MESSAGE_RE)

def test_bloat_png(self):
results = list(self.icb.run(
image_files=[get_absolute_path('gradient_bloat.png')]))

self.assertNotEqual([], results)
self.assertRegex(results[0].message, self.MESSAGE_RE)

def test_bloat_and_metadata_jpeg(self):
results = list(self.icb.run(
image_files=[
get_absolute_path('gradient_bloat_and_metadata.jpg')]))

self.assertNotEqual([], results)
self.assertRegex(results[0].message, self.MESSAGE_RE)

def test_bloat_and_metadata_png(self):
results = list(self.icb.run(image_files=[
get_absolute_path('gradient_bloat_and_metadata.png')]))

self.assertNotEqual([], results)
self.assertRegex(results[0].message, self.MESSAGE_RE)

def test_metadata_jpeg(self):
results = list(self.icb.run(image_files=[
get_absolute_path('gradient_metadata.jpg')]))

self.assertNotEqual([], results)
self.assertRegex(results[0].message, self.MESSAGE_RE)

def test_metadata_png(self):
results = list(self.icb.run(image_files=[
get_absolute_path('gradient_metadata.png')]))

self.assertNotEqual([], results)
self.assertRegex(results[0].message, self.MESSAGE_RE)
Binary file added tests/image/test_files/gradient_bloat.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/image/test_files/gradient_bloat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/image/test_files/gradient_clean.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/image/test_files/gradient_clean.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/image/test_files/gradient_metadata.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/image/test_files/gradient_metadata.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit e8d5aea

Please sign in to comment.