Introduction to TIFF Images
The Tag Image File Format (TIFF or TIF) is a complex format (full spec here). In particular, each TIFF file may contain several related images (frames) and additional information (tags) organized into an Image File Directory (IFD) structure. IFD uses a key-value format, similar to a python dictionnay - all keys are numbers. Many tags are predefined, such at the tags providing the image width, length, compression, ...> > The Class
TIFFImagedefined below handles commonly needed actions with TIFF such as retrieving tags values, adding new tags, exploring all the frames, extracting all or specific frames into separate image files ('tiff' or 'jpg').The module
PIL.TiffImagePluginoffers a set of classes to easily handle TIFF, among which:> - loading TIFF files and work with frames usingTiffImageFile(path). It operates likeImagebut with specific methods and properties for TIFF. For example,.seek(frame_nbr)selects one of the frames and makes it active. After that, any action on theTiffImageFileobject will apply to that particular frame..n_framesto get the total numer of frames in the file. It will inherit properties such assize, ...> - IFD predefined tags can be accessed throughPIL.TiffTags. > - Add new tags using the correct IFD structure withPIL.TiffImagePlugin.ImageFileDirectory_v2.
p2tiff = Path('../test/img/tiff/tif-multi-frame-with-tags.tif').resolve()
tiff = TiffImageFile(p2tiff)
Display n frames from the loaded TIFF image file
n = 3
for frame in range(min(n, tiff.n_frames)):
    print(f"--- Frame {frame} {'-'*80}")
    display(tiff.resize((150,150)))
    tiff.seek(frame)
    print(f"This frame has size {tiff.size}")
    print(f"The image was loaded from {tiff.filename}")
Example of pre-defined tags and their desciption:
predefined_tag_sublist = [256, 257, 259, 269, 270, 33432, 34665, 34675, 34853]
print(f"  Tag Nbr | Tag Name")
for tag_nbr in predefined_tag_sublist:
    print(f"  {tag_nbr:5d}   | {TiffTags.TAGS_V2[tag_nbr].name}")
