Skip to content

Commit

Permalink
Add support for reading TIFFs with PlanarConfiguration=2
Browse files Browse the repository at this point in the history
  • Loading branch information
kkopachev committed Dec 31, 2020
1 parent e1e77ff commit f566c8a
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 47 deletions.
Binary file added Tests/images/tiff_strip_planar_16bit_RGB.tiff
Binary file not shown.
Binary file added Tests/images/tiff_strip_planar_16bit_RGBa.tiff
Binary file not shown.
Binary file added Tests/images/tiff_strip_planar_lzw.tiff
Binary file not shown.
Binary file added Tests/images/tiff_tiled_planar_16bit_RGB.tiff
Binary file not shown.
Binary file added Tests/images/tiff_tiled_planar_16bit_RGBa.tiff
Binary file not shown.
Binary file added Tests/images/tiff_tiled_planar_lzw.tiff
Binary file not shown.
46 changes: 46 additions & 0 deletions Tests/test_file_libtiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,52 @@ def test_tiled_ycbcr_jpeg_2x2_sampling(self):
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5)

@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_strip_planar_rgb(self):
# gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_strip_raw.tif tiff_strip_planar_lzw.tiff
infile = "Tests/images/tiff_strip_planar_lzw.tiff"
with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")

@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_tiled_planar_rgb(self):
# gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff
infile = "Tests/images/tiff_tiled_planar_lzw.tiff"
with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")

@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_tiled_planar_16bit_RGB(self):
# gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff
with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png")

@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_strip_planar_16bit_RGB(self):
# gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff
with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png")

@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_tiled_planar_16bit_RGBa(self):
# gdal_translate -co TILED=yes \
# -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \
# tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff
with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")

@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_strip_planar_16bit_RGBa(self):
# gdal_translate -co TILED=no \
# -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \
# tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff
with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")

@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_old_style_jpeg(self):
infile = "Tests/images/old-style-jpeg-compression.tif"
Expand Down
54 changes: 54 additions & 0 deletions Tests/test_lib_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,23 @@ def test_RGB(self):
self.assert_unpack("RGB", "G", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0))
self.assert_unpack("RGB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3))

self.assert_unpack("RGB", "R;16B", 2, (1, 0, 0), (3, 0, 0), (5, 0, 0))
self.assert_unpack("RGB", "G;16B", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
self.assert_unpack("RGB", "B;16B", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))

self.assert_unpack("RGB", "R;16L", 2, (2, 0, 0), (4, 0, 0), (6, 0, 0))
self.assert_unpack("RGB", "G;16L", 2, (0, 2, 0), (0, 4, 0), (0, 6, 0))
self.assert_unpack("RGB", "B;16L", 2, (0, 0, 2), (0, 0, 4), (0, 0, 6))

if sys.byteorder == "little":
self.assert_unpack("RGB", "R;16N", 2, (2, 0, 0), (4, 0, 0), (6, 0, 0))
self.assert_unpack("RGB", "G;16N", 2, (0, 2, 0), (0, 4, 0), (0, 6, 0))
self.assert_unpack("RGB", "B;16N", 2, (0, 0, 2), (0, 0, 4), (0, 0, 6))
else:
self.assert_unpack("RGB", "R;16N", 2, (1, 0, 0), (3, 0, 0), (5, 0, 0))
self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))

def test_RGBA(self):
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
self.assert_unpack(
Expand Down Expand Up @@ -450,6 +467,43 @@ def test_RGBA(self):
self.assert_unpack("RGBA", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0))
self.assert_unpack("RGBA", "A", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3))

self.assert_unpack("RGBA", "R;16B", 2, (1, 0, 0, 0), (3, 0, 0, 0), (5, 0, 0, 0))
self.assert_unpack("RGBA", "G;16B", 2, (0, 1, 0, 0), (0, 3, 0, 0), (0, 5, 0, 0))
self.assert_unpack("RGBA", "B;16B", 2, (0, 0, 1, 0), (0, 0, 3, 0), (0, 0, 5, 0))
self.assert_unpack("RGBA", "A;16B", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5))

