Beispiel #1
0
def _tiff_save(im, fp, filename):
	from PIL import TiffImagePlugin
	# check compression mode
	try:
		compression = im.encoderinfo["compression"]
	except KeyError:
		# use standard driver
		return TiffImagePlugin._save(im, fp, filename)
	# compress via temporary file
	#if compression not in ("lzw", "zip", "jpeg", "packbits", "g3", "g4"):
	if compression not in ("lzw", "lzw:2"):
		raise IOError("unknown TIFF compression mode")
	fp.close()
	import tempfile, os, subprocess
	tiffcp = findExecutable('tiffcp')
	if tiffcp == None:
		raise IOError("TIFF compression failed: missing tiffcp program")
	t, tmp = tempfile.mkstemp(suffix='.tif')
	t = os.fdopen(t, 'wb')
	TiffImagePlugin._save(im, t, tmp)
	t.close()
	retcode = subprocess.call([tiffcp, "-c", compression, tmp, filename])
	try:
		os.remove(tmp)
	except OSError:
		pass
	if retcode != 0:
		raise IOError("TIFF compression failed")
Beispiel #2
0
    def test_custom_metadata(self):
        custom = {
            37000: 4,
            37001: 4.2,
            37002: 'custom tag value',
            37003: u'custom tag value',
            37004: b'custom tag value'
        }

        libtiff_version = TiffImagePlugin._libtiff_version()

        libtiffs = [False]
        if distutils.version.StrictVersion(libtiff_version) >= \
           distutils.version.StrictVersion("4.0"):
            libtiffs.append(True)

        for libtiff in libtiffs:
            TiffImagePlugin.WRITE_LIBTIFF = libtiff

            im = hopper()

            out = self.tempfile("temp.tif")
            im.save(out, tiffinfo=custom)
            TiffImagePlugin.WRITE_LIBTIFF = False

            reloaded = Image.open(out)
            for tag, value in custom.items():
                if libtiff and isinstance(value, bytes):
                    value = value.decode()
                self.assertEqual(reloaded.tag_v2[tag], value)
Beispiel #3
0
    def test_custom_metadata(self):
        custom = {
            37000: [4, TiffTags.SHORT],
            37001: [4.2, TiffTags.RATIONAL],
            37002: ['custom tag value', TiffTags.ASCII],
            37003: [u'custom tag value', TiffTags.ASCII],
            37004: [b'custom tag value', TiffTags.BYTE]
        }

        libtiff_version = TiffImagePlugin._libtiff_version()

        libtiffs = [False]
        if distutils.version.StrictVersion(libtiff_version) >= \
           distutils.version.StrictVersion("4.0"):
            libtiffs.append(True)

        for libtiff in libtiffs:
            TiffImagePlugin.WRITE_LIBTIFF = libtiff

            def check_tags(tiffinfo):
                im = hopper()

                out = self.tempfile("temp.tif")
                im.save(out, tiffinfo=tiffinfo)

                reloaded = Image.open(out)
                for tag, value in tiffinfo.items():
                    reloaded_value = reloaded.tag_v2[tag]
                    if isinstance(reloaded_value, TiffImagePlugin.IFDRational):
                        reloaded_value = float(reloaded_value)

                    if libtiff and isinstance(value, bytes):
                        value = value.decode()

                    self.assertEqual(reloaded_value, value)

            # Test with types
            ifd = TiffImagePlugin.ImageFileDirectory_v2()
            for tag, tagdata in custom.items():
                ifd[tag] = tagdata[0]
                ifd.tagtype[tag] = tagdata[1]
            check_tags(ifd)

            # Test without types
            check_tags({tag: tagdata[0] for tag, tagdata in custom.items()})
        TiffImagePlugin.WRITE_LIBTIFF = False
