diff --git a/calculate/utils/images.py b/calculate/utils/images.py new file mode 100644 index 0000000..ab6062e --- /dev/null +++ b/calculate/utils/images.py @@ -0,0 +1,92 @@ +import os +import hashlib +from calculate.utils.files import Process, write_file, read_file + +class ImageMagickError(Exception): + pass + +class ImageMagick: + def __init__(self, prefix='/'): + self.prefix = prefix + self.init_commands(prefix) + self.default_opts = [] + + @property + def available(self): + return self.convert_cmd and self.identify_cmd + + @property + def chroot(self): + return self.prefix != '/' + + def init_commands(self, prefix): + self.convert_cmd = "/usr/bin/convert" + self.identify_cmd = "/usr/bin/identify" + self.chroot_cmd = "/bin/chroot" + self.bash_cmd = "/bin/bash" + if not os.path.exists(os.path.join(prefix, self.convert_cmd[1:])): + self.convert_cmd = None + if not os.path.exists(os.path.join(prefix, self.identify_cmd[1:])): + self.identify_cmd = None + + def trim_prefix_path(self, filename): + retpath = "/%s" % os.path.relpath(filename, self.prefix) + if retpath.startswith("/.."): + return None + return retpath + + def get_image_resolution(self, source): + if self.chroot: + identify = Process(self.chroot_cmd, self.prefix, + self.bash_cmd, "-c", + " ".join([self.identify_cmd, + "-format '%w %h'", source])) + else: + identify = Process(self.identify_cmd, "-format", "%w %h", source) + if identify.success(): + swidth, _sep, sheight = identify.read().strip().partition(" ") + if swidth.isdigit() and sheight.isdigit(): + return int(swidth), int(sheight) + return None + + def convert(self, source, target, *opts): + command = [self.convert_cmd, "-quality", "95", + source] + command.extend(self.default_opts) + command.extend(opts) + command.append(target) + if self.chroot: + convert = Process(self.chroot_cmd, self.prefix, + self.bash_cmd, "-c", + " ".join(command)) + else: + convert = Process(*command) + if convert.success(): + return True + else: + print(convert.read_error()) + return False + + def convert_resize_crop_center(self, source, target, height, width): + #if ((width == self.source_width and height == self.source_height) and + # (source.rpartition('.')[2] == target.rpartition('.')[2])): + # with write_file(target) as sf: + # sf.write(read_file(source)) + # return True + res = "%dx%d" % (width, height) + + return self.convert(source, target, "-quality", "95", + "-resize", "%s^" % res, + "-strip", "-gravity", "center", + "-crop", "%s+0+0" % res) + + def convert_resize_gfxboot(self, source, target, height, width): + res = "%dx%d" % (width, height) + + return self.convert(source, target, "-quality", "95", + "-resize", "%s^" % res, + "-strip", "-gravity", "center", + "-crop", "%s+0+0" % res, + "-sampling-factor", "2x2", + "-interlace", "none", + "-set", "units", "PixelsPerSecond") diff --git a/conftest.py b/conftest.py index bf258f3..3590366 100644 --- a/conftest.py +++ b/conftest.py @@ -162,3 +162,19 @@ def DictionariesWithoutSections(): ResultDictionary = OrderedDict(**ParamLine1, **ParamLine3) return (OriginalDictionary, TemplateDictionary, ResultDictionary) + +import pytest + + +def pytest_addoption(parser): + parser.addoption( + "--chroot-test", action="store_true", default=False, help="run chroot tests" + ) + +def pytest_collection_modifyitems(config, items): + if config.getoption("--chroot-test"): + return + skip_chroot = pytest.mark.skip(reason="need --chroot option to run") + for item in items: + if "chroot" in item.keywords: + item.add_marker(skip_chroot) diff --git a/pytest.ini b/pytest.ini index 4d95a07..efe8c7f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -24,6 +24,7 @@ markers = files_utils: marker for running tests for calculate.utils.files module. package_utils: marker for running tests for calculate.utils.contents module. + images_utils: marker for running tests for calculate.utils.images module. gentoo: marker for running tests for utils.gentoo calculateini: marker for running tests for utils.calculateini @@ -41,3 +42,5 @@ markers = scripts: marker for testing of the scripts. commands: marker for testing of the commands. server: marker for testing of the server. + + chroot: marker for testing running by chroot diff --git a/tests/utils/test_images.py b/tests/utils/test_images.py new file mode 100644 index 0000000..c78f681 --- /dev/null +++ b/tests/utils/test_images.py @@ -0,0 +1,215 @@ +import pytest +from calculate.utils.images import ImageMagick, ImageMagickError +import os +from calculate.utils.files import Process + +@pytest.mark.images_utils +def test_imagemagick_initialization(): + im = ImageMagick() + assert im.available + assert not im.chroot + + im = ImageMagick(prefix='/mnt/somepath') + assert not im.available + assert im.chroot + + +@pytest.mark.images_utils +@pytest.mark.parametrize('case', + [ + { + "name": "simple path", + "chroot": "/mnt/install", + "source": "/mnt/install/usr/share/pixmap/image.jpg", + "result": "/usr/share/pixmap/image.jpg", + }, + { + "name": "relative", + "chroot": "/mnt/install", + "source": "/mnt/install/usr/share/../pixmap/image.jpg", + "result": "/usr/pixmap/image.jpg", + }, + { + "name": "first level", + "chroot": "/mnt/install", + "source": "/mnt/install/image.jpg", + "result": "/image.jpg", + }, + { + "name": "Wrong", + "chroot": "/mnt/install", + "source": "/mnt/image.jpg", + "result": None, + }, + ], + ids=lambda x:x["name"]) +def test_imagemagick_trim_prefix_path(case): + im = ImageMagick(case['chroot']) + assert im.trim_prefix_path(case["source"]) == case["result"] + +@pytest.mark.images_utils +@pytest.mark.parametrize('case', + [ + { + "name": "PNG file", + "image": "tests/utils/testfiles/file.png", + "result": (48,48), + }, + { + "name": "JPEG file", + "image": "tests/utils/testfiles/file.jpg", + "result": (320,180), + }, + { + "name": "No file", + "image": "tests/utils/testfiles/file2.jpg", + "result": None, + }, + { + "name": "Wrong file", + "image": "tests/utils/testfiles/wrong.jpg", + "result": None, + }, + ], + ids=lambda x:x["name"]) +def test_imagemagick_get_resolutions(case): + im = ImageMagick() + assert im.get_image_resolution(case["image"]) == case["result"] + +@pytest.fixture +def chroot_test(): + chrootpath = "/mnt/testchroot" + assert os.getuid() == 0, "Need superuser privileges" + if os.path.exists(chrootpath): + os.unlink(chrootpath) + os.symlink("/", chrootpath) + try: + yield chrootpath + finally: + os.unlink(chrootpath) + +@pytest.mark.chroot +@pytest.mark.images_utils +def test_chroot_imagemagick_get_resolution(chroot_test): + im = ImageMagick(chroot_test) + curpath = os.getcwd() + image_path = os.path.join(curpath, "tests/utils/testfiles/file.png") + assert im.get_image_resolution(image_path) == (48,48) + +def get_histogram(image, remap_image): + p = Process("/usr/bin/convert", image, "-remap", remap_image, "-format", "%c", "histogram:info:-") + return p.read() + +def get_verbose_image_info(image): + p = Process("/usr/bin/identify", "-verbose", image) + return p.read() + +@pytest.mark.images_utils +@pytest.mark.parametrize('case', + [ + { + # проверка, пропорционального уменьшения + "name": "Origin test", + "resize": (16,32), + "result": """192: (0,0,0) #000000 black + 64: (0,255,0) #00FF00 lime + 256: (255,255,255) #FFFFFF white""" + }, + { + # проверка, что при изменении размера только по горизонтали + # удаляются только части изображения справа и слева + # в исходном изображении на этих частях находится белый фон + "name": "Shrink horizontal", + "resize": (16,16), + "result": """192: (0,0,0) #000000 black + 64: (0,255,0) #00FF00 lime""" + }, + { + # проверка, что при уменьшении изображения первоначально оно сдавливается + # по вертикали а затем обрезаются части слева и справа + "name": "Shrink all", + "resize": (8,8), + "result": """48: (0,0,0) #000000 black + 16: (0,255,0) #00FF00 lime""" + }, + { + # проверка, пропорционального уменьшения + "name": "Shrink proportionately", + "resize": (8,16), + "result": """48: (0,0,0) #000000 black + 16: (0,255,0) #00FF00 lime + 64: (255,255,255) #FFFFFF white""" + }, + { + # проверка, пропорционального увеличения + "name": "Increase size proportionately", + "resize": (32,64), + "result": """768: (0,0,0) #000000 black + 256: (0,255,0) #00FF00 lime + 1024: (255,255,255) #FFFFFF white""" + }, + { + # проверка увеличения и обрезки по горизонтали + "name": "Increase size and cut", + "resize": (32,32), + "result": """768: (0,0,0) #000000 black + 256: (0,255,0) #00FF00 lime""" + }, + { + # проверка увеличения по горизонтали + # в этом случае будет отрезан верх и низ + # поэтому на выходе нет зелёного цвета + "name": "Increase horizontal size", + "resize": (16,48), + "result": """384: (0,0,0) #000000 gray(0) + 384: (255,255,255) #FFFFFF gray(255)""" + }, + ], + ids=lambda x:x["name"]) +def test_imagemagick_convert(case): + image_path = "tests/utils/testfiles/origin.png" + output_file = "tests/utils/testfiles/test_output5.png" + if os.path.exists(output_file): + os.unlink(output_file) + im = ImageMagick() + im.default_opts = ["-filter","box"] + assert im.convert_resize_crop_center(image_path, output_file, *case["resize"]) + histogram = get_histogram(output_file, image_path) + discard_space = lambda x: x.replace(" ","").replace("\n","") + assert discard_space(histogram) == discard_space(case["result"]) + +@pytest.mark.chroot +@pytest.mark.images_utils +def test_chroot_imagemagick_convert_center(chroot_test): + curpath = os.getcwd() + image_path = "tests/utils/testfiles/origin.png" + output_file = "tests/utils/testfiles/test_output.png" + image_path = os.path.join(curpath, image_path) + output_file = os.path.join(curpath, output_file) + result = """48: (0,0,0) #000000 black + 16: (0,255,0) #00FF00 lime""" + if os.path.exists(output_file): + os.unlink(output_file) + im = ImageMagick(chroot_test) + im.default_opts = ["-filter","box"] + assert im.convert_resize_crop_center(image_path, output_file, 8, 8) + histogram = get_histogram(output_file, image_path) + discard_space = lambda x: x.replace(" ","").replace("\n","") + assert discard_space(histogram) == discard_space(result) + +@pytest.mark.images_utils +def test_imagemagick_convert_gfxboot(): + output_file = "tests/utils/testfiles/test_output.jpg" + image_path = "tests/utils/testfiles/origin.png" + im = ImageMagick() + im.convert_resize_gfxboot(image_path, output_file, 32, 32) + assert "sampling-factor: 2x2" in get_verbose_image_info(output_file) + +@pytest.mark.images_utils +def test_clear_imagemagick_convert(): + for output_file in ( + "tests/utils/testfiles/test_output.png", + "tests/utils/testfiles/test_output.jpg" + ): + if os.path.exists(output_file): + os.unlink(output_file) diff --git a/tests/utils/testfiles/file.jpg b/tests/utils/testfiles/file.jpg new file mode 100644 index 0000000..3d78a81 Binary files /dev/null and b/tests/utils/testfiles/file.jpg differ diff --git a/tests/utils/testfiles/file.png b/tests/utils/testfiles/file.png new file mode 100644 index 0000000..b9cc28d Binary files /dev/null and b/tests/utils/testfiles/file.png differ diff --git a/tests/utils/testfiles/origin.png b/tests/utils/testfiles/origin.png new file mode 100644 index 0000000..307f28d Binary files /dev/null and b/tests/utils/testfiles/origin.png differ diff --git a/tests/utils/testfiles/wrong.jpg b/tests/utils/testfiles/wrong.jpg new file mode 100644 index 0000000..41bacf8 --- /dev/null +++ b/tests/utils/testfiles/wrong.jpg @@ -0,0 +1 @@ +NOFILE