# vim: set fileencoding=utf-8 :
import sys
import os
import shutil
import tempfile
import pytest

import pyvips
from helpers import *

class TestForeign:
    tempdir = None

    @classmethod
    def setup_class(cls):
        cls.tempdir = tempfile.mkdtemp()

        cls.colour = pyvips.Image.jpegload(JPEG_FILE)
        cls.rgba = pyvips.Image.new_from_file(RGBA_FILE)
        cls.mono = cls.colour.extract_band(1).copy()
        # we remove the ICC profile: the RGB one will no longer be appropriate
        cls.mono.remove("icc-profile-data")
        cls.cmyk = cls.colour.bandjoin(cls.mono)
        cls.cmyk = cls.cmyk.copy(interpretation=pyvips.Interpretation.CMYK)
        cls.cmyk.remove("icc-profile-data")

        im = pyvips.Image.new_from_file(GIF_FILE)
        cls.onebit = im[1] > 128

        all = [cls.mono, cls.colour, cls.cmyk]
        # and alpha variants of all of them
        alpha = [x.bandjoin(255) for x in all]
        # and with a second alpha
        alpha2 = [x.bandjoin(255) for x in alpha]

        cls.all = all + alpha + alpha2

    @classmethod
    def teardown_class(cls):
        shutil.rmtree(cls.tempdir, ignore_errors=True)
        cls.colour = None
        cls.rgba = None
        cls.mono = None
        cls.cmyk = None
        cls.onebit = None
        cls.all = None

    # we have test files for formats which have a clear standard
    def file_loader(self, loader, test_file, validate):
        im = pyvips.Operation.call(loader, test_file)
        validate(im)
        im = pyvips.Image.new_from_file(test_file)
        validate(im)

    def buffer_loader(self, loader, test_file, validate):
        with open(test_file, 'rb') as f:
            buf = f.read()

        im = pyvips.Operation.call(loader, buf)
        validate(im)
        im = pyvips.Image.new_from_buffer(buf, "")
        validate(im)

    def save_load(self, format, im):
        x = pyvips.Image.new_temp_file(format)
        im.write(x)

        assert im.width == x.width
        assert im.height == x.height
        assert im.bands == x.bands
        max_diff = (im - x).abs().max()
        assert max_diff == 0

    def save_load_file(self, format, options, im, max_diff=0):
        # yuk!
        # but we can't set format parameters for pyvips.Image.new_temp_file()
        filename = temp_filename(self.tempdir, format)

        im.write_to_file(filename + options)
        x = pyvips.Image.new_from_file(filename)

        assert im.width == x.width
        assert im.height == x.height
        assert im.bands == x.bands
        assert (im - x).abs().max() <= max_diff
        x = None

    def save_load_buffer(self, saver, loader, im, max_diff=0, **kwargs):
        buf = pyvips.Operation.call(saver, im, **kwargs)
        x = pyvips.Operation.call(loader, buf)

        assert im.width == x.width
        assert im.height == x.height
        assert im.bands == x.bands
        assert (im - x).abs().max() <= max_diff

    def save_buffer_tempfile(self, saver, suf, im, max_diff=0):
        filename = temp_filename(self.tempdir, suf)

        buf = pyvips.Operation.call(saver, im)
        f = open(filename, 'wb')
        f.write(buf)
        f.close()

        x = pyvips.Image.new_from_file(filename)

        assert im.width == x.width
        assert im.height == x.height
        assert im.bands == x.bands
        assert (im - x).abs().max() <= max_diff

    def test_vips(self):
        self.save_load_file(".v", "", self.colour)

        # check we can save and restore metadata
        filename = temp_filename(self.tempdir, ".v")
        self.colour.write_to_file(filename)
        x = pyvips.Image.new_from_file(filename)
        before_exif = self.colour.get("exif-data")
        after_exif = x.get("exif-data")

        assert len(before_exif) == len(after_exif)
        for i in range(len(before_exif)):
            assert before_exif[i] == after_exif[i]

        # https://github.com/libvips/libvips/issues/1847
        filename = temp_filename(self.tempdir, ".v")
        x = pyvips.Image.black(16, 16) + 128
        x.write_to_file(filename)

        x = pyvips.Image.new_from_file(filename)
        assert x.width == 16
        assert x.height == 16
        assert x.bands == 1
        assert x.avg() == 128

        x = None

    @skip_if_no("jpegload")
    def test_jpeg(self):
        def jpeg_valid(im):
            a = im(10, 10)
            # different versions of libjpeg decode have slightly different
            # rounding
            assert_almost_equal_objects(a, [141, 127, 90], threshold=3)
            profile = im.get("icc-profile-data")
            assert len(profile) == 564
            assert im.width == 290
            assert im.height == 442
            assert im.bands == 3

        self.file_loader("jpegload", JPEG_FILE, jpeg_valid)
        self.save_load("%s.jpg", self.mono)
        self.save_load("%s.jpg", self.colour)

        self.buffer_loader("jpegload_buffer", JPEG_FILE, jpeg_valid)
        self.save_load_buffer("jpegsave_buffer", "jpegload_buffer",
                              self.colour, 80)

        for image in self.all:
            target = pyvips.Target.new_to_memory()
            image.jpegsave_target(target)

        # see if we have exif parsing: our test image has this field
        x = pyvips.Image.new_from_file(JPEG_FILE)
        if x.get_typeof("exif-ifd0-Orientation") != 0:
            # we need a copy of the image to set the new metadata on
            # otherwise we get caching problems

            # can set, save and load new orientation
            x = pyvips.Image.new_from_file(JPEG_FILE)
            x = x.copy()

            x.set("orientation", 2)

            filename = temp_filename(self.tempdir, '.jpg')
            x.write_to_file(filename)

            x = pyvips.Image.new_from_file(filename)
            y = x.get("orientation")
            assert y == 2

            # can remove orientation, save, load again, orientation
            # has reset
            x = x.copy()
            x.remove("orientation")

            filename = temp_filename(self.tempdir, '.jpg')
            x.write_to_file(filename)

            x = pyvips.Image.new_from_file(filename)
            y = x.get("orientation")
            assert y == 1

            # autorotate load works
            x = pyvips.Image.new_from_file(JPEG_FILE)
            x = x.copy()

            x.set("orientation", 6)

            filename = temp_filename(self.tempdir, '.jpg')
            x.write_to_file(filename)

            x1 = pyvips.Image.new_from_file(filename)
            x2 = pyvips.Image.new_from_file(filename, autorotate=True)
            assert x1.width == x2.height
            assert x1.height == x2.width

            # sets incorrect orientation, save, load again, orientation
            # has reset to 1
            x = x.copy()
            x.set("orientation", 256)

            filename = temp_filename(self.tempdir, '.jpg')
            x.write_to_file(filename)

            x = pyvips.Image.new_from_file(filename)
            y = x.get("orientation")
            assert y == 1

            # can set, save and reload ASCII string fields
            x = pyvips.Image.new_from_file(JPEG_FILE)
            x = x.copy()

            x.set_type(pyvips.GValue.gstr_type,
                       "exif-ifd0-ImageDescription", "hello world")

            filename = temp_filename(self.tempdir, '.jpg')
            x.write_to_file(filename)

            x = pyvips.Image.new_from_file(filename)
            y = x.get("exif-ifd0-ImageDescription")
            # can't use == since the string will have an extra " (xx, yy, zz)"
            # format area at the end
            assert y.startswith("hello world")

            # can set, save and reload UTF16 string fields ... pyvips is
            # utf8, but it will be coded as utf16 and back for the XP* fields
            x = pyvips.Image.new_from_file(JPEG_FILE)
            x = x.copy()

            x.set_type(pyvips.GValue.gstr_type, "exif-ifd0-XPComment", u"йцук")

            filename = temp_filename(self.tempdir, '.jpg')
            x.write_to_file(filename)

            x = pyvips.Image.new_from_file(filename)
            y = x.get("exif-ifd0-XPComment")
            # can't use == since the string will have an extra " (xx, yy, zz)"
            # format area at the end
            assert y.startswith(u"йцук")

            # can set/save/load UserComment, a tag which has the
            # encoding in the first 8 bytes ... though libexif only supports
            # ASCII for this
            x = pyvips.Image.new_from_file(JPEG_FILE)
            x = x.copy()

            x.set_type(pyvips.GValue.gstr_type,
                       "exif-ifd2-UserComment", "hello world")

            filename = temp_filename(self.tempdir, '.jpg')
            x.write_to_file(filename)

            x = pyvips.Image.new_from_file(filename)
            y = x.get("exif-ifd2-UserComment")
            # can't use == since the string will have an extra " (xx, yy, zz)"
            # format area at the end
            assert y.startswith("hello world")

    @skip_if_no("jpegsave")
    def test_jpegsave(self):
        im = pyvips.Image.new_from_file(JPEG_FILE)

        q10 = im.jpegsave_buffer(Q=10)
        q10_subsample_auto = im.jpegsave_buffer(Q=10, subsample_mode="auto")
        q10_subsample_on = im.jpegsave_buffer(Q=10, subsample_mode="on")
        q10_subsample_off = im.jpegsave_buffer(Q=10, subsample_mode="off")

        q90 = im.jpegsave_buffer(Q=90)
        q90_subsample_auto = im.jpegsave_buffer(Q=90, subsample_mode="auto")
        q90_subsample_on = im.jpegsave_buffer(Q=90, subsample_mode="on")
        q90_subsample_off = im.jpegsave_buffer(Q=90, subsample_mode="off")

        # higher Q should mean a bigger buffer
        assert len(q90) > len(q10)

        assert len(q10_subsample_auto) == len(q10)
        assert len(q10_subsample_on) == len(q10_subsample_auto)
        assert len(q10_subsample_off) > len(q10)

        assert len(q90_subsample_auto) == len(q90)
        assert len(q90_subsample_on) < len(q90)
        assert len(q90_subsample_off) == len(q90_subsample_auto)

        # A non-zero restart_interval should result in a bigger file.
        # Otherwise, smaller restart intervals will have more restart markers
        # and therefore be larger
        r0 = im.jpegsave_buffer(restart_interval=0)
        r10 = im.jpegsave_buffer(restart_interval=10)
        r2 = im.jpegsave_buffer(restart_interval=2)
        assert len(r10) > len(r0)
        assert len(r2) > len(r10)

        # we should be able to reload jpegs with extra MCU markers
        im0 = pyvips.Image.jpegload_buffer(r0)
        im10 = pyvips.Image.jpegload_buffer(r10)
        assert im0.avg() == im10.avg()

    @skip_if_no("jpegsave")
    def test_jpegsave_exif(self):
        def exif_valid(im):
            assert im.get("exif-ifd2-UserComment").find("Undefined, 21 components, 21 bytes") != -1
            assert im.get("exif-ifd0-Software").find("ASCII, 14 components, 14 bytes") != -1
            assert im.get("exif-ifd0-XPComment").find("Byte, 28 components, 28 bytes") != -1

        def exif_removed(im):
            assert im.get_typeof("exif-ifd2-UserComment") == 0
            assert im.get_typeof("exif-ifd0-Software") == 0
            assert im.get_typeof("exif-ifd0-XPComment") == 0

        # first make sure we have exif support
        im = pyvips.Image.new_from_file(JPEG_FILE)
        if im.get_typeof("exif-ifd0-Orientation") != 0:
            x = im.copy()
            x.set_type(pyvips.GValue.gstr_type, "exif-ifd2-UserComment", "hello ( there") # tag_is_encoding
            x.set_type(pyvips.GValue.gstr_type, "exif-ifd0-Software", "hello ( there")    # tag_is_ascii
            x.set_type(pyvips.GValue.gstr_type, "exif-ifd0-XPComment", "hello ( there")   # tag_is_utf16
            buf = x.jpegsave_buffer()
            y = pyvips.Image.new_from_buffer(buf, "")
            exif_valid(y)
            # Reproduce https://github.com/libvips/libvips/issues/2388
            buf = y.jpegsave_buffer()
            z = pyvips.Image.new_from_buffer(buf, "")
            exif_valid(z)
            # Try whether we can remove EXIF, just to be sure
            z = z.copy()
            z.remove("exif-ifd2-UserComment")
            z.remove("exif-ifd0-Software")
            z.remove("exif-ifd0-XPComment")
            buf = z.jpegsave_buffer()
            im = pyvips.Image.new_from_buffer(buf, "")
            exif_removed(im)

    @skip_if_no("jpegsave")
    @pytest.mark.xfail(raises=pyvips.error.Error, reason="requires libexif >= 0.6.22")
    def test_jpegsave_exif_2_3_ascii(self):
        def exif_valid(exif_tags, im):
            for exif_tag in exif_tags:
                assert im.get(exif_tag).find("ASCII, 14 components, 14 bytes") != -1

        # first make sure we have exif support
        im = pyvips.Image.new_from_file(JPEG_FILE)
        if im.get_typeof("exif-ifd0-Orientation") != 0:
            x = im.copy()
            exif_tags = [
                "exif-ifd2-CameraOwnerName",
                "exif-ifd2-BodySerialNumber",
                "exif-ifd2-LensMake",
                "exif-ifd2-LensModel",
                "exif-ifd2-LensSerialNumber",
            ]
            for exif_tag in exif_tags:
                x.set_type(pyvips.GValue.gstr_type, exif_tag, "hello ( there")
            buf = x.jpegsave_buffer()
            y = pyvips.Image.new_from_buffer(buf, "")
            exif_valid(exif_tags, y)

    @skip_if_no("jpegsave")
    @pytest.mark.xfail(raises=pyvips.error.Error, reason="requires libexif >= 0.6.23")
    def test_jpegsave_exif_2_3_ascii_2(self):
        def exif_valid(exif_tags, im):
            for exif_tag in exif_tags:
                assert im.get(exif_tag).find("ASCII, 14 components, 14 bytes") != -1

        # first make sure we have exif support
        im = pyvips.Image.new_from_file(JPEG_FILE)
        if im.get_typeof("exif-ifd0-Orientation") != 0:
            x = im.copy()
            exif_tags = [
                "exif-ifd2-OffsetTime",
                "exif-ifd2-OffsetTimeOriginal",
                "exif-ifd2-OffsetTimeDigitized",
                "exif-ifd3-GPSLatitudeRef",
                "exif-ifd3-GPSLongitudeRef",
                "exif-ifd3-GPSSatellites",
                "exif-ifd3-GPSStatus",
                "exif-ifd3-GPSMeasureMode",
                "exif-ifd3-GPSSpeedRef",
                "exif-ifd3-GPSTrackRef",
                "exif-ifd3-GPSImgDirectionRef",
                "exif-ifd3-GPSMapDatum",
                "exif-ifd3-GPSDestLatitudeRef",
                "exif-ifd3-GPSDestLongitudeRef",
                "exif-ifd3-GPSDestBearingRef",
                "exif-ifd3-GPSDestDistanceRef",
                "exif-ifd3-GPSDateStamp",
            ]
            for exif_tag in exif_tags:
                x.set_type(pyvips.GValue.gstr_type, exif_tag, "hello ( there")

            buf = x.jpegsave_buffer()
            y = pyvips.Image.new_from_buffer(buf, "")
            exif_valid(exif_tags, y)

    @skip_if_no("jpegload")
    def test_truncated(self):
        # This should open (there's enough there for the header)
        im = pyvips.Image.new_from_file(TRUNCATED_FILE)
        # but this should fail with a warning, and knock TRUNCATED_FILE out of
        # the cache
        x = im.avg()

        # now we should open again, but it won't come from cache, it'll reload
        im = pyvips.Image.new_from_file(TRUNCATED_FILE)
        # and this should fail with a warning once more
        x = im.avg()

    @skip_if_no("uhdrload")
    def test_uhdrload(self):
        # decode as sRGB + gainmap
        im = pyvips.Image.uhdrload(UHDR_FILE)

        assert im.width == 3840
        assert im.height == 2160
        assert im.bands == 3
        assert im.format == "uchar"
        assert im.interpretation == "srgb"

        for name in ["gainmap-max-content-boost",
                     "gainmap-min-content-boost",
                     "gainmap-gamma",
                     "gainmap-offset-sdr",
                     "gainmap-offset-hdr"]:
            value = im.get(name)
            assert isinstance(value, list)
            assert len(value) == 3

        for name in ["gainmap-hdr-capacity-min",
                     "gainmap-hdr-capacity-max",
                     "gainmap-use-base-cg"]:
            value = im.get(name)
            assert isinstance(value, (int, float))

        assert im.get("gainmap-scale-factor") == 4

        value = im.get("gainmap-data")
        assert len(value) > 10000

        value = im.get("icc-profile-data")
        assert len(value) > 100

    @skip_if_no("uhdrsave")
    def test_uhdrsave(self):
        im = pyvips.Image.uhdrload(UHDR_FILE)
        data = im.uhdrsave_buffer()
        im2 = pyvips.Image.uhdrload_buffer(data)

        assert im2.width == 3840
        assert im2.height == 2160
        assert im2.bands == 3
        assert im2.format == "uchar"
        assert im2.interpretation == "srgb"
        value = im2.get("gainmap-data")
        assert len(value) > 10000

    @skip_if_no("uhdrsave")
    def test_uhdrsave_roundtrip(self):
        im = pyvips.Image.uhdrload(UHDR_FILE)
        data = im.uhdrsave_buffer()
        im_hdr2 = pyvips.Image.uhdrload_buffer(data).uhdr2scRGB()
        im_hdr = pyvips.Image.uhdrload(UHDR_FILE).uhdr2scRGB()

        assert (im_hdr2 - im_hdr).abs().avg() < 0.02

    @skip_if_no("uhdrsave")
    def test_uhdrsave_roundtrip_hdr(self):
        im = pyvips.Image.uhdrload(UHDR_FILE).uhdr2scRGB()
        data = im.uhdrsave_buffer()
        im2 = pyvips.Image.uhdrload_buffer(data).uhdr2scRGB()

        assert (im2 - im).abs().avg() < 0.05

    @skip_if_no("uhdrsave")
    def test_uhdrsave_gainmap_scale_factor(self):
        scrgb = self.colour.colourspace("scrgb")

        data = scrgb.uhdrsave_buffer()
        im = pyvips.Image.uhdrload_buffer(data)
        assert im.get("gainmap-scale-factor") == 2

        data = scrgb.uhdrsave_buffer(gainmap_scale_factor=4)
        im = pyvips.Image.uhdrload_buffer(data)
        assert im.get("gainmap-scale-factor") == 4

    @skip_if_no("uhdrload")
    def test_uhdr_thumbnail(self):
        im = pyvips.Image.uhdrload(UHDR_FILE)
        thumb = pyvips.Image.thumbnail(UHDR_FILE, 128)
        buf = thumb.uhdrsave_buffer()
        im2 = pyvips.Image.uhdrload_buffer(buf)

        gainmap_data_before = im.get("gainmap-data")
        gainmap_data_after = im2.get("gainmap-data")
        assert len(gainmap_data_after) < len(gainmap_data_before)

        gainmap_before = pyvips.Image.jpegload_buffer(gainmap_data_before)
        gainmap_after = pyvips.Image.jpegload_buffer(gainmap_data_after)
        assert gainmap_before.width > gainmap_after.width
        assert gainmap_before.height > gainmap_after.height

    @skip_if_no("uhdrload")
    def test_uhdr_thumbnail_crop(self):
        thumb = pyvips.Image.thumbnail(UHDR_FILE, 128, crop="centre")
        buf = thumb.uhdrsave_buffer()
        im2 = pyvips.Image.uhdrload_buffer(buf)

        gainmap_data_after = im2.get("gainmap-data")
        gainmap_after = pyvips.Image.jpegload_buffer(gainmap_data_after)
        assert abs(gainmap_after.width - gainmap_after.height) < 5

    @skip_if_no("uhdrload")
    @skip_if_no("dzsave")
    def test_uhdr_dzsave(self):
        filename = temp_filename(self.tempdir, '')
        uhdr = pyvips.Image.uhdrload(UHDR_FILE)

        gainmap_data_before = uhdr.get("gainmap-data")
        gainmap_before = pyvips.Image.jpegload_buffer(gainmap_data_before)
        hscale = uhdr.width / gainmap_before.width
        vscale = uhdr.height / gainmap_before.height

        uhdr.dzsave(filename, keep="gainmap")

        # _files/12 is the full res image
        deepest = 12

        for [level, tile_x, tile_y] in [
                [deepest, 0, 0],
                [deepest, 3, 1],
                [10, 1, 0],
                [9, 0, 0]
            ]:
            tile_path = f"{filename}_files/{level}/{tile_x}_{tile_y}.jpeg"

            tile = pyvips.Image.uhdrload(tile_path)
            gainmap_data_after = tile.get("gainmap-data")
            gainmap_after = pyvips.Image.jpegload_buffer(gainmap_data_after)

            # rounding plus overlaps
            assert abs(gainmap_after.width - tile.width / hscale) < 2
            assert abs(gainmap_after.height - tile.height / vscale) < 2

            shrunk_gainmap = gainmap_before.resize(1 / (1 << (deepest - level)),
                                                   kernel="linear")
            left = tile_x * tile.width / hscale
            top = tile_y * tile.height / vscale
            expected_gainmap_after = shrunk_gainmap.crop(left,
                                                         top,
                                                         gainmap_after.width,
                                                         gainmap_after.height)
            assert abs(expected_gainmap_after.avg() - gainmap_after.avg()) < 1

    @skip_if_no("pngload")
    def test_png(self):
        def png_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [38671.0, 33914.0, 26762.0])
            assert im.width == 290
            assert im.height == 442
            assert im.bands == 3
            assert im.get("bits-per-sample") == 16
            assert im.get_typeof("palette") == 0

        self.file_loader("pngload", PNG_FILE, png_valid)
        self.buffer_loader("pngload_buffer", PNG_FILE, png_valid)
        self.save_load_buffer("pngsave_buffer", "pngload_buffer", self.colour)
        self.save_load("%s.png", self.mono)
        self.save_load("%s.png", self.colour)
        self.save_load_file(".png", "[interlace]", self.colour)
        self.save_load_file(".png", "[interlace]", self.mono)

        for image in self.all:
            target = pyvips.Target.new_to_memory()
            image.pngsave_target(target)

        def png_indexed_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [148.0, 131.0, 109.0])
            assert im.width == 290
            assert im.height == 442
            assert im.bands == 3
            assert im.get("bits-per-sample") == 8
            assert im.get("palette") == 1

        self.file_loader("pngload", PNG_INDEXED_FILE, png_indexed_valid)
        self.buffer_loader("pngload_buffer",
            PNG_INDEXED_FILE, png_indexed_valid)

        # size of a regular mono PNG
        len_mono = len(self.mono.write_to_buffer(".png"))

        # 4-bit should be smaller
        len_mono4 = len(self.mono.write_to_buffer(".png", bitdepth=4))
        assert( len_mono4 < len_mono )

        len_mono2 = len(self.mono.write_to_buffer(".png", bitdepth=2))
        assert( len_mono2 < len_mono4 )

        len_mono1 = len(self.mono.write_to_buffer(".png", bitdepth=1))
        assert( len_mono1 < len_mono2 )

        # take a 1-bit image to png and back
        onebit = self.mono > 128
        data = onebit.write_to_buffer(".png", bitdepth=1)
        after = pyvips.Image.new_from_buffer(data, "")
        assert( (onebit - after).abs().max() == 0 )
        assert after.get("bits-per-sample") == 1

        # we can't test palette save since we can't be sure libimagequant is
        # available and there's no easy test for its presence

        # see if we have exif parsing: our test jpg image has this field
        x = pyvips.Image.new_from_file(JPEG_FILE)
        if x.get_typeof("exif-ifd0-Orientation") != 0:
            # we need a copy of the image to set the new metadata on
            # otherwise we get caching problems

            # can set, save and load new orientation
            x = pyvips.Image.new_from_file(JPEG_FILE)
            x = x.copy()

            x.set("orientation", 2)

            filename = temp_filename(self.tempdir, '.png')
            x.write_to_file(filename)

            x = pyvips.Image.new_from_file(filename)
            y = x.get("orientation")
            assert y == 2

        # Add EXIF to new PNG
        im1 = pyvips.Image.black(8, 8)
        im1.set_type(pyvips.GValue.gstr_type,
            "exif-ifd0-ImageDescription", "test description")
        im2 = pyvips.Image.new_from_buffer(
            im1.write_to_buffer(".png"), "")
        assert im2.get("exif-ifd0-ImageDescription") \
            .startswith("test description")

        # https://github.com/libvips/libvips/issues/4509
        data = self.rgba.cast("double").write_to_buffer(".png")
        after = pyvips.Image.new_from_buffer(data, "")
        assert (self.rgba - after).abs().max() == 0

        # we should be able to save an 8-bit image as a 16-bit PNG
        data = self.colour.pngsave_buffer(bitdepth=16)
        rgb16 = pyvips.Image.pngload_buffer(data)
        assert rgb16.format == "ushort"

        # we should be able to save a 16-bit image as an 8-bit PNG
        data = rgb16.pngsave_buffer(bitdepth=8)
        rgb = pyvips.Image.pngload_buffer(data)
        assert rgb.format == "uchar"

        # we should be able to save a 16-bit image as an 8-bit WebP
        if have("webpsave"):
            data = rgb16.webpsave_buffer(lossless=True)
            rgb = pyvips.Image.webpload_buffer(data)
            assert rgb.format == "uchar"
            # ... and check if it was correctly shifted down
            # https://github.com/libvips/libvips/issues/4568
            assert (self.colour - rgb).abs().max() == 0

        # sanity-check MULTIBAND save, this will be remapped
        # to its standard RGB-like or greyscale equivalent
        for i in range(1, 5):
            im = pyvips.Image.black(16, 16, bands=i)
            im.pngsave_buffer()

        # https://github.com/libvips/lua-vips/issues/93
        im = pyvips.Image.new_from_array([1, 2, 3, 4])
        buf = im.pngsave_buffer()
        im2 = pyvips.Image.new_from_buffer(buf, "")

        assert im.avg() == im2.avg()

        # https://github.com/libvips/ruby-vips/issues/431
        im = pyvips.Image.new_from_array([1, 2, 3, 4])
        im = im.bandjoin([im, im]).cast("uchar")
        buf = im.pngsave_buffer(bitdepth=1)
        im2 = pyvips.Image.new_from_buffer(buf, "")

        assert im.avg() == im2.avg()

    @skip_if_no("tiffload")
    def test_tiff(self):
        def tiff_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [38671.0, 33914.0, 26762.0])
            assert im.width == 290
            assert im.height == 442
            assert im.bands == 3
            assert im.get("bits-per-sample") == 16

        self.file_loader("tiffload", TIF_FILE, tiff_valid)
        self.buffer_loader("tiffload_buffer", TIF_FILE, tiff_valid)

        for image in self.all:
            target = pyvips.Target.new_to_memory()
            image.tiffsave_target(target)

        def tiff1_valid(im):
            a = im(127, 0)
            assert_almost_equal_objects(a, [0.0])
            a = im(128, 0)
            assert_almost_equal_objects(a, [255.0])
            assert im.width == 256
            assert im.height == 4
            assert im.bands == 1
            assert im.get("bits-per-sample") == 1

        self.file_loader("tiffload", TIF1_FILE, tiff1_valid)

        def tiff2_valid(im):
            a = im(127, 0)
            assert_almost_equal_objects(a, [85.0])
            a = im(128, 0)
            assert_almost_equal_objects(a, [170.0])
            assert im.width == 256
            assert im.height == 4
            assert im.bands == 1
            assert im.get("bits-per-sample") == 2

        self.file_loader("tiffload", TIF2_FILE, tiff2_valid)

        def tiff4_valid(im):
            a = im(127, 0)
            assert_almost_equal_objects(a, [119.0])
            a = im(128, 0)
            assert_almost_equal_objects(a, [136.0])
            assert im.width == 256
            assert im.height == 4
            assert im.bands == 1
            assert im.get("bits-per-sample") == 4

        self.file_loader("tiffload", TIF4_FILE, tiff4_valid)

        def tiff_subsampled_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [6.0, 5.0, 21.0, 255.0])
            assert im.width == 250
            assert im.height == 325
            assert im.bands == 4
            assert im.get("bits-per-sample") == 8

        self.file_loader("tiffload", TIF_SUBSAMPLED_FILE, tiff_subsampled_valid)
        self.buffer_loader("tiffload_buffer", TIF_SUBSAMPLED_FILE, tiff_subsampled_valid)

        self.save_load_buffer("tiffsave_buffer", "tiffload_buffer", self.colour)
        self.save_load("%s.tif", self.mono)
        self.save_load("%s.tif", self.colour)
        self.save_load("%s.tif", self.cmyk)
        self.save_load("%s.tif", self.rgba)
        self.save_load("%s.tif", self.onebit)

        self.save_load_file(".tif", "[bitdepth=1]", self.onebit)
        self.save_load_file(".tif", "[miniswhite]", self.onebit)
        self.save_load_file(".tif", "[bitdepth=1,miniswhite]", self.onebit)

        self.save_load_file(".tif", f"[profile={SRGB_FILE}]", self.colour)
        self.save_load_file(".tif", "[tile]", self.colour)
        self.save_load_file(".tif", "[tile,pyramid]", self.colour)
        self.save_load_file(".tif", "[tile,pyramid,subifd]", self.colour)
        self.save_load_file(".tif",
                            "[tile,pyramid,compression=jpeg]", self.colour, 80)
        self.save_load_file(".tif",
                            "[tile,pyramid,subifd,compression=jpeg]",
                            self.colour, 80)
        self.save_load_file(".tif", "[bigtiff]", self.colour)
        self.save_load_file(".tif", "[compression=jpeg]", self.colour, 80)
        self.save_load_file(".tif",
                            "[tile,tile-width=256]", self.colour, 10)

        im = pyvips.Image.new_from_file(TIF2_FILE)
        self.save_load_file(".tif", "[bitdepth=2]", im)
        im = pyvips.Image.new_from_file(TIF4_FILE)
        self.save_load_file(".tif", "[bitdepth=4]", im)

        filename = temp_filename(self.tempdir, '.tif')
        self.colour.write_to_file(filename, pyramid=True, compression="jpeg")
        x = pyvips.Image.new_from_file(filename, page=2)
        assert x.width == 72
        assert abs(x.avg() - 117.3) < 1

        filename = temp_filename(self.tempdir, '.tif')
        self.colour.write_to_file(filename, pyramid=True, subifd=True, compression="jpeg")
        x = pyvips.Image.new_from_file(filename, subifd=1)
        assert x.width == 72
        assert abs(x.avg() - 117.3) < 1

        filename = temp_filename(self.tempdir, '.tif')
        x = pyvips.Image.new_from_file(TIF_FILE)
        x = x.copy()
        x.set("orientation", 2)
        x.write_to_file(filename)
        x = pyvips.Image.new_from_file(filename)
        y = x.get("orientation")
        assert y == 2

        filename = temp_filename(self.tempdir, '.tif')
        x = pyvips.Image.new_from_file(TIF_FILE)
        x = x.copy()
        x.set("orientation", 2)
        x.write_to_file(filename)
        x = pyvips.Image.new_from_file(filename)
        y = x.get("orientation")
        assert y == 2
        x = x.copy()
        x.remove("orientation")

        filename = temp_filename(self.tempdir, '.tif')
        x.write_to_file(filename)
        x = pyvips.Image.new_from_file(filename)
        y = x.get("orientation")
        assert y == 1

        filename = temp_filename(self.tempdir, '.tif')
        x = pyvips.Image.new_from_file(TIF_FILE)
        x = x.copy()
        x.set("orientation", 6)
        x.write_to_file(filename)
        x1 = pyvips.Image.new_from_file(filename)
        x2 = pyvips.Image.new_from_file(filename, autorotate=True)
        assert x1.width == x2.height
        assert x1.height == x2.width

        filename = temp_filename(self.tempdir, '.tif')
        x = pyvips.Image.new_from_file(TIF_FILE)
        x = x.copy()
        x.write_to_file(filename, xres=100, yres=200, resunit="cm")
        x1 = pyvips.Image.new_from_file(filename)
        assert x1.get("resolution-unit") == "cm"
        assert x1.xres == 100
        assert x1.yres == 200

        filename = temp_filename(self.tempdir, '.tif')
        x = pyvips.Image.new_from_file(TIF_FILE)
        x = x.copy()
        x.write_to_file(filename, xres=100, yres=200, resunit="inch")
        x1 = pyvips.Image.new_from_file(filename)
        assert x1.get("resolution-unit") == "in"
        assert x1.xres == 100
        assert x1.yres == 200

        filename = temp_filename(self.tempdir, '.tif')
        x = pyvips.Image.new_from_file(TIF_FILE)
        x = x.copy(xres=100, yres=200)
        x.remove("resolution-unit")
        x.write_to_file(filename)
        x1 = pyvips.Image.new_from_file(filename)
        assert x1.get("resolution-unit") == "in"
        assert x1.xres == 100
        assert x1.yres == 200

        if sys.platform == "darwin":
            with open(TIF2_FILE, 'rb') as f:
                buf = bytearray(f.read())
            buf = buf[:-4]
            source = pyvips.Source.new_from_memory(buf)
            im = pyvips.Image.tiffload_source(source, fail_on="warning")
            with pytest.raises(Exception) as e_info:
                im.avg() > 0

        # OME support in 8.5
        x = pyvips.Image.new_from_file(OME_FILE)
        assert x.width == 439
        assert x.height == 167
        page_height = x.height

        x = pyvips.Image.new_from_file(OME_FILE, n=-1)
        assert x.width == 439
        assert x.height == page_height * 15

        x = pyvips.Image.new_from_file(OME_FILE, page=1, n=-1)
        assert x.width == 439
        assert x.height == page_height * 14

        x = pyvips.Image.new_from_file(OME_FILE, page=1, n=2)
        assert x.width == 439
        assert x.height == page_height * 2

        x = pyvips.Image.new_from_file(OME_FILE, n=-1)
        assert x(0, 166)[0] == 96
        assert x(0, 167)[0] == 0
        assert x(0, 168)[0] == 1

        filename = temp_filename(self.tempdir, '.tif')
        x.write_to_file(filename)

        x = pyvips.Image.new_from_file(filename, n=-1)
        assert x.width == 439
        assert x.height == page_height * 15
        assert x(0, 166)[0] == 96
        assert x(0, 167)[0] == 0
        assert x(0, 168)[0] == 1

        # pyr save to buffer added in 8.6
        x = pyvips.Image.new_from_file(TIF_FILE)
        buf = x.tiffsave_buffer(tile=True, pyramid=True)
        filename = temp_filename(self.tempdir, '.tif')
        x.tiffsave(filename, tile=True, pyramid=True)
        with open(filename, 'rb') as f:
            buf2 = f.read()
        assert len(buf) == len(buf2)

        filename = temp_filename(self.tempdir, '.tif')
        self.rgba.write_to_file(filename, premultiply=True)
        a = pyvips.Image.new_from_file(filename)
        b = self.rgba.premultiply().cast("uchar").unpremultiply().cast("uchar")
        assert (a == b).min() == 255

        a = pyvips.Image.new_from_buffer(buf, "", page=2)
        b = pyvips.Image.new_from_buffer(buf2, "", page=2)
        assert a.width == b.width
        assert a.height == b.height
        assert (a == b).min() == 255

        # just 0/255 in each band, shrink with mode and all pixels should be 0
        # or 255 in layer 1
        x = pyvips.Image.new_from_file(TIF_FILE) > 128
        for shrink in ["mode", "median", "max", "min"]:
            buf = x.tiffsave_buffer(pyramid=True, region_shrink=shrink)
            y = pyvips.Image.new_from_buffer(buf, "", page=1)
            z = y.hist_find(band=0)
            assert z(0, 0)[0] + z(255, 0)[0] == y.width * y.height

        # metadata tile-width and tile-height should be correct
        x = pyvips.Image.new_from_file(TIF_FILE)
        buf = x.tiffsave_buffer(tile=True, tile_width=192, tile_height=224)
        y = pyvips.Image.new_from_buffer(buf, "")
        assert y.get("tile-width") == 192
        assert y.get("tile-height") == 224

    @skip_if_no("tiffload")
    @pytest.mark.xfail(raises=AssertionError, reason="fails when libtiff was configured with --disable-old-jpeg")
    def test_tiff_ojpeg(self):
        def tiff_ojpeg_tile_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [135.0, 156.0, 177.0, 255.0])
            assert im.width == 234
            assert im.height == 213
            assert im.bands == 4
            assert im.get("bits-per-sample") == 8
            assert im.get("tile-width") == 240
            assert im.get("tile-height") == 224

        self.file_loader("tiffload", TIF_OJPEG_TILE_FILE, tiff_ojpeg_tile_valid)
        self.buffer_loader("tiffload_buffer", TIF_OJPEG_TILE_FILE, tiff_ojpeg_tile_valid)

        def tiff_ojpeg_strip_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [228.0, 15.0, 9.0, 255.0])
            assert im.width == 160
            assert im.height == 160
            assert im.bands == 4
            assert im.get("bits-per-sample") == 8

        self.file_loader("tiffload", TIF_OJPEG_STRIP_FILE, tiff_ojpeg_strip_valid)
        self.buffer_loader("tiffload_buffer", TIF_OJPEG_STRIP_FILE, tiff_ojpeg_strip_valid)

    @skip_if_no("jp2kload")
    @skip_if_no("tiffload")
    def test_tiffjp2k(self):
        self.save_load_file(".tif", "[tile,compression=jp2k]", self.colour, 80)
        self.save_load_file(".tif",
                            "[tile,pyramid,compression=jp2k]", self.colour, 80)
        self.save_load_file(".tif",
                            "[tile,pyramid,subifd,compression=jp2k]",
                            self.colour, 80)

    @skip_if_no("magickload")
    def test_magickload(self):
        def bmp_valid(im):
            a = im(100, 100)

            assert_almost_equal_objects(a, [227, 216, 201])
            assert im.width == 1419
            assert im.height == 1001
            assert im.get("bits-per-sample") == 8

        self.file_loader("magickload", BMP_FILE, bmp_valid)
        self.buffer_loader("magickload_buffer", BMP_FILE, bmp_valid)
        source = pyvips.Source.new_from_file(BMP_FILE)
        x = pyvips.Image.new_from_source(source, "")
        bmp_valid(x)

        # we should have rgb or rgba for svg files ... different versions of
        # IM handle this differently. GM even gives 1 band.
        im = pyvips.Image.magickload(SVG_FILE)
        assert im.bands == 3 or im.bands == 4 or im.bands == 1

        # density should change size of generated svg
        im = pyvips.Image.magickload(SVG_FILE, density='100')
        width = im.width
        height = im.height
        im = pyvips.Image.magickload(SVG_FILE, density='200')
        # This seems to fail on travis, no idea why, some problem in their IM
        # perhaps
        # assert im.width == width * 2
        # assert im.height == height * 2

        im = pyvips.Image.magickload(GIF_ANIM_FILE)
        width = im.width
        height = im.height
        im = pyvips.Image.magickload(GIF_ANIM_FILE, n=-1)
        assert im.width == width
        assert im.height == height * 5

        # page/n let you pick a range of pages
        im = pyvips.Image.magickload(GIF_ANIM_FILE)
        width = im.width
        height = im.height
        im = pyvips.Image.magickload(GIF_ANIM_FILE, page=1, n=2)
        assert im.width == width
        assert im.height == height * 2
        page_height = im.get("page-height")
        assert page_height == height

        # should work for dicom
        im = pyvips.Image.magickload(DICOM_FILE)
        assert im.width == 128
        assert im.height == 128
        # some IMs are 3 bands, some are 1, can't really test
        # assert im.bands == 1

        # libvips has its own sniffer for ICO, test that
        with open(ICO_FILE, 'rb') as f:
            buf = f.read()

        im = pyvips.Image.new_from_buffer(buf, "")
        assert im.width == 16
        assert im.height == 16

        # libvips has its own sniffer for CUR, test that
        with open(CUR_FILE, 'rb') as f:
            buf = f.read()

        im = pyvips.Image.new_from_buffer(buf, "")
        assert im.width == 32
        assert im.height == 32

        # libvips has its own sniffer for TGA, test that
        with open(TGA_FILE, 'rb') as f:
            buf = f.read()
        im = pyvips.Image.new_from_buffer(buf, "")
        assert im.width == 433
        assert im.height == 433

        # Test SGI/RGB files to sanity check that sniffers
        # aren't too broad
        with open(SGI_FILE, 'rb') as f:
            buf = f.read()
        im = pyvips.Image.new_from_buffer(buf, "")
        assert im.width == 433
        assert im.height == 433

        # load should see metadata like eg. icc profiles
        im = pyvips.Image.magickload(JPEG_FILE)
        assert len(im.get("icc-profile-data")) == 564

        im = pyvips.Image.magickload(JPEG_FILE)

    # added in 8.7
    @skip_if_no("magicksave")
    def test_magicksave(self):
        # save to a file and load again ... we can't use save_load_file since
        # we want to make sure we use magickload/save
        # don't use BMP - GraphicsMagick always adds an alpha
        # don't use TIF - IM7 will save as 16-bit
        filename = temp_filename(self.tempdir, ".jpg")

        self.colour.magicksave(filename)
        x = pyvips.Image.magickload(filename)

        assert self.colour.width == x.width
        assert self.colour.height == x.height
        assert self.colour.bands == x.bands
        max_diff = (self.colour - x).abs().max()
        assert max_diff < 60
        assert len(x.get("icc-profile-data")) == 564

        self.save_load_buffer("magicksave_buffer", "magickload_buffer",
                              self.colour, 60, format="JPG")

        # try an animation
        if have("gifload"):
            x1 = pyvips.Image.new_from_file(GIF_ANIM_FILE, n=-1)
            w1 = x1.magicksave_buffer(format="GIF")
            x2 = pyvips.Image.new_from_buffer(w1, "", n=-1)
            assert x1.get("delay") == x2.get("delay")
            assert x1.get("page-height") == x2.get("page-height")
            # magicks vary in how they handle this ... just pray we are close
            assert abs(x1.get("gif-loop") - x2.get("gif-loop")) < 5

    @skip_if_no("webpload")
    def test_webp(self):
        def webp_valid(im):
            a = im(10, 10)
            # different webp versions use different rounding systems leading
            # to small variations
            assert_almost_equal_objects(a, [71, 166, 236], threshold=2)
            assert im.width == 550
            assert im.height == 368
            assert im.bands == 3

        self.file_loader("webpload", WEBP_FILE, webp_valid)
        self.buffer_loader("webpload_buffer", WEBP_FILE, webp_valid)
        self.save_load_buffer("webpsave_buffer", "webpload_buffer",
                              self.colour, 60)
        self.save_load("%s.webp", self.colour)

        # test lossless mode
        im = pyvips.Image.new_from_file(WEBP_FILE)
        buf = im.webpsave_buffer(lossless=True)
        im2 = pyvips.Image.new_from_buffer(buf, "")
        assert (im - im2).abs().max() == 0

        # higher Q should mean a bigger buffer
        b1 = im.webpsave_buffer(Q=10)
        b2 = im.webpsave_buffer(Q=90)
        assert len(b2) > len(b1)

        # test exact mode
        im = pyvips.Image.new_from_file(RGBA_FILE)
        buf = im.webpsave_buffer(lossless=True, exact=True)
        im2 = pyvips.Image.new_from_buffer(buf, "")
        assert (im - im2).abs().max() == 0
        buf = im.webpsave_buffer(lossless=True)
        im2 = pyvips.Image.new_from_buffer(buf, "")
        assert (im - im2).abs().max() != 0

        # try saving an image with an ICC profile and reading it back ... if we
        # can do it, our webp supports metadata load/save
        buf = self.colour.webpsave_buffer()
        im = pyvips.Image.new_from_buffer(buf, "")
        if im.get_typeof("icc-profile-data") != 0:
            # verify that the profile comes back unharmed
            p1 = self.colour.get("icc-profile-data")
            p2 = im.get("icc-profile-data")
            assert p1 == p2

            # add tests for exif, xmp, iptc
            # the exif test will need us to be able to walk the header,
            # we can't just check exif-data

            # we can test that exif changes change the output of webpsave
            # first make sure we have exif support
            z = pyvips.Image.new_from_file(JPEG_FILE)
            if z.get_typeof("exif-ifd0-Orientation") != 0:
                x = self.colour.copy()
                x.set("orientation", 6)
                buf = x.webpsave_buffer()
                y = pyvips.Image.new_from_buffer(buf, "")
                assert y.get("orientation") == 6

        # try converting an animated gif to webp
        if have("gifload"):
            x1 = pyvips.Image.new_from_file(GIF_ANIM_FILE, n=-1)
            w1 = x1.webpsave_buffer(Q=10)

            # our test gif has delay 0 for the first frame set in error,
            # when converting to WebP this should result in a 100ms delay.
            expected_delay = [100 if d <= 10 else d for d in x1.get("delay")]

            x2 = pyvips.Image.new_from_buffer(w1, "", n=-1)
            assert x1.width == x2.width
            assert x1.height == x2.height
            assert expected_delay == x2.get("delay")
            assert x1.get("page-height") == x2.get("page-height")
            assert x1.get("gif-loop") == x2.get("gif-loop")

        # WebP image that happens to contain the string "<svg"
        if have("svgload"):
            x = pyvips.Image.new_from_file(WEBP_LOOKS_LIKE_SVG_FILE)
            assert x.get("vips-loader") == "webpload"

        # Animated WebP roundtrip
        x = pyvips.Image.new_from_file(WEBP_ANIMATED_FILE, n=-1)
        assert x.width == 13
        assert x.height == 16731
        buf = x.webpsave_buffer()

        # target_size should reasonably work, +/- 2% is fine
        x = pyvips.Image.new_from_file(WEBP_FILE)
        buf_size = len(x.webpsave_buffer(target_size=20_000, keep='none'))
        assert 19600 < buf_size < 20400

    @skip_if_no("analyzeload")
    def test_analyzeload(self):
        def analyze_valid(im):
            a = im(10, 10)
            assert pytest.approx(a[0]) == 3335
            assert im.width == 128
            assert im.height == 8064
            assert im.bands == 1

        self.file_loader("analyzeload", ANALYZE_FILE, analyze_valid)

    @skip_if_no("matload")
    def test_matload(self):
        def matlab_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [38671.0, 33914.0, 26762.0])
            assert im.width == 290
            assert im.height == 442
            assert im.bands == 3

        self.file_loader("matload", MATLAB_FILE, matlab_valid)

    @skip_if_no("openexrload")
    def test_openexrload(self):
        def exr_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [0.124512, 0.159668, 0.040375,
                                            1.0],
                                        threshold=0.00001)
            assert im.width == 610
            assert im.height == 406
            assert im.bands == 4

        self.file_loader("openexrload", EXR_FILE, exr_valid)

    @skip_if_no("fitsload")
    def test_fitsload(self):
        def fits_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [-0.165013, -0.148553, 1.09122,
                                            -0.942242], threshold=0.00001)
            assert im.width == 200
            assert im.height == 200
            assert im.bands == 4

        self.file_loader("fitsload", FITS_FILE, fits_valid)
        self.save_load("%s.fits", self.mono)

    @skip_if_no("niftiload")
    def test_niftiload(self):
        def nifti_valid(im):
            a = im(30, 26)
            assert_almost_equal_objects(a, [131])
            assert im.width == 91
            assert im.height == 9919
            assert im.bands == 1

        self.file_loader("niftiload", NIFTI_FILE, nifti_valid)
        self.save_load("%s.nii.gz", self.mono)

    @skip_if_no("openslideload")
    def test_openslideload(self):

        def openslide_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [244, 250, 243, 255])
            assert im.width == 2220
            assert im.height == 2967
            assert im.bands == 4
            assert im.get("tile-width") == 240
            assert im.get("tile-height") == 240

        self.file_loader("openslideload", OPENSLIDE_FILE, openslide_valid)

        source = pyvips.Source.new_from_file(OPENSLIDE_FILE)
        x = pyvips.Image.new_from_source(source, "")
        openslide_valid(x)

    @skip_if_no("pdfload")
    def test_pdfload(self):
        def pdf_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [35, 31, 32, 255])
            assert im.width == 1134
            assert im.height == 680
            assert im.bands == 4

        self.file_loader("pdfload", PDF_FILE, pdf_valid)
        self.buffer_loader("pdfload_buffer", PDF_FILE, pdf_valid)

        im = pyvips.Image.new_from_file(PDF_FILE)
        x = pyvips.Image.new_from_file(PDF_FILE, scale=2)
        assert abs(im.width * 2 - x.width) < 2
        assert abs(im.height * 2 - x.height) < 2

        im = pyvips.Image.new_from_file(PDF_FILE)
        x = pyvips.Image.new_from_file(PDF_FILE, dpi=144)
        assert abs(im.width * 2 - x.width) < 2
        assert abs(im.height * 2 - x.height) < 2

        im = pyvips.Image.new_from_file(PDF_PAGE_BOX_FILE)
        assert im.width == 709
        assert im.height == 955
        assert im.get("pdf-creator") == "Adobe InDesign 20.4 (Windows)"
        assert im.get("pdf-producer") == "Adobe PDF Library 17.0"

        pdfloadOp = pyvips.Operation.new_from_name("pdfload").get_description()

        if "poppler" in pdfloadOp:
            # only crop is implemented, ignore requested page box
            im = pyvips.Image.new_from_file(PDF_FILE, page_box="art")
            assert im.width == 1134
            assert im.height == 680
            im = pyvips.Image.new_from_file(PDF_PAGE_BOX_FILE, page_box="art")
            assert im.width == 709
            assert im.height == 955

        if "pdfium" in pdfloadOp:
            im = pyvips.Image.new_from_file(PDF_FILE, page_box="art")
            assert im.width == 1121
            assert im.height == 680
            im = pyvips.Image.new_from_file(PDF_FILE, page_box="trim") # missing, will fallback to crop
            assert im.width == 1134
            assert im.height == 680
            im = pyvips.Image.new_from_file(PDF_PAGE_BOX_FILE, page_box="media")
            assert im.width == 822
            assert im.height == 1069
            im = pyvips.Image.new_from_file(PDF_PAGE_BOX_FILE, page_box="crop")
            assert im.width == 709
            assert im.height == 955
            im = pyvips.Image.new_from_file(PDF_PAGE_BOX_FILE, page_box="bleed")
            assert im.width == 652
            assert im.height == 899
            im = pyvips.Image.new_from_file(PDF_PAGE_BOX_FILE, page_box="trim")
            assert im.width == 595
            assert im.height == 842
            im = pyvips.Image.new_from_file(PDF_PAGE_BOX_FILE, page_box="art")
            assert im.width == 539
            assert im.height == 785

    @skip_if_no("gifload")
    def test_gifload(self):
        def gif_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [33, 33, 33, 255])
            assert im.width == 159
            assert im.height == 203
            assert im.bands == 3

        self.file_loader("gifload", GIF_FILE, gif_valid)
        self.buffer_loader("gifload_buffer", GIF_FILE, gif_valid)

        # test metadata
        x2 = pyvips.Image.new_from_file(GIF_FILE, n=-1)
        assert x2.get("n-pages") == 1
        assert x2.get("background") == [81, 81, 81]
        assert x2.get("interlaced") == 1
        assert x2.get("bits-per-sample") == 4
        assert x2.get("palette") == 1

        x2 = pyvips.Image.new_from_file(GIF_ANIM_FILE,
                                        n=-1,
                                        access="sequential")
        # our test gif has delay 0 for the first frame set in error
        assert x2.get("delay") == [0, 50, 50, 50, 50]
        assert x2.get("loop") == 32761
        assert x2.get("background") == [255, 255, 255]
        assert x2.get_typeof("interlaced") == 0
        assert x2.get("palette") == 1
        # test deprecated fields too
        assert x2.get("gif-loop") == 32760
        assert x2.get("gif-delay") == 0

        # test every pixel
        x1 = pyvips.Image.new_from_file(GIF_ANIM_FILE, n=-1)
        x2 = pyvips.Image.new_from_file(GIF_ANIM_EXPECTED_PNG_FILE)
        assert (x1 - x2).abs().max() == 0

        # test page handling
        x1 = pyvips.Image.new_from_file(GIF_ANIM_FILE)
        x2 = pyvips.Image.new_from_file(GIF_ANIM_FILE, n=2)
        assert x2.height == 2 * x1.height
        page_height = x2.get("page-height")
        assert page_height == x1.height

        x2 = pyvips.Image.new_from_file(GIF_ANIM_FILE, n=-1)
        assert x2.height == 5 * x1.height

        x2 = pyvips.Image.new_from_file(GIF_ANIM_FILE, page=1, n=-1)
        assert x2.height == 4 * x1.height

        with pytest.raises(pyvips.error.Error):
            x1 = pyvips.Image.new_from_file(GIF_ANIM_FILE_INVALID, n=-1)
            x1.avg()

    @skip_if_no("gifload")
    def test_gifload_animation_dispose_background(self):
        x1 = pyvips.Image.new_from_file(GIF_ANIM_DISPOSE_BACKGROUND_FILE, n=-1)
        x2 = pyvips.Image.new_from_file(GIF_ANIM_DISPOSE_BACKGROUND_EXPECTED_PNG_FILE)
        assert (x1 - x2).abs().max() == 0

    @skip_if_no("gifload")
    def test_gifload_animation_dispose_previous(self):
        x1 = pyvips.Image.new_from_file(GIF_ANIM_DISPOSE_PREVIOUS_FILE, n=-1)
        x2 = pyvips.Image.new_from_file(GIF_ANIM_DISPOSE_PREVIOUS_EXPECTED_PNG_FILE)
        assert (x1 - x2).abs().max() == 0

    @skip_if_no("gifload")
    def test_gifload_truncated(self):
        # should load with just a warning
        truncated_gif = os.path.join(IMAGES, "truncated.gif")
        im = pyvips.Image.new_from_file(truncated_gif)
        assert im.width == 575

        # should fail on truncation and warning
        with pytest.raises(Exception):
            im = pyvips.Image.new_from_file(truncated_gif, fail_on="warning")
        with pytest.raises(Exception):
            im = pyvips.Image.new_from_file(truncated_gif, fail_on="truncated")

    @skip_if_no("gifload")
    def test_gifload_frame_error(self):
        # should load with just a warning
        truncated_gif = os.path.join(IMAGES, "garden.gif")
        im = pyvips.Image.new_from_file(truncated_gif)
        assert im.width == 800

        # should fail on warning only
        im = pyvips.Image.new_from_file(truncated_gif, fail_on="truncated")
        assert im.width == 800
        with pytest.raises(Exception):
            im = pyvips.Image.new_from_file(truncated_gif, fail_on="warning")

    @skip_if_no("svgload")
    def test_svgload(self):
        def svg_valid(im):
            a = im(10, 10)
            assert_almost_equal_objects(a, [0, 0, 0, 0])
            assert im.width == 736
            assert im.height == 552
            assert im.bands == 4

        self.file_loader("svgload", SVG_FILE, svg_valid)
        self.buffer_loader("svgload_buffer", SVG_FILE, svg_valid)

        self.file_loader("svgload", SVGZ_FILE, svg_valid)
        self.buffer_loader("svgload_buffer", SVGZ_FILE, svg_valid)

        self.file_loader("svgload", SVG_GZ_FILE, svg_valid)

        im = pyvips.Image.new_from_file(SVG_FILE)
        x = pyvips.Image.new_from_file(SVG_FILE, scale=2)
        assert abs(im.width * 2 - x.width) < 2
        assert abs(im.height * 2 - x.height) < 2

        im = pyvips.Image.new_from_file(SVG_FILE)
        x = pyvips.Image.new_from_file(SVG_FILE, dpi=144)
        assert abs(im.width * 2 - x.width) < 2
        assert abs(im.height * 2 - x.height) < 2

        with pytest.raises(pyvips.error.Error):
            svg = b'<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0"></svg>'
            im = pyvips.Image.new_from_buffer(svg, "")

        # recognize dimensions for SVGs without width/height
        svg = b'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"></svg>'
        im = pyvips.Image.new_from_buffer(svg, "")
        assert im.width == 100
        assert im.height == 100

        svg = b'<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" /></svg>'
        im = pyvips.Image.new_from_buffer(svg, "")
        assert im.width == 100
        assert im.height == 100

        # width and height of 0.5 is valid
        svg = b'<svg xmlns="http://www.w3.org/2000/svg" width="0.5" height="0.5"></svg>'
        im = pyvips.Image.new_from_buffer(svg, "")
        assert im.width == 1
        assert im.height == 1

        # scale up
        svg = b'<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>'
        im = pyvips.Image.new_from_buffer(svg, "", scale=10000)
        assert im.width == 10000
        assert im.height == 10000

        # scale down
        svg = b'<svg xmlns="http://www.w3.org/2000/svg" width="100000" height="100000"></svg>'
        im = pyvips.Image.new_from_buffer(svg, "", scale=0.0001)
        assert im.width == 10
        assert im.height == 10

        # Custom CSS stylesheet
        im = pyvips.Image.new_from_file(SVG_FILE)
        assert im.avg() < 5
        im = pyvips.Image.new_from_file(SVG_FILE, stylesheet=b'path{stroke:#f00;stroke-width:1em;}')
        assert im.avg() > 5

        with pytest.raises(pyvips.error.Error):
            im = pyvips.Image.new_from_file(TRUNCATED_SVGZ_FILE)

    def test_csv(self):
        self.save_load("%s.csv", self.mono)

    def test_matrix(self):
        self.save_load("%s.mat", self.mono)

        for image in self.all:
            target = pyvips.Target.new_to_memory()
            image.matrixsave_target(target)

    @skip_if_no("ppmload")
    def test_ppm(self):
        self.save_load("%s.ppm", self.colour)

        self.save_load_file("%s.pgm", "[ascii]", self.mono, 0)
        self.save_load_file("%s.ppm", "[ascii]", self.colour, 0)

        self.save_load_file("%s.pbm", "[ascii]", self.onebit, 0)

        rgb16 = self.colour.colourspace("rgb16")
        grey16 = self.mono.colourspace("rgb16")

        self.save_load("%s.ppm", rgb16)

        self.save_load_file("%s.ppm", "[ascii]", grey16, 0)
        self.save_load_file("%s.ppm", "[ascii]", rgb16, 0)

        im = pyvips.Image.new_from_buffer(b'P1\n#\n#\n1 1\n0\n', "")
        assert im.height == 1
        assert im.width == 1

    @skip_if_no("radload")
    def test_rad(self):
        def hdr_valid(im):
            # might still be in RADIANCE coding
            if im.coding == "rad":
                im = im.rad2float()

            assert_almost_equal_objects(im(10, 10),
                                        [13.3, 10.8, 9.5],
                                        threshold=0.1)

            assert_almost_equal_objects(im(85, 59),
                                        [4720, 4336, 4528],
                                        threshold=1)

            assert im.width == 141
            assert im.height == 980
            assert im.bands == 3

        self.file_loader("radload", RAD_FILE, hdr_valid)
        self.buffer_loader("radload_buffer", RAD_FILE, hdr_valid)

        rad = pyvips.Image.radload(RAD_FILE)
        self.save_load("%s.hdr", rad)
        self.save_load("%s.pfm", rad)
        self.save_load("%s.tif", rad)
        self.save_buffer_tempfile("radsave_buffer", ".hdr",
                                  rad, max_diff=0)

    @skip_if_no("dzsave")
    def test_dzsave(self):
        # dzsave is hard to test, there are so many options
        # test each option separately and hope they all function together
        # correctly

        # default deepzoom layout ... we must use png here, since we want to
        # test the overlap for equality
        filename = temp_filename(self.tempdir, '')
        self.colour.dzsave(filename, suffix=".png")

        # test horizontal overlap ... expect 256 step, overlap 1
        x = pyvips.Image.new_from_file(filename + "_files/9/0_0.png")
        assert x.width == 255
        y = pyvips.Image.new_from_file(filename + "_files/9/1_0.png")
        assert y.width == 37

        # the right two columns of x should equal the left two columns of y
        left = x.crop(x.width - 2, 0, 2, x.height)
        right = y.crop(0, 0, 2, y.height)
        assert (left - right).abs().max() == 0

        # test vertical overlap
        assert x.height == 255
        y = pyvips.Image.new_from_file(filename + "_files/9/0_1.png")
        assert y.height == 189

        # the bottom two rows of x should equal the top two rows of y
        top = x.crop(0, x.height - 2, x.width, 2)
        bottom = y.crop(0, 0, y.width, 2)
        assert (top - bottom).abs().max() == 0

        # there should be a bottom layer
        x = pyvips.Image.new_from_file(filename + "_files/0/0_0.png")
        assert x.width == 1
        assert x.height == 1

        # 9 should be the final layer
        assert not os.path.isdir(filename + "_files/10")

        # default google layout
        filename = temp_filename(self.tempdir, '')
        self.colour.dzsave(filename, layout="google")

        # test bottom-right tile ... default is 256x256 tiles, overlap 0
        x = pyvips.Image.new_from_file(filename + "/1/1/1.jpg")
        assert x.width == 256
        assert x.height == 256
        assert not os.path.exists(filename + "/1/1/2.jpg")
        assert not os.path.exists(filename + "/2")
        x = pyvips.Image.new_from_file(filename + "/blank.png")
        assert x.width == 256
        assert x.height == 256

        # google layout with overlap ... verify that we clip correctly

        # overlap 1, 510x510 pixels, 256 pixel tiles, should be exactly 2x2
        # tiles, though in fact the bottom and right edges will be white
        filename = temp_filename(self.tempdir, '')
        self.colour \
            .replicate(2, 2) \
            .crop(0, 0, 510, 510) \
            .dzsave(filename, layout="google", overlap=1, depth="one")

        x = pyvips.Image.new_from_file(filename + "/0/1/1.jpg")
        assert x.width == 256
        assert x.height == 256
        assert not os.path.exists(filename + "/0/2/2.jpg")

        # with 511x511, it'll fit exactly into 2x2 -- we we actually generate
        # 3x3, since we output the overlaps
        filename = temp_filename(self.tempdir, '')
        self.colour \
            .replicate(2, 2) \
            .crop(0, 0, 511, 511) \
            .dzsave(filename, layout="google", overlap=1, depth="one")

        x = pyvips.Image.new_from_file(filename + "/0/2/2.jpg")
        assert x.width == 256
        assert x.height == 256
        assert not os.path.exists(filename + "/0/3/3.jpg")

        # default zoomify layout
        filename = temp_filename(self.tempdir, '')
        self.colour.dzsave(filename, layout="zoomify")

        # 256x256 tiles, no overlap
        assert os.path.exists(filename + "/ImageProperties.xml")
        with open(filename + "/ImageProperties.xml", 'rb') as f:
            buf = f.read()
        assert buf == (b'<IMAGE_PROPERTIES WIDTH="290" HEIGHT="442" '
                       b'NUMTILES="5" NUMIMAGES="1" VERSION="1.8" '
                       b'TILESIZE="256" />\n')
        x = pyvips.Image.new_from_file(filename + "/TileGroup0/1-0-0.jpg")
        assert x.width == 256
        assert x.height == 256

        # IIIF v2
        im = pyvips.Image.black(3509, 2506, bands=3)
        filename = temp_filename(self.tempdir, '')
        im.dzsave(filename, layout="iiif")
        assert os.path.exists(filename + "/info.json")
        assert os.path.exists(filename + "/0,0,512,512/512,/0/default.jpg")
        assert os.path.exists(filename + "/2560,2048,512,458/512,/0/default.jpg")
        x = pyvips.Image.new_from_file(filename + "/full/439,/0/default.jpg")
        assert x.width == 439
        assert x.height == 314

        # IIIF v3
        filename = temp_filename(self.tempdir, '')
        im.dzsave(filename, layout="iiif3")
        assert os.path.exists(filename + "/info.json")
        assert os.path.exists(filename + "/0,0,512,512/512,512/0/default.jpg")
        assert os.path.exists(filename + "/2560,2048,512,458/512,458/0/default.jpg")
        x = pyvips.Image.new_from_file(filename + "/full/439,314/0/default.jpg")
        assert x.width == 439
        assert x.height == 314

        # test zip output
        filename = temp_filename(self.tempdir, '.zip')
        self.colour.dzsave(filename)
        assert os.path.exists(filename)
        assert not os.path.exists(filename + "_files")
        assert not os.path.exists(filename + ".dzi")

        # test compressed zip output
        filename2 = temp_filename(self.tempdir, '.zip')
        self.colour.dzsave(filename2, compression=-1)
        assert os.path.exists(filename2)
        with open(filename, 'rb') as f:
            buf1 = f.read()
        with open(filename2, 'rb') as f:
            buf2 = f.read()
        # compressed output should produce smaller file size
        assert len(buf2) < len(buf1)
        # check whether the *.dzi file is Deflate-compressed
        assert buf1.find(b'http://schemas.microsoft.com/deepzoom/2008') != -1
        assert buf2.find(b'http://schemas.microsoft.com/deepzoom/2008') == -1

        # test suffix
        filename = temp_filename(self.tempdir, '')
        self.colour.dzsave(filename, suffix=".png")

        x = pyvips.Image.new_from_file(filename + "_files/9/0_0.png")
        assert x.width == 255

        # test overlap
        filename = temp_filename(self.tempdir, '')
        self.colour.dzsave(filename, overlap=200)

        y = pyvips.Image.new_from_file(filename + "_files/9/1_1.jpeg")
        assert y.width == 236

        # test tile-size
        filename = temp_filename(self.tempdir, '')
        self.colour.dzsave(filename, tile_size=512)

        y = pyvips.Image.new_from_file(filename + "_files/9/0_0.jpeg")
        assert y.width == 290
        assert y.height == 442

        # test save to memory buffer
        filename = temp_filename(self.tempdir, '.zip')
        base = os.path.basename(filename)
        root, ext = os.path.splitext(base)

        self.colour.dzsave(filename)
        with open(filename, 'rb') as f:
            buf1 = f.read()
        buf2 = self.colour.dzsave_buffer(basename=root)
        assert len(buf1) == len(buf2)

        # we can't test the bytes are exactly equal -- the timestamp in
        # vips-properties.xml will be different

        # added in 8.7
        buf = self.colour.dzsave_buffer(region_shrink="mean")
        buf = self.colour.dzsave_buffer(region_shrink="mode")
        buf = self.colour.dzsave_buffer(region_shrink="median")

        # test keep=pyvips.ForeignKeep.ICC ... icc profiles should be
        # passed down
        filename = temp_filename(self.tempdir, '')
        self.colour.dzsave(filename, keep=pyvips.ForeignKeep.ICC)

        y = pyvips.Image.new_from_file(filename + "_files/0/0_0.jpeg")
        assert y.get_typeof("icc-profile-data") != 0

    @skip_if_no("heifload")
    def test_heifload(self):
        def heif_valid(im):
            a = im(10, 10)
            # different versions of libheif decode have slightly different
            # rounding
            assert_almost_equal_objects(a, [197.0, 181.0, 158.0], threshold=2)
            assert im.width == 3024
            assert im.height == 4032
            assert im.bands == 3

        self.file_loader("heifload", AVIF_FILE, heif_valid)
        self.buffer_loader("heifload_buffer", AVIF_FILE, heif_valid)

        with pytest.raises(Exception) as e_info:
            im = pyvips.Image.heifload(AVIF_FILE_HUGE)
            assert im.avg() == 0.0

    @skip_if_no("heifsave")
    def test_avifsave(self):
        self.save_load_buffer("heifsave_buffer", "heifload_buffer",
                              self.colour, compression="av1", lossless=True)
        self.save_load("%s.avif", self.colour)

    @skip_if_no("heifsave")
    def test_avifsave_lossless(self):
        im = pyvips.Image.new_from_file(AVIF_FILE)
        buf = im.heifsave_buffer(effort=0, lossless=True, compression="av1")
        im2 = pyvips.Image.new_from_buffer(buf, "")
        assert (im - im2).abs().max() == 0

    @skip_if_no("heifsave")
    def test_avifsave_Q(self):
        # higher Q should mean a bigger buffer, needs libheif >= v1.8.0,
        # see: https://github.com/libvips/libvips/issues/1757
        b1 = self.mono.heifsave_buffer(Q=10, compression="av1")
        b2 = self.mono.heifsave_buffer(Q=90, compression="av1")
        assert len(b2) > len(b1)

    @skip_if_no("heifsave")
    def test_avifsave_chroma(self):
        # Chroma subsampling should produce smaller file size for same Q
        b1 = self.colour.heifsave_buffer(compression="av1", subsample_mode="on")
        b2 = self.colour.heifsave_buffer(compression="av1", subsample_mode="off")
        assert len(b2) > len(b1)

    @skip_if_no("heifsave")
    def test_avifsave_icc(self):
        # try saving an image with an ICC profile and reading it back
        # not all libheif have profile support, so put it in an if
        buf = self.colour.heifsave_buffer(Q=10, compression="av1")
        im = pyvips.Image.new_from_buffer(buf, "")
        p1 = self.colour.get("icc-profile-data")
        if im.get_typeof("icc-profile-data") != 0:
            p2 = im.get("icc-profile-data")
            assert p1 == p2

        # add tests for xmp, iptc
        # the exif test will need us to be able to walk the header,
        # we can't just check exif-data

    @skip_if_no("heifsave")
    def test_avifsave_exif(self):
        # first make sure we have exif support
        x = pyvips.Image.new_from_file(JPEG_FILE)
        if x.get_typeof("exif-ifd0-Orientation") != 0:
            x = x.copy()
            x.set_type(pyvips.GValue.gstr_type, "exif-ifd0-XPComment", "banana")
            buf = x.heifsave_buffer(Q=10, compression="av1")
            y = pyvips.Image.new_from_buffer(buf, "")
            assert y.get("exif-ifd0-XPComment").startswith("banana")

    @skip_if_no("heifsave")
    def test_avifsave_tune(self):
        buf = self.colour.heifsave_buffer(compression="av1", tune="ssim")
        assert len(buf) > 10000

    @skip_if_no("heifsave")
    @pytest.mark.xfail(raises=pyvips.error.Error, reason="requires libheif built with patent-encumbered HEVC dependencies")
    def test_heicsave_16_to_12(self):
        rgb16 = self.colour.colourspace("rgb16")
        data = rgb16.heifsave_buffer(lossless=True)
        im = pyvips.Image.heifload_buffer(data)

        assert(im.width == rgb16.width)
        assert(im.format == rgb16.format)
        assert(im.interpretation == rgb16.interpretation)
        assert(im.get("bits-per-sample") == 12)
        # good grief, some kind of lossless
        assert((im - rgb16).abs().max() < 4500)

    @skip_if_no("heifsave")
    @pytest.mark.xfail(raises=pyvips.error.Error, reason="requires libheif built with patent-encumbered HEVC dependencies")
    def test_heicsave_16_to_8(self):
        rgb16 = self.colour.colourspace("rgb16")
        data = rgb16.heifsave_buffer(lossless=True, bitdepth=8)
        im = pyvips.Image.heifload_buffer(data)

        assert(im.width == rgb16.width)
        assert(im.format == "uchar")
        assert(im.interpretation == "srgb")
        assert(im.get("bits-per-sample") == 8)
        # good grief, some kind of lossless
        assert((im - rgb16 / 256).abs().max() < 80)

    @skip_if_no("heifsave")
    @pytest.mark.xfail(raises=pyvips.error.Error, reason="requires libheif built with patent-encumbered HEVC dependencies")
    def test_heicsave_8_to_16(self):
        data = self.colour.heifsave_buffer(lossless=True, bitdepth=12)
        im = pyvips.Image.heifload_buffer(data)

        assert(im.width == self.colour.width)
        assert(im.format == "ushort")
        assert(im.interpretation == "rgb16")
        assert(im.get("bits-per-sample") == 12)
        # good grief, some kind of lossless
        assert((im - self.colour * 256).abs().max() < 4500)

    @skip_if_no("jp2kload")
    def test_jp2kload(self):
        def jp2k_valid(im):
            a = im(402, 73)
            assert_almost_equal_objects(a, [141, 144, 73], threshold=2)
            assert im.width == 800
            assert im.height == 400
            assert im.bands == 3
            assert im.get("bits-per-sample") == 8
            assert im.get("tile-width") == 800
            assert im.get("tile-height") == 400

        self.file_loader("jp2kload", JP2K_FILE, jp2k_valid)
        self.buffer_loader("jp2kload_buffer", JP2K_FILE, jp2k_valid)

        # Bretagne2_4.j2k is a tiled jpeg2000 image with 127x127 pixel tiles,
        # triggering tricky rounding issues
        filename = os.path.join(IMAGES, "Bretagne2_4.j2k")
        im4 = pyvips.Image.new_from_file(filename, page=4)
        im3 = pyvips.Image.new_from_file(filename, page=3)
        assert abs(im4.avg() - im3.avg()) < 0.2

        # Bretagne2_1.j2k is an untiled jpeg2000 image with non-zero offset
        filename = os.path.join(IMAGES, "Bretagne2_1.j2k")
        im4 = pyvips.Image.new_from_file(filename, page=4)
        im3 = pyvips.Image.new_from_file(filename, page=3)
        assert abs(im4.avg() - im3.avg()) < 0.5

        # issue412.jp2 is a palette image: the header declares a single
        # palette index component, and openjpeg expands it to three bands
        # during decode ... we cannot detect such images with openjpeg < 2.5.1
        # at header read time, so decoding fails
        filename = os.path.join(IMAGES, "issue412.jp2")
        im = pyvips.Image.new_from_file(filename)
        assert im.bands in (1, 3)
        assert im.interpretation in ("b-w", "srgb")
        try:
            assert abs(im.avg() - 123.318) < 0.1
        except pyvips.error.Error as e:
            assert "decoded image does not match container" in str(e)

    @skip_if_no("jp2kload")
    def test_jp2kload_palette(self):
        # palette.jp2 is a paletted JP2 (see
        # https://github.com/libvips/libvips/issues/5027): the header
        # declares a single 8-bit palette index component plus pclr/cmap
        # boxes, and openjpeg expands it to three sRGB bands during decode
        #
        # pixel (0, 0) holds index 0, whose palette entry is (0, 255, 0);
        # pixel (1, 0) holds index 4, whose entry is (68, 187, 148)
        filename = os.path.join(IMAGES, "palette.jp2")

        im = pyvips.Image.new_from_file(filename)
        assert im.width == 64
        assert im.height == 64
        assert im.bands in (1, 3)
        assert im.format == "uchar"
        assert im.interpretation in ("b-w", "srgb")
        try:
           assert im(0, 0) == [0, 255, 0]
           assert im(1, 0) == [68, 187, 148]
        except pyvips.error.Error as e:
            assert "decoded image does not match container" in str(e)

    @skip_if_no("jp2ksave")
    def test_jp2ksave(self):
        self.save_load_buffer("jp2ksave_buffer", "jp2kload_buffer",
                              self.colour, 80)

        buf = self.colour.jp2ksave_buffer(lossless=True)
        im2 = pyvips.Image.new_from_buffer(buf, "")
        assert (self.colour == im2).min() == 255

        # higher Q should mean a bigger buffer
        b1 = self.mono.jp2ksave_buffer(Q=10)
        b2 = self.mono.jp2ksave_buffer(Q=90)
        assert len(b2) > len(b1)

        # disabling chroma subsample should mean a bigger buffer
        b1 = self.colour.jp2ksave_buffer(subsample_mode="on")
        b2 = self.colour.jp2ksave_buffer(subsample_mode="off")
        assert len(b2) > len(b1)

        # regression test for OOB read with odd dimensions and chroma
        # subsampling, see https://github.com/libvips/libvips/pull/5020
        im = pyvips.Image.black(22, 3, bands=3)
        im.jp2ksave_buffer(subsample_mode="on")

        # regression test for heap buffer overflow with chroma subsampling
        # on non-RGB images, see https://github.com/libvips/libvips/pull/5030
        im = pyvips.Image.black(32, 32, bands=1)
        im.jp2ksave_buffer(subsample_mode="on")

        # enabling lossless should mean a bigger buffer
        b1 = self.colour.jp2ksave_buffer(lossless=False)
        b2 = self.colour.jp2ksave_buffer(lossless=True)
        assert len(b2) > len(b1)

        # 16-bit colour load and save
        im = self.colour.colourspace("rgb16")
        buf = im.jp2ksave_buffer(lossless=True)
        im2 = pyvips.Image.new_from_buffer(buf, "")
        assert (im == im2).min() == 255
        assert im2.get("bits-per-sample") == 16

        # openjpeg 32-bit load and save doesn't seem to work, comment out
        # im = self.colour.colourspace("rgb16").cast("uint") << 14
        # buf = im.jp2ksave_buffer(lossless=True)
        # im2 = pyvips.Image.new_from_buffer(buf, "")
        # assert (im == im2).min() == 255

    @skip_if_no("jxlsave")
    def test_jxlsave(self):
        # save and load with an icc profile
        self.save_load_buffer("jxlsave_buffer", "jxlload_buffer",
                              self.colour, 130)

        # with no icc profile
        no_profile = self.colour.copy()
        no_profile.remove("icc-profile-data")
        self.save_load_buffer("jxlsave_buffer", "jxlload_buffer",
                              no_profile, 120)

        # scrgb mode
        scrgb = self.colour.colourspace("scrgb")
        self.save_load_buffer("jxlsave_buffer", "jxlload_buffer",
                              scrgb, 120)

        # scrgb mode, no profile
        scrgb_no_profile = scrgb.copy()
        scrgb_no_profile.remove("icc-profile-data")
        self.save_load_buffer("jxlsave_buffer", "jxlload_buffer",
                              scrgb_no_profile, 120)

        # 16-bit mode
        rgb16 = self.colour.colourspace("rgb16").copy()
        # remove the ICC profile: the RGB one will no longer be appropriate
        rgb16.remove("icc-profile-data")
        self.save_load_buffer("jxlsave_buffer", "jxlload_buffer",
                              rgb16, 12000)

        # repeat for lossless mode
        self.save_load_buffer("jxlsave_buffer", "jxlload_buffer",
                              self.colour, 0, lossless=True)
        self.save_load_buffer("jxlsave_buffer", "jxlload_buffer",
                              no_profile, 0, lossless=True)
        self.save_load_buffer("jxlsave_buffer", "jxlload_buffer",
                              scrgb, 0, lossless=True)
        self.save_load_buffer("jxlsave_buffer", "jxlload_buffer",
                              scrgb_no_profile, 0, lossless=True)

        # lossy should be much smaller than lossless
        lossy = self.colour.jxlsave_buffer()
        lossless = self.colour.jxlsave_buffer(lossless=True)
        assert len(lossy) < len(lossless) / 5

        # bitdepth=8 should be saved as uchar
        buf = rgb16.jxlsave_buffer(bitdepth=8)
        im = pyvips.Image.jxlload_buffer(buf)
        assert im.format == "uchar"
        assert im.get("bits-per-sample") == 8

    @skip_if_no("gifload")
    @skip_if_no("gifsave")
    def test_gifsave(self):
        # Animated GIF round trip
        x1 = pyvips.Image.new_from_file(GIF_ANIM_FILE, n=-1)
        b1 = x1.gifsave_buffer()
        x2 = pyvips.Image.new_from_buffer(b1, "", n=-1)
        assert x1.width == x2.width
        assert x1.height == x2.height
        assert x1.get("n-pages") == x2.get("n-pages")
        assert x1.get("delay") == x2.get("delay")
        assert x1.get("page-height") == x2.get("page-height")
        # FIXME ... this requires cgif0.3 or later for fixed loop support
        # assert x1.get("loop") == x2.get("loop")

        # Interlaced write
        x1 = pyvips.Image.new_from_file(GIF_FILE, n=-1)
        b1 = x1.gifsave_buffer(interlace=False)
        b2 = x1.gifsave_buffer(interlace=True)
        # Interlaced GIFs are usually larger in file size
        # FIXME ... cgif v0.3 or later required for interlaced write.
        # If interlaced write is not supported b2 and b1 are expected to be
        # of the same file size.
        assert len(b2) >= len(b1)

        # Reducing dither will typically reduce file size (and quality)
        little_dither = self.colour.gifsave_buffer(dither=0.1, effort=1)
        large_dither = self.colour.gifsave_buffer(dither=0.9, effort=1)
        assert len(little_dither) < len(large_dither)

        # Reducing effort will typically increase file size (and reduce quality)
        # quantizr does not yet implement effort, so use >=
        little_effort = self.colour.gifsave_buffer(effort=1)
        large_effort = self.colour.gifsave_buffer(effort=10)
        assert len(little_effort) >= len(large_effort)

        # Reducing bitdepth will typically reduce file size (and reduce quality)
        bitdepth8 = self.colour.gifsave_buffer(bitdepth=8,effort=1)
        bitdepth7 = self.colour.gifsave_buffer(bitdepth=7,effort=1)
        assert len(bitdepth8) > len(bitdepth7)

        if have("webpload"):
            # Animated WebP to GIF
            x1 = pyvips.Image.new_from_file(WEBP_ANIMATED_FILE, n=-1)
            b1 = x1.gifsave_buffer()
            x2 = pyvips.Image.new_from_buffer(b1, "", n=-1)
            assert x1.width == x2.width
            assert x1.height == x2.height
            assert x1.get("n-pages") == x2.get("n-pages")
            assert x1.get("delay") == x2.get("delay")
            assert x1.get("page-height") == x2.get("page-height")
            assert x1.get("loop") == x2.get("loop")

    def test_fail_on(self):
        # csvload should spot trunc correctly
        target = pyvips.Target.new_to_memory()
        self.mono.write_to_target(target, ".csv")
        buf = target.get("blob")

        source = pyvips.Source.new_from_memory(buf)
        im = pyvips.Image.csvload_source(source)
        assert im.avg() > 0

        # truncation should be OK by default
        buf_trunc = buf[:-100]
        source = pyvips.Source.new_from_memory(buf_trunc)
        im = pyvips.Image.csvload_source(source)
        assert im.avg() > 0

        # set trunc should make it fail
        with pytest.raises(Exception) as e_info:
            im = pyvips.Image.csvload_source(source, fail_on="truncated")
            # this will now force parsing of the whole file, which should
            # trigger an error
            im.avg() > 0

        # warn should fail too, since trunc implies warn
        with pytest.raises(Exception) as e_info:
            im = pyvips.Image.csvload_source(source, fail_on="warning")
            im.avg() > 0

if __name__ == '__main__':
    pytest.main()