It is possible to retrieve all existing tags to one frame.
.tag_v2.keys(),.tag_v2.values(),.tag_v2.items(),.tag_v2.get(tag_nbr)as in a dictionary.ifdto retrieve the entire Image File Directory. It behaves as a dictionary.TiffTags.TAGS_V2is a fixed dictionary that gives access to the predefined tags and their meanings
List of predefined tags in the loaded TIFF image:
len(tiff.tag_v2.keys())
tiff.seek(0)
tags_predefined = [tag for tag in tiff.tag_v2.keys() if tag in list(TiffTags.TAGS_V2.keys())]
[f"{tag:5d} - {TiffTags.TAGS_V2[tag].name:30s}:  {tiff.tag_v2.get(tag)}" for tag in tags_predefined]
List of custom tags in the loaded TIFF image:
tags_custom = [tag for tag in tiff.tag_v2.keys() if tag not in list(TiffTags.TAGS_V2.keys())]
[f"{str(tag):38s}:  {tiff.tag_v2.get(tag)}" for tag in tags_custom]
Class TiffImage 
Create a new instance of TiffImage and display its default representation.
tiff = TiffImage(p2tiff)
tiff
Explore the TIFF file frames
tiff.show(4)
tiff.show_all()
Access the TiffImage properties:
tiff.n_frames, tiff.size
tiff.tags
tiff.tags[65001]
Retrieve a specific frame as a PIL.Image.Image object
tiff.get_frame(4)
type(tiff.get_frame(0)), isinstance(tiff.get_frame(0), Image.Image)
Review all the tags for one frame
tiff.summary_tags(0)
Extract one or several frames to be saved as 'jpg' or 'tif'
with TemporaryDirectory() as tdir:
    p2dir = Path(tdir)
    print('Files in the temporary directory prior to extraction:')
    print('/n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
    tiff.extract_one_frame(frame_nbr=2, dest=p2dir)
    tiff.extract_one_frame(frame_nbr=3, dest=p2dir, image_format='tif')
    tiff.extract_one_frame(frame_nbr=4, dest=p2dir, fname='specific-name')
    
    print('Files in the temporary directory after extraction:')
    print('\n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
with TemporaryDirectory() as tdir:
    p2dir = Path(tdir)
    print('Files in the temporary directory prior to extraction:')
    print('/n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
    
    tiff.extract_frames(dest=p2dir, naming_method='counter')
    print('Files in the temporary directory after extraction:')
    print('\n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
with TemporaryDirectory() as tdir:
    p2dir = Path(tdir)
    print('Files in the temporary directory prior to extraction:')
    print('/n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
    
    tiff.extract_frames(dest=p2dir, naming_method='tag_value', tag=65001)
    print('Files in the temporary directory after extraction:')
    print('\n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
# For Test Cases (might have duplicate import because it will be in a dedicated file)
from pathlib import Path
from PIL import Image
from tempfile import TemporaryDirectory
from typing import List
import pytest
from test_common.utils_4_tests import DATA_DIR, IMG_DIR
from test_utils import GITHUB_TEST_DATA_URL, check_connection_github
tiff_fname = 'tif-multi-frame-with-tags.tif'
gif_fname = 'animated.gif'
LOCAL_TEST_TIFF = IMG_DIR / "tiff" / tiff_fname
LOCAL_TEST_GIF = IMG_DIR / "tiff" / gif_fname
LOCAL_TEST_SUMMARY_TAGS = DATA_DIR / 'tiff_summary_tags.txt'
LOCAL_TEST_REPR = DATA_DIR / 'tiff_repr.txt'
GITHUB_TEST_IMG_DIR_URL = "https://raw.githubusercontent.com/unpackAI/unpackai/main/test/img"
GITHUB_TEST_TIFF = f"{GITHUB_TEST_IMG_DIR_URL}/tiff/{tiff_fname}"
GITHUB_TEST_GIF = f"{GITHUB_TEST_IMG_DIR_URL}/tiff/{gif_fname}"
def test_exception_on_missing_path():
    with pytest.raises(FileExistsError) as msg:
        TiffImage('blabla.tif')
    expected_msg = f"Cannot find path {'blabla.tif'}"
    assert str(msg.value) == expected_msg
        
def test_exception_on_wrong_suffix():
    with pytest.raises(ValueError) as msg:
        TiffImage(IMG_DIR/'tiff'/gif_fname)
    expected_msg = f"Image file should be .tif or .tiff, not '{'.gif'}'"
    assert str(msg.value) == expected_msg
@pytest.fixture(scope="session")
def test_tiff_image():
    """Fixture to pass the test TIFF image path"""
    return LOCAL_TEST_TIFF
@pytest.fixture(scope="session")
def test_repr():
    return LOCAL_TEST_REPR.read_text()
@pytest.fixture(scope="session")
def test_summary_tag_printout():
    return LOCAL_TEST_SUMMARY_TAGS.read_text()
@pytest.fixture(scope="session")
def local_TiffImage(test_tiff_image):
    """Create instance of TiffImage by loading the test TIFF image"""
    return TiffImage(test_tiff_image)
class Test_TiffImage:
    """Testing class for TiffImage"""
       
    def test_n_frames(self, local_TiffImage):
        """Test property n_frame"""
        expected_value = 8
        assert local_TiffImage.n_frames == expected_value
        
    def test_size(self, local_TiffImage):
        """Test property Size"""
        expected_value = (512, 512)
        assert local_TiffImage.size == expected_value
        
    def test_tags(self, local_TiffImage):
        """Test returned dictionary"""
        expected_value_0 = {256: 512, 257: 512, 259: 1, 262: 1, 270: '{"shape": [512, 512]}', 
                            273: (320,), 277: 1, 278: 512, 279: (32768,), 282: 1.0, 283: 1.0, 296: 1,
                            65001: 'Specular reflection',
                            305: 'tifffile.py'}
        expected_value_1 = {256: 512, 257: 512, 259: 1, 262: 1, 270: '{"shape": [512, 512]}',
                            273: (33408,), 277: 1, 278: 512, 279: (32768,), 282: 1.0, 283: 1.0, 296: 1, 
                            65001: 'Attached gingiva', 
                            305: 'tifffile.py'}
        local_TiffImage.tiff.seek(0)
        assert local_TiffImage.tags == expected_value_0
        local_TiffImage.tiff.seek(1)
        assert local_TiffImage.tags == expected_value_1
    
    def test_repr(self, local_TiffImage, capsys, test_repr):
        """Test the __repr__"""
        expected_value = test_repr
        local_TiffImage.tiff.seek(0)
        print(local_TiffImage)
        captured = capsys.readouterr()
        assert captured.out == expected_value
        
    def test_summary_tags_printout(self, local_TiffImage, capsys, test_summary_tag_printout):
        """Test the tag summary method"""        
        expected_value = test_summary_tag_printout
        local_TiffImage.tiff.seek(0)
        local_TiffImage.summary_tags()
        captured = capsys.readouterr()
        assert captured.out == expected_value
        
    def test_type_returned_by_get_frame(self, local_TiffImage):
        """Test that object is 'PIL.TiffImagePlugin.TiffImageFile'"""
        frame = local_TiffImage.get_frame(0)
        assert isinstance(frame, TiffImageFile)
        
    def test_extract_one_frame(self, local_TiffImage):
        """Test that frames are extracted and saved"""
        with TemporaryDirectory() as tdir:
            p2dir = Path(tdir)        
            tiff.extract_one_frame(frame_nbr=2, dest=p2dir)
            assert (p2dir / 'tif-multi-frame-with-tags-0002.jpg').is_file()
            tiff.extract_one_frame(frame_nbr=3, dest=p2dir, image_format='tif')
            assert (p2dir / 'tif-multi-frame-with-tags-0003.tif').is_file()
            tiff.extract_one_frame(frame_nbr=4, dest=p2dir, fname='specific-name')
            assert (p2dir / 'specific-name.jpg').is_file()
    def test_extract_frames_by_tag(self, local_TiffImage):
        """Test that all frames are saved and naming by tag is correct """
        with TemporaryDirectory() as tdir:
            p2dir = Path(tdir)        
            tiff.extract_frames(dest=p2dir, naming_method='tag_value', tag=65001)
            assert (p2dir / 'tif-multi-frame-with-tags-Hair.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-Skin.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-Stain.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-Marginal_gingiva.tif').is_file()
            assert len(list(p2dir.iterdir())) == local_TiffImage.n_frames
    def test_extract_frames_by_nbr(self, local_TiffImage):
        """Test that all frames are saved and naming by number is correct """
        with TemporaryDirectory() as tdir:
            p2dir = Path(tdir)        
            tiff.extract_frames(dest=p2dir)
            assert (p2dir / 'tif-multi-frame-with-tags-0001.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-0002.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-0005.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-0007.tif').is_file()
            assert len(list(p2dir.iterdir())) == local_TiffImage.n_frames
    def test_is_valid_frame_(self, local_TiffImage):
        nframes = local_TiffImage.n_frames
        assert local_TiffImage.is_valid_frame_(0)
        with pytest.raises(ValueError) as msg:
            local_TiffImage.is_valid_frame_(nframes + 1)
        expected_msg = f"'frame_nbr' is {nframes+1} but this TIFF file only has {nframes} frames."
        assert str(msg.value)[:-1] == expected_msg