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")
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)
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
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
def test_load_string(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" ret = ifd.load_string(data, False) self.assertEqual(ret, "abc")
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))
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
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)
def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" self.assertRaises(SyntaxError, lambda: TiffImagePlugin.TiffImageFile(invalid_file))
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)
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)
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))
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))
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