Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow uncompressed TIFF images to be saved in chunks #7650

Merged
merged 2 commits into from Jan 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 23 additions & 2 deletions Tests/test_file_tiff.py
Expand Up @@ -484,13 +484,13 @@ def test_modify_exif(self, tmp_path):
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
exif[256] = 100
exif[264] = 100

im.save(outfile, exif=exif)

with Image.open(outfile) as im:
exif = im.getexif()
assert exif[256] == 100
assert exif[264] == 100

def test_reload_exif_after_seek(self):
with Image.open("Tests/images/multipage.tiff") as im:
Expand Down Expand Up @@ -781,6 +781,27 @@ def test_get_photoshop_blocks(self):
4001,
]

def test_tiff_chunks(self, tmp_path):
tmpfile = str(tmp_path / "temp.tif")

im = hopper()
with open(tmpfile, "wb") as fp:
for y in range(0, 128, 32):
chunk = im.crop((0, y, 128, y + 32))
if y == 0:
chunk.save(
fp,
"TIFF",
tiffinfo={
TiffImagePlugin.IMAGEWIDTH: 128,
TiffImagePlugin.IMAGELENGTH: 128,
},
)
else:
fp.write(chunk.tobytes())

assert_image_equal_tofile(im, tmpfile)

def test_close_on_load_exclusive(self, tmp_path):
# similar to test_fd_leak, but runs on unixlike os
tmpfile = str(tmp_path / "temp.tif")
Expand Down
1 change: 1 addition & 0 deletions Tests/test_file_tiff_metadata.py
Expand Up @@ -164,6 +164,7 @@ def test_change_stripbytecounts_tag_type(tmp_path):

# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
im = im.resize((500, 500))
info[TiffImagePlugin.IMAGEWIDTH] = im.width

# STRIPBYTECOUNTS can be a SHORT or a LONG
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
Expand Down
15 changes: 7 additions & 8 deletions src/PIL/TiffImagePlugin.py
Expand Up @@ -1704,28 +1704,27 @@ def _save(im, fp, filename):
colormap += [0] * (256 - colors)
ifd[COLORMAP] = colormap
# data orientation
stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
stride = len(bits) * ((w * bits[0] + 7) // 8)
if ROWSPERSTRIP not in ifd:
# aim for given strip size (64 KB by default) when using libtiff writer
if libtiff:
im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
rows_per_strip = (
1 if stride == 0 else min(im_strip_size // stride, im.size[1])
)
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
# JPEG encoder expects multiple of 8 rows
if compression == "jpeg":
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
else:
rows_per_strip = im.size[1]
rows_per_strip = h
if rows_per_strip == 0:
rows_per_strip = 1
ifd[ROWSPERSTRIP] = rows_per_strip
strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP]
strips_per_image = (im.size[1] + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
if strip_byte_counts >= 2**16:
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),
stride * h - strip_byte_counts * (strips_per_image - 1),
)
ifd[STRIPOFFSETS] = tuple(
range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
Expand Down