self.assert_unpack("RGBA", "R;16L", 2, (2, 0, 0, 0), (4, 0, 0, 0), (6, 0, 0, 0))
self.assert_unpack("RGBA", "G;16L", 2, (0, 2, 0, 0), (0, 4, 0, 0), (0, 6, 0, 0))
self.assert_unpack("RGBA", "B;16L", 2, (0, 0, 2, 0), (0, 0, 4, 0), (0, 0, 6, 0))
self.assert_unpack("RGBA", "A;16L", 2, (0, 0, 0, 2), (0, 0, 0, 4), (0, 0, 0, 6))

if sys.byteorder == "little":
self.assert_unpack(
"RGBA", "R;16N", 2, (2, 0, 0, 0), (4, 0, 0, 0), (6, 0, 0, 0)
)
self.assert_unpack(
"RGBA", "G;16N", 2, (0, 2, 0, 0), (0, 4, 0, 0), (0, 6, 0, 0)
)
self.assert_unpack(
"RGBA", "B;16N", 2, (0, 0, 2, 0), (0, 0, 4, 0), (0, 0, 6, 0)
)
self.assert_unpack(
"RGBA", "A;16N", 2, (0, 0, 0, 2), (0, 0, 0, 4), (0, 0, 0, 6)
)
else:
self.assert_unpack(
"RGBA", "R;16N", 2, (1, 0, 0, 0), (3, 0, 0, 0), (5, 0, 0, 0)
)
self.assert_unpack(
"RGBA", "G;16N", 2, (0, 1, 0, 0), (0, 3, 0, 0), (0, 5, 0, 0)
)
self.assert_unpack(
"RGBA", "B;16N", 2, (0, 0, 1, 0), (0, 0, 3, 0), (0, 0, 5, 0)
)
self.assert_unpack(
"RGBA", "A;16N", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5)
)

def test_RGBa(self):
self.assert_unpack(
"RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)
Expand Down
161 changes: 115 additions & 46 deletions src/libImaging/TiffDecode.c
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ int ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) {
}


