1# -*- coding: utf-8 -*-
2#
3# These tests cover the basic I/O & pythonic interfaces of the Image class.
4#
5import codecs
6import io
7import os
8import os.path
9import shutil
10import struct
11import sys
12import tempfile
13
14from pytest import mark, raises
15
16from wand.image import ClosedImageError, Image
17from wand.color import Color
18from wand.compat import PY3, text, text_type
19
20try:
21    filesystem_encoding = sys.getfilesystemencoding()
22except RuntimeError:
23    unicode_filesystem_encoding = False
24else:
25    try:
26        codec_info = codecs.lookup(filesystem_encoding)
27    except LookupError:
28        unicode_filesystem_encoding = False
29    else:
30        unicode_filesystem_encoding = codec_info.name in (
31            'utf-8', 'utf-16', 'utf-16-be', 'utf-16-le',
32            'utf-32', 'utf-32-be', 'utf-32-le',
33            'mbcs'  # for Windows
34        )
35
36try:
37    import numpy as np
38except ImportError:
39    np = None
40
41
42def test_empty_image():
43    with Image() as img:
44        assert img.size == (0, 0)
45        assert repr(img) == '<wand.image.Image: (empty)>'
46
47
48def test_image_invalid_params():
49    with raises(TypeError):
50        Image(image=Image(), width=100, height=100)
51    with raises(TypeError):
52        Image(image=Image(), blob=b"blob")
53    with raises(TypeError):
54        Image(image=b"blob")
55
56
57def test_blank_image():
58    gray = Color('#ccc')
59    transparent = Color('transparent')
60    with raises(ValueError):
61        Image(width=0, height=0)
62    with Image(width=20, height=10) as img:
63        assert img[10, 5] == transparent
64    with Image(width=20, height=10, background=gray) as img:
65        assert img.size == (20, 10)
66        assert img[10, 5] == gray
67    with Image(width=20, height=10, background='#ccc') as img:
68        assert img.size == (20, 10)
69        assert img[10, 5] == gray
70
71
72def test_raw_image(fx_asset):
73    b = b"".join([struct.pack("BBB", i, j, 0)
74                  for i in range(256) for j in range(256)])
75    with raises(ValueError):
76        Image(blob=b, depth=8, width=0, height=0, format="RGB")
77    with raises(TypeError):
78        Image(blob=b, depth=8, width=256, height=256, format=1)
79    with Image(blob=b, depth=8, width=256, height=256, format="RGB") as img:
80        assert img.size == (256, 256)
81        assert img[0, 0] == Color('#000000')
82        assert img[255, 255] == Color('#ffff00')
83        assert img[64, 128] == Color('#804000')
84    with Image(filename=str(fx_asset.join('blob.rgb')),
85               depth=8, width=256, height=256, format="RGB") as img:
86        assert img.size == (256, 256)
87        assert img[0, 0] == Color('#000000')
88        assert img[255, 255] == Color('#ffff00')
89        assert img[64, 128] == Color('#804000')
90
91
92def test_clear_image(fx_asset):
93    with Image() as img:
94        img.read(filename=str(fx_asset.join('mona-lisa.jpg')))
95        assert img.size == (402, 599)
96        img.clear()
97        assert img.size == (0, 0)
98        img.read(filename=str(fx_asset.join('beach.jpg')))
99        assert img.size == (800, 600)
100
101
102def test_read_from_filename(fx_asset):
103    with Image() as img:
104        img.read(filename=str(fx_asset.join('mona-lisa.jpg')))
105        assert img.width == 402
106        img.clear()
107        with fx_asset.join('mona-lisa.jpg').open('rb') as f:
108            img.read(file=f)
109            assert img.width == 402
110            img.clear()
111        blob = fx_asset.join('mona-lisa.jpg').read('rb')
112        img.read(blob=blob)
113        assert img.width == 402
114
115
116@mark.skipif(not unicode_filesystem_encoding,
117             reason='Unicode filesystem encoding needed')
118def test_read_from_unicode_filename(fx_asset, tmpdir):
119    """https://github.com/emcconville/wand/issues/122"""
120    filename = '모나리자.jpg'
121    if not PY3:
122        filename = filename.decode('utf-8')
123    path = os.path.join(text_type(tmpdir), filename)  # workaround py.path bug
124    shutil.copyfile(str(fx_asset.join('mona-lisa.jpg')), path)
125    with Image() as img:
126        img.read(filename=text(path))
127        assert img.width == 402
128
129
130def test_read_with_colorspace(fx_asset):
131    fpath = str(fx_asset.join('cmyk.jpg'))
132    with Image(filename=fpath,
133               colorspace='srgb',
134               units='pixelspercentimeter') as img:
135        assert img.units == 'pixelspercentimeter'
136
137
138def test_read_with_extract():
139    with Image(filename='rose:', extract="10x10+10+10") as img:
140        assert (10, 10) == img.size
141    with Image() as img:
142        img.read(filename='rose:', extract="10x10+10+10")
143        assert (10, 10) == img.size
144
145
146def test_new_from_file(fx_asset):
147    """Opens an image from the file object."""
148    with fx_asset.join('mona-lisa.jpg').open('rb') as f:
149        with Image(file=f) as img:
150            assert img.width == 402
151    with raises(ClosedImageError):
152        img.wand
153    strio = io.BytesIO(fx_asset.join('mona-lisa.jpg').read('rb'))
154    with Image(file=strio) as img:
155        assert img.width == 402
156    strio.close()
157    with raises(ClosedImageError):
158        img.wand
159    with raises(TypeError):
160        Image(file='not file object')
161
162
163def test_new_from_filename(fx_asset):
164    """Opens an image through its filename."""
165    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as img:
166        assert img.width == 402
167    with raises(ClosedImageError):
168        img.wand
169    with raises(IOError):
170        Image(filename=str(fx_asset.join('not-exists.jpg')))
171
172
173@mark.skipif(not unicode_filesystem_encoding,
174             reason='Unicode filesystem encoding needed')
175def test_new_from_unicode_filename(fx_asset, tmpdir):
176    """https://github.com/emcconville/wand/issues/122"""
177    filename = '모나리자.jpg'
178    if not PY3:
179        filename = filename.decode('utf-8')
180    path = os.path.join(text_type(tmpdir), filename)  # workaround py.path bug
181    shutil.copyfile(str(fx_asset.join('mona-lisa.jpg')), path)
182    with Image(filename=text(path)) as img:
183        assert img.width == 402
184
185
186def test_new_from_blob(fx_asset):
187    """Opens an image from blob."""
188    blob = fx_asset.join('mona-lisa.jpg').read('rb')
189    with Image(blob=blob) as img:
190        assert img.width == 402
191    with raises(ClosedImageError):
192        img.wand
193
194
195def test_new_with_format(fx_asset):
196    blob = fx_asset.join('google.ico').read('rb')
197    with raises(Exception):
198        Image(blob=blob)
199    with Image(blob=blob, format='ico') as img:
200        assert img.size == (16, 16)
201
202
203def test_new_from_pseudo(fx_asset):
204    with Image() as img:
205        img.pseudo(10, 10, 'xc:none')
206        assert img.size == (10, 10)
207
208
209def test_clone(fx_asset):
210    """Clones the existing image."""
211    funcs = (lambda img: Image(image=img),
212             lambda img: img.clone())
213    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as img:
214        for func in funcs:
215            with func(img) as cloned:
216                assert img.wand is not cloned.wand
217                assert img.size == cloned.size
218            with raises(ClosedImageError):
219                cloned.wand
220    with raises(ClosedImageError):
221        img.wand
222
223
224def test_image_add():
225    with Image(filename='rose:') as a:
226        with Image(filename='rose:') as b:
227            a.image_add(b)
228        assert a.iterator_length() == 2
229    with raises(TypeError):
230        with Image(filename='rose:') as img:
231            img.image_add(0xdeadbeef)
232
233
234def test_image_get():
235    with Image(filename='rose:') as img:
236        with img.image_get() as i:
237            assert isinstance(i, Image)
238
239
240def test_image_remove():
241    with Image(filename='null:') as empty:
242        empty.image_remove()
243        assert empty.iterator_length() == 0
244
245
246def test_image_set():
247    with Image(filename='null:') as a:
248        with Image(filename='rose:') as b:
249            a.image_set(b)
250        assert a.iterator_length() == 1
251
252
253def test_image_swap():
254    with Image(width=1, height=1, background='red') as a:
255        a.read(filename='xc:green')
256        a.read(filename='xc:blue')
257        was = a.iterator_get()
258        a.image_swap(0, 2)
259        assert a.iterator_get() == was
260    with raises(TypeError):
261        a.image_swap('a', 'b')
262
263def test_ping_from_filename(fx_asset):
264    file_path = str(fx_asset.join('mona-lisa.jpg'))
265    with Image.ping(filename=file_path) as img:
266        assert img.size == (402, 599)
267
268
269def test_ping_from_blob(fx_asset):
270    blob = fx_asset.join('mona-lisa.jpg').read('rb')
271    with Image.ping(blob=blob) as img:
272        assert img.size == (402, 599)
273
274
275def test_ping_from_file(fx_asset):
276    with fx_asset.join('mona-lisa.jpg').open('rb') as fd:
277        with Image.ping(file=fd) as img:
278            assert img.size == (402, 599)
279
280
281def test_save_to_filename(fx_asset):
282    """Saves an image to the filename."""
283    savefile = os.path.join(tempfile.mkdtemp(), 'savetest.jpg')
284    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as orig:
285        orig.save(filename=savefile)
286        with raises(IOError):
287            orig.save(filename=os.path.join(savefile, 'invalid.jpg'))
288        with raises(TypeError):
289            orig.save(filename=1234)
290    assert os.path.isfile(savefile)
291    with Image(filename=savefile) as saved:
292        assert saved.size == (402, 599)
293    os.remove(savefile)
294
295
296@mark.skipif(not unicode_filesystem_encoding,
297             reason='Unicode filesystem encoding needed')
298def test_save_to_unicode_filename(fx_asset, tmpdir):
299    filename = '모나리자.jpg'
300    if not PY3:
301        filename = filename.decode('utf-8')
302    path = os.path.join(text_type(tmpdir), filename)  # workaround py.path bug
303    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as orig:
304        orig.save(filename=path)
305    with Image(filename=path) as img:
306        assert img.width == 402
307
308
309def test_save_to_file(fx_asset):
310    """Saves an image to the Python file object."""
311    buffer = io.BytesIO()
312    with tempfile.TemporaryFile() as savefile:
313        with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as orig:
314            orig.save(file=savefile)
315            orig.save(file=buffer)
316            with raises(TypeError):
317                orig.save(file='filename')
318            with raises(TypeError):
319                orig.save(file=1234)
320        savefile.seek(0)
321        with Image(file=savefile) as saved:
322            assert saved.size == (402, 599)
323        buffer.seek(0)
324        with Image(file=buffer) as saved:
325            assert saved.size == (402, 599)
326    buffer.close()
327
328
329def test_save_full_animated_gif_to_file(fx_asset):
330    """Save all frames of an animated to a Python file object."""
331    temp_filename = os.path.join(tempfile.mkdtemp(), 'savetest.gif')
332    orig_filename = str(fx_asset.join('nocomments.gif'))
333    with open(temp_filename, 'w+b') as fp:
334        with Image(filename=orig_filename) as orig:
335            orig.save(file=fp)
336    assert os.path.isfile(temp_filename)
337    with Image(filename=orig_filename) as orig:
338        with Image(filename=temp_filename) as temp:
339            assert len(orig.sequence) == len(temp.sequence)
340    os.remove(temp_filename)
341
342
343def test_save_error(fx_asset):
344    filename = os.path.join(tempfile.mkdtemp(), 'savetest.jpg')
345    fileobj = io.BytesIO()
346    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as orig:
347        with raises(TypeError):
348            orig.save()
349        with raises(TypeError):
350            orig.save(filename=filename, file=fileobj)
351
352
353def test_make_blob(fx_asset):
354    """Makes a blob string."""
355    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as img:
356        with Image(blob=img.make_blob('png')) as img2:
357            assert img2.size == (402, 599)
358            assert img2.format == 'PNG'
359        assert img.format == 'JPEG'
360        with raises(TypeError):
361            img.make_blob(123)
362    svg = b'''
363    <svg width="100px" height="100px">
364        <circle cx="100" cy="50" r="40" stroke="black"
365         stroke-width="2" fill="red" />
366    </svg>
367    '''
368    with Image(blob=svg, format='svg') as img:
369        assert img.size == (100, 100)
370        assert img.format in ('SVG', 'MVG')
371        img.format = 'PNG'
372        assert img.size == (100, 100)
373        assert img.format == 'PNG'
374        png = img.make_blob()
375    with Image(blob=png, format='png') as img:
376        assert img.size == (100, 100)
377        assert img.format == 'PNG'
378
379
380def test_convert(fx_asset):
381    """Converts the image format."""
382    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as img:
383        with img.convert('png') as converted:
384            assert converted.format == 'PNG'
385            strio = io.BytesIO()
386            converted.save(file=strio)
387            strio.seek(0)
388            with Image(file=strio) as png:
389                assert png.format == 'PNG'
390        with raises(ValueError):
391            img.convert('HONG')
392        with raises(TypeError):
393            img.convert(123)
394
395
396@mark.slow
397def test_iterate(fx_asset):
398    """Uses iterator."""
399    with Color('#000') as black:
400        with Color('transparent') as transparent:
401            with Image(filename=str(fx_asset.join('croptest.png'))) as img:
402                for i, row in enumerate(img):
403                    assert len(row) == 300
404                    if i % 30:
405                        continue  # avoid slowness
406                    if 100 <= i < 200:
407                        for x, color in enumerate(row):
408                            if x % 30:
409                                continue  # avoid slowness
410                            if 100 <= x < 200:
411                                assert color == black
412                            else:
413                                assert color == transparent
414                    else:
415                        for color in row:
416                            assert color == transparent
417                assert i == 299
418
419
420def test_slice_clone(fx_asset):
421    """Clones using slicing."""
422    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as img:
423        with img[:, :] as cloned:
424            assert img.size == cloned.size
425
426
427def test_slice_invalid_types(fx_asset):
428    """Slicing with invalid types should throw exceptions."""
429    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as img:
430        with raises(TypeError):
431            img['12']
432        with raises(TypeError):
433            img[1.23]
434        with raises(ValueError):
435            img[()]
436        with raises(ValueError):
437            img[:, :, :]
438        with raises(ValueError):
439            img[::2, :]
440        with raises(IndexError):
441            img[1:1, :]
442        with raises(IndexError):
443            img[:, 2:2]
444        with raises(TypeError):
445            img[100.0:, 100.0]
446        with raises(TypeError):
447            img['100':, '100']
448        with raises(IndexError):
449            img[500:, 900]
450        with raises(TypeError):
451            img['1', 0]
452        with raises(TypeError):
453            img[1, '0']
454    with Image(filename=str(fx_asset.join('croptest.png'))) as img:
455        with raises(IndexError):
456            img[300, 300]
457        with raises(IndexError):
458            img[-301, -301]
459
460
461def test_index_pixel(fx_asset):
462    """Gets a pixel."""
463    with Image(filename=str(fx_asset.join('croptest.png'))) as img:
464        assert img[0, 0] == Color('transparent')
465        assert img[99, 99] == Color('transparent')
466        assert img[100, 100] == Color('black')
467        assert img[150, 150] == Color('black')
468        assert img[-200, -200] == Color('black')
469        assert img[-201, -201] == Color('transparent')
470
471
472def test_index_pixel_set(fx_asset):
473    with Image(filename='rose:') as img:
474        with Color('black') as dot:
475            img[0, 0] = dot
476            assert img[0, 0] == dot
477            img[0, 0] = 'black'
478            assert img[0, 0] == dot
479        img.colorspace = 'gray'
480        with Color('gray50') as dot:
481            img[0, 0] = dot
482            assert img[0, 0] == dot
483        img.colorspace = 'cmyk'
484        with Color('cmyk(255, 0, 0, 0') as dot:
485            img[0, 0] = dot
486            assert img[0, 0] == dot
487        with raises(TypeError):
488            img[0, 0] = 1
489        with raises(TypeError):
490            img[0xDEADBEEF] = Color('black')
491        with raises(ValueError):
492            img[1, 2, 3] = Color('black')
493        with raises(TypeError):
494            img[0.5, "d"] = Color('black')
495
496
497def test_index_row(fx_asset):
498    """Gets a row."""
499    with Color('transparent') as transparent:
500        with Color('black') as black:
501            with Image(filename=str(fx_asset.join('croptest.png'))) as img:
502                for c in img[0]:
503                    assert c == transparent
504                for c in img[99]:
505                    assert c == transparent
506                for i, c in enumerate(img[100]):
507                    if 100 <= i < 200:
508                        assert c == black
509                    else:
510                        assert c == transparent
511                for i, c in enumerate(img[150]):
512                    if 100 <= i < 200:
513                        assert c == black
514                    else:
515                        assert c == transparent
516                for i, c in enumerate(img[-200]):
517                    if 100 <= i < 200:
518                        assert c == black
519                    else:
520                        assert c == transparent
521                for c in img[-201]:
522                    assert c == transparent
523
524
525def test_slice_crop(fx_asset):
526    """Crops using slicing."""
527    with Image(filename=str(fx_asset.join('croptest.png'))) as img:
528        with img[100:200, 100:200] as cropped:
529            assert cropped.size == (100, 100)
530            with Color('#000') as black:
531                for row in cropped:
532                    for col in row:
533                        assert col == black
534        with img[150:, :150] as cropped:
535            assert cropped.size == (150, 150)
536        with img[-200:-100, -200:-100] as cropped:
537            assert cropped.size == (100, 100)
538        with img[100:200] as cropped:
539            assert cropped.size == (300, 100)
540        assert img.size == (300, 300)
541        with raises(IndexError):
542            img[:500, :500]
543        with raises(IndexError):
544            img[290:310, 290:310]
545
546
547def test_equal(fx_asset):
548    """Equals (``==``) and not equals (``!=``) operators."""
549    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as a:
550        with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as a2:
551            assert a == a2
552            assert not (a != a2)
553        with Image(filename=str(fx_asset.join('sasha.jpg'))) as b:
554            assert a != b
555            assert not (a == b)
556        with a.convert('png') as a3:
557            assert a == a3
558            assert not (a != a3)
559
560
561def test_object_hash(fx_asset):
562    """Gets :func:`hash()` of the image."""
563    with Image(filename=str(fx_asset.join('mona-lisa.jpg'))) as img:
564        a = hash(img)
565        img.format = 'png'
566        b = hash(img)
567        assert a == b
568
569
570def test_issue_150(fx_asset, tmpdir):
571    """Should not be terminated with segmentation fault.
572
573    https://github.com/emcconville/wand/issues/150
574
575    """
576    with Image(filename=str(fx_asset.join('tiger_hd-1920x1080.jpg'))) as img:
577        img.format = 'pjpeg'
578        with open(str(tmpdir.join('out.jpg')), 'wb') as f:
579            img.save(file=f)
580
581
582@mark.skipif(np is None, reason='Numpy not available.')
583def test_from_array():
584    # From float values.
585    rand = np.random.rand(10, 10, 4)
586    # We should have a 10x10 image with RGBA data created.
587    with Image.from_array(rand) as img:
588        assert img.size == (10, 10)
589        assert img.alpha_channel
590    # From char values
591    red8 = np.zeros([10, 10, 3], dtype=np.uint8)
592    red8[:, :] = [0xFF, 0x00, 0x00]
593    # We should have a RED image.
594    with Image.from_array(red8) as img:
595        assert img[0, 0] == Color('#F00')
596    # From short values.
597    green16 = np.zeros([10, 10, 3], dtype=np.uint16)
598    green16[:, :] = [0x0000, 0xFFFF, 0x0000]
599    # We should have a GREEN image.
600    with Image.from_array(green16) as img:
601        assert img[0, 0] == Color('#00FF00')
602
603
604@mark.skipif(np is None, reason='Numpy not available.')
605def test_array_interface():
606    with Image(filename='rose:') as img:
607        img.alpha_channel = 'off'
608        array = np.array(img)
609        assert array.shape == (46, 70, 3)
610    with Image(filename='rose:') as img:
611        img.alpha_channel = 'off'
612        img.transform_colorspace('gray')
613        array = np.array(img)
614        assert array.shape == (46, 70, 1)
615    with Image(filename='rose:') as img:
616        img.alpha_channel = 'off'
617        img.transform_colorspace('cmyk')
618        array = np.array(img)
619        assert array.shape == (46, 70, 4)
620
621
622@mark.skipif(np is None, reason='Numpy not available.')
623def test_numpy_array_hairpinning():
624    with Image(filename='rose:') as left:
625        with Image.from_array(left) as right:
626            assert left.size == right.size
627
628
629def test_data_url():
630    with Image(filename='rose:') as img:
631        img.format = 'PNG'
632        data = img.data_url()
633        assert data.startswith('data:image/png;base64,')
634