Beispiel #4
0
def _getmp(self):
    # Extract MP information.  This method was inspired by the "highly
    # experimental" _getexif version that's been in use for years now,
    # itself based on the ImageFileDirectory class in the TIFF plug-in.

    # The MP record essentially consists of a TIFF file embedded in a JPEG
    # application marker.
    try:
        data = self.info["mp"]
    except KeyError:
        return None
    file_contents = io.BytesIO(data)
    head = file_contents.read(8)
    endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<'
    mp = {}
    # process dictionary
    info = TiffImagePlugin.ImageFileDirectory(head)
    info.load(file_contents)
    for key, value in info.items():
        mp[key] = _fixup(value)
    # it's an error not to have a number of images
    try:
        quant = mp[0xB001]
    except KeyError:
        raise SyntaxError("malformed MP Index (no number of images)")
    # get MP entries
    try:
        mpentries = []
        for entrynum in range(0, quant):
            rawmpentry = mp[0xB002][entrynum * 16:(entrynum + 1) * 16]
            unpackedentry = unpack('{0}LLLHH'.format(endianness), rawmpentry)
            labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1',
                      'EntryNo2')
            mpentry = dict(zip(labels, unpackedentry))
            mpentryattr = {
                'DependentParentImageFlag': bool(mpentry['Attribute'] &
                                                 (1 << 31)),
                'DependentChildImageFlag': bool(mpentry['Attribute'] &
                                                (1 << 30)),
                'RepresentativeImageFlag': bool(mpentry['Attribute'] &
                                                (1 << 29)),
                'Reserved': (mpentry['Attribute'] & (3 << 27)) >> 27,
                'ImageDataFormat': (mpentry['Attribute'] & (7 << 24)) >> 24,
                'MPType': mpentry['Attribute'] & 0x00FFFFFF
            }
            if mpentryattr['ImageDataFormat'] == 0:
                mpentryattr['ImageDataFormat'] = 'JPEG'
            else:
                raise SyntaxError("unsupported picture format in MPO")
            mptypemap = {
                0x000000: 'Undefined',
                0x010001: 'Large Thumbnail (VGA Equivalent)',
                0x010002: 'Large Thumbnail (Full HD Equivalent)',
                0x020001: 'Multi-Frame Image (Panorama)',
                0x020002: 'Multi-Frame Image: (Disparity)',
                0x020003: 'Multi-Frame Image: (Multi-Angle)',
                0x030000: 'Baseline MP Primary Image'
            }
            mpentryattr['MPType'] = mptypemap.get(mpentryattr['MPType'],
                                                  'Unknown')
            mpentry['Attribute'] = mpentryattr
            mpentries.append(mpentry)
        mp[0xB002] = mpentries
    except KeyError:
        raise SyntaxError("malformed MP Index (bad MP Entry)")
    # Next we should try and parse the individual image unique ID list;
    # we don't because I've never seen this actually used in a real MPO
    # file and so can't test it.
    return mp
Beispiel #5
0
 def test_load_string(self):
     ifd = TiffImagePlugin.ImageFileDirectory_v2()
     data = b"abc\0"
     ret = ifd.load_string(data, False)
     self.assertEqual(ret, "abc")
Beispiel #6
0
 def test_load_float(self):
     ifd = TiffImagePlugin.ImageFileDirectory_v2()
     data = b"abcdabcd"
     ret = ifd.load_float(data, False)
     self.assertEqual(ret, (1.6777999408082104e22, 1.6777999408082104e22))