int ReadTile(TIFF* tiff, UINT32 col, UINT32 row, UINT32* buffer) {
int ReadTile(TIFF* tiff, UINT32 col, UINT32 row, UINT8 plane, UINT32* buffer) {
uint16 photometric = 0;

TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric);
Expand Down Expand Up @@ -228,7 +228,7 @@ int ReadTile(TIFF* tiff, UINT32 col, UINT32 row, UINT32* buffer) {
return 0;
}

if (TIFFReadTile(tiff, (tdata_t)buffer, col, row, 0, 0) == -1) {
if (TIFFReadTile(tiff, (tdata_t)buffer, col, row, 0, plane) == -1) {
TRACE(("Decode Error, Tile at %dx%d\n", col, row));
return -1;
}
Expand All @@ -238,7 +238,7 @@ int ReadTile(TIFF* tiff, UINT32 col, UINT32 row, UINT32* buffer) {
return 0;
}

int ReadStrip(TIFF* tiff, UINT32 row, UINT32* buffer) {
int ReadStrip(TIFF* tiff, UINT32 row, UINT8 plane, UINT32* buffer) {
uint16 photometric = 0; // init to not PHOTOMETRIC_YCBCR
TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric);

Expand All @@ -259,6 +259,8 @@ int ReadStrip(TIFF* tiff, UINT32 row, UINT32* buffer) {
if (TIFFRGBAImageOK(tiff, emsg) && TIFFRGBAImageBegin(&img, tiff, 0, emsg)) {
TRACE(("Initialized RGBAImage\n"));

// Using TIFFRGBAImageGet instead of TIFFReadRGBAStrip
// just to get strip in TOP_LEFT orientation
img.req_orientation = ORIENTATION_TOPLEFT;
img.row_offset = row;
img.col_offset = 0;
Expand All @@ -281,7 +283,7 @@ int ReadStrip(TIFF* tiff, UINT32 row, UINT32* buffer) {
return 0;
}

if (TIFFReadEncodedStrip(tiff, TIFFComputeStrip(tiff, row, 0), (tdata_t)buffer, -1) == -1) {
if (TIFFReadEncodedStrip(tiff, TIFFComputeStrip(tiff, row, plane), (tdata_t)buffer, -1) == -1) {
TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, row, 0)));
return -1;
}
Expand All @@ -294,6 +296,10 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_
char *filename = "tempfile.tif";
char *mode = "r";
TIFF *tiff;
uint16 photometric = 0; // init to not PHOTOMETRIC_YCBCR
UINT8 planarconfig;
UINT8 planes = 1;
ImagingShuffler unpackers[4];

/* buffer is the encoded file, bytes is the length of the encoded file */
/* it all ends up in state->buffer, which is a uint8* from Imaging.h */
Expand Down Expand Up @@ -350,8 +356,38 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_
rv = TIFFSetSubDirectory(tiff, ifdoffset);
if (!rv){
TRACE(("error in TIFFSetSubDirectory"));
state->errcode = IMAGING_CODEC_BROKEN;
TIFFClose(tiff);
return -1;
}
}

TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric);
TIFFGetFieldDefaulted(tiff, TIFFTAG_PLANARCONFIG, &planarconfig);
// YCbCr data is read as RGB by libtiff and we don't need to worry about planar storage in that case
// if number of bands is 1, there is no difference with contig case
if (planarconfig == PLANARCONFIG_SEPARATE && im->bands > 1 && photometric != PHOTOMETRIC_YCBCR) {
uint16 bps = 8;

TIFFGetFieldDefaulted(tiff, TIFFTAG_BITSPERSAMPLE, &bps);
if (bps != 8 && bps != 16) {
TRACE(("Invalid value for bits per sample: %d\n", bps));
state->errcode = IMAGING_CODEC_BROKEN;
TIFFClose(tiff);
return -1;
}

planes = im->bands;

// We'll pick appropriate set of unpackers depending on planar_configuration
// It does not matter if data is RGB(A), CMYK or LUV really,
// we just copy it plane by plane
unpackers[0] = ImagingFindUnpacker("RGBA", bps == 16 ? "R;16N" : "R", NULL);
unpackers[1] = ImagingFindUnpacker("RGBA", bps == 16 ? "G;16N" : "G", NULL);
unpackers[2] = ImagingFindUnpacker("RGBA", bps == 16 ? "B;16N" : "B", NULL);
unpackers[3] = ImagingFindUnpacker("RGBA", bps == 16 ? "A;16N" : "A", NULL);
} else {
unpackers[0] = state->shuffle;
}

if (TIFFIsTiled(tiff)) {
Expand All @@ -370,19 +406,19 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_
}

// We could use TIFFTileSize, but for YCbCr data it returns subsampled data size
row_byte_size = (tile_width * state->bits + 7) / 8;
row_byte_size = (tile_width * state->bits / planes + 7) / 8;

/* overflow check for realloc */
if (INT_MAX / row_byte_size < tile_length) {
state->errcode = IMAGING_CODEC_MEMORY;
TIFFClose(tiff);
return -1;
}

state->bytes = row_byte_size * tile_length;

if (TIFFTileSize(tiff) > state->bytes) {
// If the strip size as expected by LibTiff isn't what we're expecting, abort.
// If the tile size as expected by LibTiff isn't what we're expecting, abort.
state->errcode = IMAGING_CODEC_MEMORY;
TIFFClose(tiff);
return -1;
Expand All @@ -402,29 +438,33 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_
TRACE(("TIFFTileSize: %d\n", state->bytes));

for (y = state->yoff; y < state->ysize; y += tile_length) {
for (x = state->xoff; x < state->xsize; x += tile_width) {
if (ReadTile(tiff, x, y, (UINT32*) state->buffer) == -1) {
TRACE(("Decode Error, Tile at %dx%d\n", x, y));
state->errcode = IMAGING_CODEC_BROKEN;
TIFFClose(tiff);
return -1;
}

TRACE(("Read tile at %dx%d; \n\n", x, y));

current_tile_width = min((INT32) tile_width, state->xsize - x);

// iterate over each line in the tile and stuff data into image
for (tile_y = 0; tile_y < min((INT32) tile_length, state->ysize - y); tile_y++) {
TRACE(("Writing tile data at %dx%d using tile_width: %d; \n", tile_y + y, x, current_tile_width));

// UINT8 * bbb = state->buffer + tile_y * row_byte_size;
// TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3]));

state->shuffle((UINT8*) im->image[tile_y + y] + x * im->pixelsize,
state->buffer + tile_y * row_byte_size,
current_tile_width
);
UINT8 plane;
for (plane = 0; plane < planes; plane++) {
ImagingShuffler shuffler = unpackers[plane];
for (x = state->xoff; x < state->xsize; x += tile_width) {
if (ReadTile(tiff, x, y, plane, (UINT32*) state->buffer) == -1) {
TRACE(("Decode Error, Tile at %dx%d\n", x, y));
state->errcode = IMAGING_CODEC_BROKEN;
TIFFClose(tiff);
return -1;
}

TRACE(("Read tile at %dx%d; \n\n", x, y));

current_tile_width = min((INT32) tile_width, state->xsize - x);

// iterate over each line in the tile and stuff data into image
for (tile_y = 0; tile_y < min((INT32) tile_length, state->ysize - y); tile_y++) {
TRACE(("Writing tile data at %dx%d using tile_width: %d; \n", tile_y + y, x, current_tile_width));

// UINT8 * bbb = state->buffer + tile_y * row_byte_size;
// TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3]));

shuffler((UINT8*) im->image[tile_y + y] + x * im->pixelsize,
state->buffer + tile_y * row_byte_size,
current_tile_width
);
}
}
}
}
Expand All @@ -441,7 +481,7 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_
TRACE(("RowsPerStrip: %u \n", rows_per_strip));

// We could use TIFFStripSize, but for YCbCr data it returns subsampled data size
row_byte_size = (state->xsize * state->bits + 7) / 8;
row_byte_size = (state->xsize * state->bits / planes + 7) / 8;

/* overflow check for realloc */
if (INT_MAX / row_byte_size < rows_per_strip) {
Expand Down Expand Up @@ -476,26 +516,55 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_
state->buffer = new_data;

for (; state->y < state->ysize; state->y += rows_per_strip) {
if (ReadStrip(tiff, state->y, (UINT32 *)state->buffer) == -1) {
TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)));
state->errcode = IMAGING_CODEC_BROKEN;
TIFFClose(tiff);
return -1;
}
UINT8 plane;
for (plane = 0; plane < planes; plane++) {
ImagingShuffler shuffler = unpackers[plane];
if (ReadStrip(tiff, state->y, plane, (UINT32 *)state->buffer) == -1) {
TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)));
state->errcode = IMAGING_CODEC_BROKEN;
TIFFClose(tiff);
return -1;
}

TRACE(("Decoded strip for row %d \n", state->y));
TRACE(("Decoded strip for row %d \n", state->y));

// iterate over each row in the strip and stuff data into image
for (strip_row = 0; strip_row < min((INT32) rows_per_strip, state->ysize - state->y); strip_row++) {
// iterate over each row in the strip and stuff data into image
for (strip_row = 0; strip_row < min((INT32) rows_per_strip, state->ysize - state->y); strip_row++) {
TRACE(("Writing data into line %d ; \n", state->y + strip_row));

// UINT8 * bbb = state->buffer + strip_row * (state->bytes / rows_per_strip);
// TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3]));
// UINT8 * bbb = state->buffer + strip_row * (state->bytes / rows_per_strip);
// TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3]));

shuffler((UINT8*) im->image[state->y + state->yoff + strip_row] +
state->xoff * im->pixelsize,
state->buffer + strip_row * row_byte_size,
state->xsize);
}
}
}
}


// Check if raw mode was RGBa and it was stored on separate planes
// so we have to convert it to RGBA
if (planes > 3 && strcmp(im->mode, "RGBA") == 0) {
uint16 extrasamples;
uint16* sampleinfo;
ImagingShuffler shuffle;
INT32 y;

TIFFGetFieldDefaulted(tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo);

if (extrasamples >= 1 &&
(sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA)
) {
shuffle = ImagingFindUnpacker("RGBA", "RGBa", NULL);

for (y = state->yoff; y < state->ysize; y++) {
UINT8* ptr = (UINT8*) im->image[y + state->yoff] +
state->xoff * im->pixelsize;

state->shuffle((UINT8*) im->image[state->y + state->yoff + strip_row] +
state->xoff * im->pixelsize,
state->buffer + strip_row * row_byte_size,
state->xsize);
shuffle(ptr, ptr, state->xsize);
}
}
}
Expand Down
Loading

0 comments on commit f566c8a

Please sign in to comment.