From 933ab35b1c2b012384dfa20d086b7c00ea39c9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B8=D1=80=D0=B5=D1=86=D0=BA=D0=B8=D0=B9=20=D0=9C?= =?UTF-8?q?=D0=B8=D1=85=D0=B0=D0=B8=D0=BB?= Date: Thu, 26 Nov 2020 16:14:07 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20utils.images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В модуль добавлен объект ImageMagick для получения размеров изображения, изменения размеров изображения для формата backgrounds, с поддержкой запуска через chroot. --- calculate/utils/images.py | 92 +++++++++++++ conftest.py | 16 +++ pytest.ini | 3 + tests/utils/test_images.py | 215 +++++++++++++++++++++++++++++++ tests/utils/testfiles/file.jpg | Bin 0 -> 13393 bytes tests/utils/testfiles/file.png | Bin 0 -> 4589 bytes tests/utils/testfiles/origin.png | Bin 0 -> 5505 bytes tests/utils/testfiles/wrong.jpg | 1 + 8 files changed, 327 insertions(+) create mode 100644 calculate/utils/images.py create mode 100644 tests/utils/test_images.py create mode 100644 tests/utils/testfiles/file.jpg create mode 100644 tests/utils/testfiles/file.png create mode 100644 tests/utils/testfiles/origin.png create mode 100644 tests/utils/testfiles/wrong.jpg 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 0000000000000000000000000000000000000000..3d78a819a0ab1cd4c2010e8ab3ae87ac4384112d GIT binary patch literal 13393 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 M%s7Ti&X2YK13E1D_5c6? literal 0 HcmV?d00001 diff --git a/tests/utils/testfiles/file.png b/tests/utils/testfiles/file.png new file mode 100644 index 0000000000000000000000000000000000000000..b9cc28d6e0bf5d8cfe3393132446b9185c3d1383 GIT binary patch literal 4589 zcmVW5qe2P zK~!ko?V4GP9M^S*zgl`kK$d;_lq9{=s4k-?YoRzcpOm|Q3-Boq(tvqy-qG-~l zBpYym6i}$@>gwwAo%7#w&pqAne|*z^P0zf32XJrLe>#L;@KHj$uy7$s(=90CxOyFf#DuBBkE6f_x6t-+P!0B-|^m^ zdq%r^`v$t(TVpL@zZUeE=sH+JlCKGOWg}f(SzcaTo|~SSzHt7>>+_cC;$fF1)75GeTQ~G{LB}he)Qp! zhjw&FTcZKpr)$@qrt2D-rq^F;plcesp`)aWtAS~lxTZngmhRL_y7Yrr-Z=Hv^WXTV z={H|~6(}^K@hbzkDw!a<+19%6zGqK9{p?px9^ThI)DcJ5^$%{;Ktn^*>TXoBt_s_> z357x*KsBCf7zQ4nmsG`ee>OR@@yZXs|F3U-=j;Di%5JU!_U%N&xD9~1WDIX#VBpTL zJn-OP31EE|rSCI$>yov7K9R;C(U!WHTAgo_UK%w24xoz|UU&32&czoyqePyz$CQ%v`?6 z=w0`)V`M+N*NdxbRLWIi$!7WodOY!t?p-Bmc(0s$>n*_gMN%?%!*`V$~e{(Fm@lq3IgYSUk9rcjC(vZ@wX=%->2%#x1<>1LAty@MHV# zdft|JVEDF0`Val~@W{vkzu#wE%{H4`C8lPlxqSIFf#3`+;VbmCFEP}Up}!+Vdpt!f zFh{agF*N3vmCP{a~8wpBq% zliY?wYEfX7!SD#0J57Rd2g@mu&s8xzE=^4uak~jmpCXy`knG=&86Q9z9&$B7uIdo< z=)`;`UPHrc8b(KV?@0U5@ZROg@!6ZzO}7H@`eMmlp?FJg&9d3d7MWkokjt#`cV8Hw zkTUqmi?7jlq=;RG^*N2qN`OGnWZxZ4bPnn)Ew1BKJ!IDu*>w-m_8Q46Kqu-LSCJy$DpaLy`yDY-#x&O8pWS`W2(Gq?$-lNBd%r! zm)5tKm|kHuo#S}F;`e^%Nt$CWrL@7)`5@D00!*BVv61o8yE{nFP?)tjm+F?ELPnCB zg>qhi>*6Jb=9-j?HmmbhG+kq$=OP=IUc|0c8Z$XaSCOkatd}hWn%>;qzps(fwcC-9 z7YMk%*ia@{*7D^VmLot39zMF8OH=3g&sP_jnk&;*s!^zhk-DF!zZ@kNRpg6l^7%5k ztYCdvaQU4yT|476TM}fDe9mF=?GoDtOq@iXfjtrA#!GC>m?U@J2Zjk~xUNgt7Pv0> znp^vU7?8cOPBL#Xi2YU9Y_H{txL2VygR_&<^u;Y+o3^P1_TUtA^mUavcGAEX$g?nC zq_E**>Vk_a17tHWc*iL5=qg@S#1|CAV$B4i-7L@g2xX^nWU+LScC-X>w&S8g`j-t3y!7mfy8P5V&I_rmFQmRC3EPnb0Ob3FKv&I3;v z7@-_1OL_7eI&+tFGR|J|`Y@sHL-Zaz1lZ_)n?k8TIO=8Rq5bSR@dQflVtKm8!sR0Q zVw&dg%WO=&gk33t3vNU096(v!`2Gw#wwL(Q zmvn~qSE!UMN+~btD~ic;8mUSry6H#Ly)<|A5N+=T&A{i=aV$kKU&GZ*yrCFvJMTsm zhsb9Hg)IwzNI>=nWEV~&m8wU<1guZqU2m zpPBo2^ISju=IBrlW2&4$&H+91C zW@3YfsOJ2XbBfK)4O+W%_~bOTN&)4%DAz?Qg|IE>c55L8Se0C^YI?lUqfb1@OaJ~o zGAk)`O%W=>)Q<|Js}-6$LO8adT2k037=7Z`2!FjK6sQYqq;ODN4i_bNhu7uPAFq=V*yQXm~imbW%B0%&T9Wa6y} zLeVh0j~qbLHAJ9|C+~Zdzxk`b;GyB2OrFVMRUJgtpkzncapW)uAAOYOjyA9>q)&ej zrCx*DRz(avnwW9 z%Z#(Wu}LD-%u`4EY3dvy91d}C?{323Fot1p;rv++96Zdy(J^Mvzs@s{4D#R;zt7U@ z0-b$hZ0p=s*Q!!+;FVY2U`us!_{b4n|DSI#J9UO)K2NonAt~m0_K6d$%%0`juT3)V z+k-!zL^#6TTAI7GaQdfTE37Te->i3Zs|AfAYGr(pSogl6!GllTJ!)E34coR6LZW#h zEU#{&8#*m_jt6YNfojcH!NZXI^>XMOO;D0U&M%Km+tz zJ|iRB29AE=vHP16iDq0^F*!Mjrs;HcZ>OVogqth?6uO~(Wt+BPixcA(6M{F-#PQ{g$oxZ-@9H$ zw_4Q8Mw_otSXnr8{=&ud$k1RCAdyJ$+0Q=B> za+%YoPjmUwB?kKYNi;Vz(BBV$-{+^bCCS|U9K~V@$FcEvOe&Q!Jw2lg3=CkJb+dAB zzKO_Wuoq|W{`KGDI6}MKu7umjMi#I+d+Fjkmi5qMKA#VOP$dajr_-CIWHQmDq{OyuYPD*^t97X;sgP3OI0DrGSy@?OWn~r1aR^5uM57T*A&8)n zOeTrPV}!$D;;|TBuMg)#5x5HBV%rwNaXvPHH90vsJ3cTW7G;FK(!8$-m zlvF6U4r0|lSp+O8W##R+-?{wcryf6CtJS|elgYZNRH~BR++15+S}rA9lS8eot%25# z4u<#bC*b$v_xmtSv;Iqt>we%}zrS%VkxF6NHg3b?u2M)@fA4w;IUgeeTsI$-Pn|k- zVRG`KySlo%H9a#keR+Cjd~J1gVPRn*)7IYJ`PHxf<)0lma3J6@JzQ;QxD9U#N8I4m z8_utj%{9ZNx^~^N?T;0Kt5Fa@etdlV)aRc0{FhUy)T)%S0OT6IdvkbX_(U`kkzS7n zB_;CbWbk20T<=}gkd3^0eBy@EgR2znKLzmbE#LVpM>UW?2uUFnkzL~scw3Kx%yVspJ zH1A#)rTWaQ>QRuQk$paVZ$4j0XR_J38y7UcivLO&jmB%Jar|{{vav4S;{UhbpzVJF XHPJ@T{-`CS00000NkvXXu0mjfTmlhL5CNGa69^=kFd0aKT0vAq zDgss)mAbWnD}H}rMWjekRK%*aRZ(<7QKTqhtD?2S&ICl`-?q9nO(c@FE;A|yi-3%P9z|4Y4Gdsr zJq*AmwTeVCU9MP@n$hiRzjVNFh2JPIE0>0P{)Iz+2O9!BLcjNt&R^enH0#|DJ^W9m zJsTKU_B1D7&K&bqY?F^UZenipyMO+rbJlFB|MtxI!@0c+l8l&pV#%l-rK{E+sBI|t zT0MXFp=Yj_ZmcY8?K_edAUoiBp!NuSthVrMbry1EW=p}6zNYpC{*Rh`HeAzfiqA;A zBJaGbauacniN?xCKicY3hTO@y-g?=U=iB)tfgcvI>P!z~&8EDX!pF0&R7QMPb0lcX z9G6=~qaM`S`~A%G8Du!+bmo$w^S`ytqr#1ZJh0%>A)uR0oN58Yq zp>9;$xUX*APIs_Lz9U@Oodw?X2d2hY?K@b|vb+BJs`JO*@A5pL%>J-BKRLXy{O*E9 zJlmLQLH0lU6oz{hxtQVEQWzMGJ4b2NwO0jbJXRD>2^SsmaF1)mH7~B`wGf78X zid5eZKUtmEBSrH!bl+cg<%1M$zxJZ|PV=|s%?X7IOCm4zHq`lSsKIn|EB7>qxmUG2 zHUGvKGZ5ds(ebm-|8_>PMEGTITE$;>%;y1D2imUITv+&s-gqj(-{;q*#o;SH-PVzC z&SS>T71w5=?_a;xmu8!G&-aPTz_uCRIGq^t`ERl3qV)-p52WmxMVlWw?Q0QC;r71U z)B`$rwX}V5(*^H8lIoS6r`BT%qVohR_Bdosx3_*8T5t2D^x+ZCuH3~1i=T-@vdb9NE;<$C>%MTJy8=N11opv_4D-h(Z-q@0t?V+^!aqg@{@Zsz+b9)~B zaGyl7TA~&TWfGzAd9UMfN*5LhLK>!zJ2r2wxYBL(w2+x6WYgw2yQTf$>$)jui(70u zt@?r8dZw2TwP0~-ef_FqTXXXF?R4C^7a_Q^SMF=G9jI z*`~E!%fyER`L*h<(!RWg?Or}dJC7>fi!Xd!)V=onk%P2 z0DA4?)Y?T=R?Q2F6Ohz%o{5Pb2M_8}e;%)|wzd~^6;I?wWNogKk*=M&y#YPaMsbca z%ul_05|AytJp4Cd2FZX0f~5+`F;W>IL{J!DQkhg5 zMQl=MFv$M403TJVcoCxDAqspXASYv(o(F5tD!rNiDj!;p%&o}dZRgLEnl)M{VyFks@07x7-{VTi(yKrjL} zAnB+A7H7aZ%yVQ=y(Zl-GG@8~CQO#RHA)qTCuNB|(&rP2R5t8G$e5(o>Mb4wdL&Y* z7{=+-QH=$oRDiGs*5Zj6aAx{Tc-(*3;gyeLA-4#{6CsLpf<+<{kck9%N<^Vn@+?1L zI*qQBtJoAK7skIBFpDBrF_;vHt>nm=9Hvr9V~wDa=nNR7Q@{ijPEJ+hJW4v93o|%0 z3WuiTP?&6dy*MnnAB8m)k}DW2E<~eG9YGO}s`1i>G$XSjsFXMrSLH{8{ruPz?o@^! zg{fe&DUgCgr*K(pg`6RWnOyl`HiWReK$%29W>9Gsa*IW#fiM+%jlP z6q$?|b^jaB3+OOQAc`3gG&LLzm#4rA?Daga0uM7q;G2{IL(P)kd8mKG@dwK?1ouTy z^N9G7aN6K#a3|5IEm8r1rF(ghf>2RFHb5D$(y|1&j)PN*WJs3;l0l>6=9k$Ghzc`8C>)rCHxa(hcnew98Su7L zna|5;V={~h5V2~R>)SY*0huTjJ3%fdV_ zl>dSo;XhQAzvn&@Hs~!x^k%#{ld*84?zQ1x0UTrqRV!ef0eM~OBO!ybj5sjxoCj_A zL4==c;PAmVR2D?%{DFT%<@N_gz^UH`c_V$_%Jo*RH&Wn@z;CPTtz2)Uz#DwmC8I-vAUzr>kTL1t6 literal 0 HcmV?d00001 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