Beispiel #7
0
    def test_custom_metadata(self, tmp_path):
        tc = namedtuple("test_case", "value,type,supported_by_default")
        custom = {
            37000 + k: v
            for k, v in enumerate([
                tc(4, TiffTags.SHORT, True),
                tc(123456789, TiffTags.LONG, True),
                tc(-4, TiffTags.SIGNED_BYTE, False),
                tc(-4, TiffTags.SIGNED_SHORT, False),
                tc(-123456789, TiffTags.SIGNED_LONG, False),
                tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
                tc(4.25, TiffTags.FLOAT, True),
                tc(4.25, TiffTags.DOUBLE, True),
                tc("custom tag value", TiffTags.ASCII, True),
                tc(b"custom tag value", TiffTags.BYTE, True),
                tc((4, 5, 6), TiffTags.SHORT, True),
                tc((123456789, 9, 34, 234, 219387,
                    92432323), TiffTags.LONG, True),
                tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
                tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
                tc(
                    (-123456789, 9, 34, 234, 219387, -92432323),
                    TiffTags.SIGNED_LONG,
                    False,
                ),
                tc((4.25, 5.25), TiffTags.FLOAT, True),
                tc((4.25, 5.25), TiffTags.DOUBLE, True),
                # array of TIFF_BYTE requires bytes instead of tuple for backwards
                # compatibility
                tc(bytes([4]), TiffTags.BYTE, True),
                tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
            ])
        }

        libtiffs = [False]
        if Image.core.libtiff_support_custom_tags:
            libtiffs.append(True)

        for libtiff in libtiffs:
            TiffImagePlugin.WRITE_LIBTIFF = libtiff

            def check_tags(tiffinfo):
                im = hopper()

                out = str(tmp_path / "temp.tif")
                im.save(out, tiffinfo=tiffinfo)

                with Image.open(out) as reloaded:
                    for tag, value in tiffinfo.items():
                        reloaded_value = reloaded.tag_v2[tag]
                        if (isinstance(reloaded_value,
                                       TiffImagePlugin.IFDRational)
                                and libtiff):
                            # libtiff does not support real RATIONALS
                            assert (round(
                                abs(float(reloaded_value) - float(value)),
                                7) == 0)
                            continue

                        assert reloaded_value == value

            # Test with types
            ifd = TiffImagePlugin.ImageFileDirectory_v2()
            for tag, tagdata in custom.items():
                ifd[tag] = tagdata.value
                ifd.tagtype[tag] = tagdata.type
            check_tags(ifd)

            # Test without types. This only works for some types, int for example are
            # always encoded as LONG and not SIGNED_LONG.
            check_tags({
                tag: tagdata.value
                for tag, tagdata in custom.items()
                if tagdata.supported_by_default
            })
        TiffImagePlugin.WRITE_LIBTIFF = False
Beispiel #8
0
 def test_load_byte(self):
     for legacy_api in [False, True]:
         ifd = TiffImagePlugin.ImageFileDirectory_v2()
         data = b"abc"
         ret = ifd.load_byte(data, legacy_api)
         self.assertEqual(ret, b"abc")
def test_rt_metadata(tmp_path):
    """Test writing arbitrary metadata into the tiff image directory
    Use case is ImageJ private tags, one numeric, one arbitrary
    data.  https://github.com/python-pillow/Pillow/issues/291
    """

    img = hopper()

    # Behaviour change: re #1416
    # Pre ifd rewrite, ImageJMetaData was being written as a string(2),
    # Post ifd rewrite, it's defined as arbitrary bytes(7). It should
    # roundtrip with the actual bytes, rather than stripped text
    # of the premerge tests.
    #
    # For text items, we still have to decode('ascii','replace') because
    # the tiff file format can't take 8 bit bytes in that field.

    basetextdata = "This is some arbitrary metadata for a text field"
    bindata = basetextdata.encode("ascii") + b" \xff"
    textdata = basetextdata + " " + chr(255)
    reloaded_textdata = basetextdata + " ?"
    floatdata = 12.345
    doubledata = 67.89
    info = TiffImagePlugin.ImageFileDirectory()

    ImageJMetaData = TAG_IDS["ImageJMetaData"]
    ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"]
    ImageDescription = TAG_IDS["ImageDescription"]

    info[ImageJMetaDataByteCounts] = len(bindata)
    info[ImageJMetaData] = bindata
    info[TAG_IDS["RollAngle"]] = floatdata
    info.tagtype[TAG_IDS["RollAngle"]] = 11
    info[TAG_IDS["YawAngle"]] = doubledata
    info.tagtype[TAG_IDS["YawAngle"]] = 12

    info[ImageDescription] = textdata

    f = str(tmp_path / "temp.tif")

    img.save(f, tiffinfo=info)

    with Image.open(f) as loaded:

        assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata), )
        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata), )

        assert loaded.tag[ImageJMetaData] == bindata
        assert loaded.tag_v2[ImageJMetaData] == bindata

        assert loaded.tag[ImageDescription] == (reloaded_textdata, )
        assert loaded.tag_v2[ImageDescription] == reloaded_textdata

        loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0]
        assert round(abs(loaded_float - floatdata), 5) == 0
        loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0]
        assert round(abs(loaded_double - doubledata), 7) == 0

    # check with 2 element ImageJMetaDataByteCounts, issue #2006

    info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8)
    img.save(f, tiffinfo=info)
    with Image.open(f) as loaded:

        assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
def test_empty_metadata():
    f = io.BytesIO(b"II*\x00\x08\x00\x00\x00")
    head = f.read(8)
    info = TiffImagePlugin.ImageFileDirectory(head)
    # Should not raise struct.error.
    pytest.warns(UserWarning, info.load, f)
Beispiel #11
0
    def test_invalid_file(self):
        invalid_file = "Tests/images/flower.jpg"

        self.assertRaises(SyntaxError,
                          lambda: TiffImagePlugin.TiffImageFile(invalid_file))
Beispiel #12
0
    for tif_file in tif_files:
        # file_name_without_extension = os.path.basename(tif_file).replace('.tif', '')
        file_path_without_extension = tif_file.replace('.tif', '')
        box_file = file_path_without_extension + '.box'

        tif_img = Image.open(tif_file, mode='r')
        for pepper_noise_setting in pepper_noise_settings:
            # Copy box file
            out_box_file = file_path_without_extension + '_pepper-{}-{}.box'.format(
                pepper_noise_setting[0], pepper_noise_setting[1])
            shutil.copyfile(box_file, out_box_file)

            # Make new .tif file with noise
            out_tif_file = file_path_without_extension + '_pepper-{}-{}.tif'.format(
                pepper_noise_setting[0], pepper_noise_setting[1])
            with TiffImagePlugin.AppendingTiffWriter(out_tif_file, True) as tf:
                findex = 0
                while True:
                    try:
                        tif_img.seek(findex)
                    except Exception:
                        break
                    frame = tif_img.copy()

                    # Add noise
                    add_pepper_noise(
                        frame,
                        row_freq=pepper_noise_setting[0],
                        points_each_row_fraction=pepper_noise_setting[1])

                    frame.save(tf)
Beispiel #13
0
def addMetaData(path, job, result):
    """ Use this method to add meta data to the image. Due to a bug in
    exiv2, its python wrapper pyexiv2 is of no use to us. This bug
    (http://dev.exiv2.org/issues/762) hinders us to work on multi-page
    TIFF files. Instead, we use Pillow to write meta data.
    """
    # Add resolution information in pixel per nanometer. The stack info
    # available is nm/px and refers to a zoom-level of zero.
    res_x_scaled = job.ref_stack.resolution.x * 2**job.zoom_level
    res_y_scaled = job.ref_stack.resolution.y * 2**job.zoom_level
    res_x_nm_px = 1.0 / res_x_scaled
    res_y_nm_px = 1.0 / res_y_scaled
    res_z_nm_px = 1.0 / job.ref_stack.resolution.z
    ifd = dict()
    ifd = TiffImagePlugin.ImageFileDirectory_v2()
    ifd[TiffImagePlugin.X_RESOLUTION] = res_x_nm_px
    ifd[TiffImagePlugin.Y_RESOLUTION] = res_y_nm_px
    ifd[TiffImagePlugin.RESOLUTION_UNIT] = 1  # 1 = None

    # ImageJ specific meta data to allow easy embedding of units and
    # display options.
    n_images = len(result)
    ij_version = "1.51n"
    unit = "nm"

    n_channels = len(job.stack_mirrors)
    if n_images % n_channels != 0:
        raise ValueError( "Meta data creation: the number of images " \
                "modulo the channel count is not zero" )
    n_slices = n_images / n_channels

    # sample with (the actual is a line break instead of a .):
    # ImageJ=1.45p.images={0}.channels=1.slices=2.hyperstack=true.mode=color.unit=micron.finterval=1.spacing=1.5.loop=false.min=0.0.max=4095.0.
    ij_data = [
        "ImageJ={}".format(ij_version),
        "unit={}".format(unit),
        "spacing={}".format(str(res_z_nm_px)),
    ]

    if n_channels > 1:
        ij_data.append("images={}".format(str(n_images)))
        ij_data.append("slices={}".format(str(n_slices)))
        ij_data.append("channels={}".format(str(n_channels)))
        ij_data.append("hyperstack=true")
        ij_data.append("mode=composite")

    # We want to end with a final newline
    ij_data.append("")

    ifd[TiffImagePlugin.IMAGEDESCRIPTION] = "\n".join(ij_data)

    # Information about the software used
    ifd[TiffImagePlugin.SOFTWARE] = "CATMAID {}".format(settings.VERSION)

    image = PILImage.open(path)
    # Can't use libtiff for saving non core libtiff exif tags, therefore
    # compression="raw" is used. Also, we don't want to re-encode.
    tmp_path = path + ".tmp"
    image.save(tmp_path,
               "tiff",
               compression="raw",
               tiffinfo=ifd,
               save_all=True)
    os.remove(path)
    os.rename(tmp_path, path)
Beispiel #14
0
 def test_set_legacy_api(self):
     ifd = TiffImagePlugin.ImageFileDirectory_v2()
     with pytest.raises(Exception) as e:
         ifd.legacy_api = None
     assert str(e.value) == "Not allowing setting of legacy api"
 def test_set_legacy_api(self):
     ifd = TiffImagePlugin.ImageFileDirectory_v2()
     with self.assertRaises(Exception) as e:
         ifd.legacy_api = None
     self.assertEqual(str(e.exception),
                      "Not allowing setting of legacy api")
 def test_empty_metadata(self):
     f = io.BytesIO(b'II*\x00\x08\x00\x00\x00')
     head = f.read(8)
     info = TiffImagePlugin.ImageFileDirectory(head)
     # Should not raise struct.error.
     self.assert_warning(UserWarning, info.load, f)
    def test_rt_metadata(self):
        """ Test writing arbitrary metadata into the tiff image directory
            Use case is ImageJ private tags, one numeric, one arbitrary
            data.  https://github.com/python-pillow/Pillow/issues/291
            """

        img = hopper()

        # Behaviour change: re #1416
        # Pre ifd rewrite, ImageJMetaData was being written as a string(2),
        # Post ifd rewrite, it's defined as arbitrary bytes(7). It should
        # roundtrip with the actual bytes, rather than stripped text
        # of the premerge tests.
        #
        # For text items, we still have to decode('ascii','replace') because
        # the tiff file format can't take 8 bit bytes in that field.

        basetextdata = "This is some arbitrary metadata for a text field"
        bindata = basetextdata.encode('ascii') + b" \xff"
        textdata = basetextdata + " " + chr(255)
        reloaded_textdata = basetextdata + " ?"
        floatdata = 12.345
        doubledata = 67.89
        info = TiffImagePlugin.ImageFileDirectory()

        ImageJMetaData = tag_ids['ImageJMetaData']
        ImageJMetaDataByteCounts = tag_ids['ImageJMetaDataByteCounts']
        ImageDescription = tag_ids['ImageDescription']

        info[ImageJMetaDataByteCounts] = len(bindata)
        info[ImageJMetaData] = bindata
        info[tag_ids['RollAngle']] = floatdata
        info.tagtype[tag_ids['RollAngle']] = 11
        info[tag_ids['YawAngle']] = doubledata
        info.tagtype[tag_ids['YawAngle']] = 12

        info[ImageDescription] = textdata

        f = self.tempfile("temp.tif")

        img.save(f, tiffinfo=info)

        loaded = Image.open(f)

        self.assertEqual(loaded.tag[ImageJMetaDataByteCounts],
                         (len(bindata), ))
        self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts],
                         (len(bindata), ))

        self.assertEqual(loaded.tag[ImageJMetaData], bindata)
        self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata)

        self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata, ))
        self.assertEqual(loaded.tag_v2[ImageDescription], reloaded_textdata)

        loaded_float = loaded.tag[tag_ids['RollAngle']][0]
        self.assertAlmostEqual(loaded_float, floatdata, places=5)
        loaded_double = loaded.tag[tag_ids['YawAngle']][0]
        self.assertAlmostEqual(loaded_double, doubledata)

        # check with 2 element ImageJMetaDataByteCounts, issue #2006

        info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8)
        img.save(f, tiffinfo=info)
        loaded = Image.open(f)

        self.assertEqual(loaded.tag[ImageJMetaDataByteCounts],
                         (8, len(bindata) - 8))
        self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts],
                         (8, len(bindata) - 8))
Beispiel #18
0
 def test_load_double(self):
     ifd = TiffImagePlugin.ImageFileDirectory_v2()
     data = b"abcdefghabcdefgh"
     ret = ifd.load_double(data, False)
     self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194))
Beispiel #19
0
    def create_tiff_metadata(self, n_images):
        # Add resolution information in pixel per nanometer. The stack info
        # available is nm/px and refers to a zoom-level of zero.
        res_x_scaled = self.ref_stack.resolution.x * 2**self.zoom_level
        res_y_scaled = self.ref_stack.resolution.y * 2**self.zoom_level
        res_x_nm_px = 1.0 / res_x_scaled
        res_y_nm_px = 1.0 / res_y_scaled
        res_z_nm_px = 1.0 / self.ref_stack.resolution.z
        ifd = TiffImagePlugin.ImageFileDirectory_v2()
        ifd[TiffImagePlugin.X_RESOLUTION] = res_x_nm_px
        ifd[TiffImagePlugin.Y_RESOLUTION] = res_y_nm_px
        ifd[TiffImagePlugin.RESOLUTION_UNIT] = 1 # 1 = None

        # ImageJ specific meta data to allow easy embedding of units and
        # display options.
        ij_version= "1.51n"
        unit = "nm"

        n_channels = len(self.stack_mirrors)
        if n_images % n_channels != 0:
            raise ValueError( "Meta data creation: the number of images " \
                    "modulo the channel count is not zero" )
        n_slices = n_images / n_channels

        # Add bounding box information both to the image description tag
        # (displayable in debug mode in ImageJ). And also misuse the ARTIST tag
        # (code 315) to store this information.
        bb = {
            "minx": self.x_min,
            "miny": self.y_min,
            "minz": self.z_min,
            "maxx": self.x_max,
            "maxy": self.y_max,
            "maxz": self.z_max,
        }
        artist_meta = {
            'resx': res_x_nm_px,
            'resy': res_y_nm_px,
            'resz': res_z_nm_px,
            'zoomlevel': self.zoom_level,
            'rotation_cw': self.rotation_cw,
            'ref_stack_id': self.ref_stack.id,
        }
        artist_meta.update(bb)
        ifd[TiffImagePlugin.ARTIST] = json.dumps(artist_meta)

        # sample with (the actual is a line break instead of a .):
        # ImageJ=1.45p.images={0}.channels=1.slices=2.hyperstack=true.mode=color.unit=micron.finterval=1.spacing=1.5.loop=false.min=0.0.max=4095.0.
        ij_data = [
            f"ImageJ={ij_version}",
            f"unit={unit}",
            f"spacing={str(res_z_nm_px)}",
        ]

        if n_channels > 1:
            ij_data.append(f"images={n_images}")
            ij_data.append(f"slices={n_slices}")
            ij_data.append(f"channels={n_channels}")
            ij_data.append("hyperstack=true")
            ij_data.append("mode=composite")

        for k,v in bb.items():
            ij_data.append(f'{k}={v}')

        # Add information on the exported view
        ij_data.append(f"zoomlevel={self.zoom_level}")
        ij_data.append(f"rotation_cw={self.rotation_cw}")
        ij_data.append(f"ref_stack_id={self.ref_stack.id}")

        # We want to end with a final newline
        ij_data.append("")

        ifd[TiffImagePlugin.IMAGEDESCRIPTION] = "\n".join(ij_data)

        # Information about the software used
        ifd[TiffImagePlugin.SOFTWARE] = f"CATMAID {settings.VERSION}"

        return ifd