diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bbcec6a..492d47eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. ## [0.12.0 - 2023-0x-xx] +## Added + +- (Heif) restored lost ability after `0.9.x` versions to open HDR images in 10/12 bit. #96 + ## Fixed - (Heif) `encode` function with `stride` argument correctly saves image. diff --git a/MANIFEST.in b/MANIFEST.in index b0fcc75b..6f1452c9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,7 @@ include README.md include CHANGELOG.md include pyproject.toml -recursive-include pillow_heif *.c +recursive-include pillow_heif *.c *.h graft libheif graft tests diff --git a/pillow_heif/_ph_postprocess.h b/pillow_heif/_ph_postprocess.h new file mode 100644 index 00000000..9be261fc --- /dev/null +++ b/pillow_heif/_ph_postprocess.h @@ -0,0 +1,413 @@ +/* =========== Decode postprocess stuff ======== */ + +void postprocess__bgr__byte(int width, int height, uint8_t* data, int stride, int channels) { + uint8_t tmp; + if (channels == 3) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data[i2 * 3 + 0]; + data[i2 * 3 + 0] = data[i2 * 3 + 2]; + data[i2 * 3 + 2] = tmp; + } + data += stride; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data[i2 * 4 + 0]; + data[i2 * 4 + 0] = data[i2 * 4 + 2]; + data[i2 * 4 + 2] = tmp; + } + data += stride; + } + } +} + +void postprocess__bgr__word(int width, int height, uint16_t* data, int stride, int channels, int shift_size) { + uint16_t tmp; + if (channels == 3) { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data[i2 * 3 + 0]; + data[i2 * 3 + 0] = data[i2 * 3 + 2] << 4; + data[i2 * 3 + 1] = data[i2 * 3 + 1] << 4; + data[i2 * 3 + 2] = tmp << 4; + } + data += stride / 2; + } + } + else if (shift_size == 6) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data[i2 * 3 + 0]; + data[i2 * 3 + 0] = data[i2 * 3 + 2] << 6; + data[i2 * 3 + 1] = data[i2 * 3 + 1] << 6; + data[i2 * 3 + 2] = tmp << 6; + } + data += stride / 2; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data[i2 * 3 + 0]; + data[i2 * 3 + 0] = data[i2 * 3 + 2]; + data[i2 * 3 + 2] = tmp; + } + data += stride / 2; + } + } + } + else { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data[i2 * 4 + 0]; + data[i2 * 4 + 0] = data[i2 * 4 + 2] << 4; + data[i2 * 4 + 1] = data[i2 * 4 + 1] << 4; + data[i2 * 4 + 2] = tmp << 4; + data[i2 * 4 + 3] = data[i2 * 4 + 3] << 4; + } + data += stride / 2; + } + } + else if (shift_size == 6) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data[i2 * 4 + 0]; + data[i2 * 4 + 0] = data[i2 * 4 + 2] << 6; + data[i2 * 4 + 1] = data[i2 * 4 + 1] << 6; + data[i2 * 4 + 2] = tmp << 6; + data[i2 * 4 + 3] = data[i2 * 4 + 3] << 6; + } + data += stride / 2; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data[i2 * 4 + 0]; + data[i2 * 4 + 0] = data[i2 * 4 + 2]; + data[i2 * 4 + 2] = tmp; + } + data += stride / 2; + } + } + } +} + +void postprocess__bgr_stride__byte(int width, int height, uint8_t* data, int stride_in, int stride_out, int channels) { + uint8_t *data_in = data, *data_out = data, tmp; + if (channels == 3) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data_in[i2 * 3 + 0]; + data_out[i2 * 3 + 0] = data_in[i2 * 3 + 2]; + data_out[i2 * 3 + 1] = data_in[i2 * 3 + 1]; + data_out[i2 * 3 + 2] = tmp; + } + data_in += stride_in; + data_out += stride_out; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data_in[i2 * 4 + 0]; + data_out[i2 * 4 + 0] = data_in[i2 * 4 + 2]; + data_out[i2 * 4 + 1] = data_in[i2 * 4 + 1]; + data_out[i2 * 4 + 2] = tmp; + data_out[i2 * 4 + 3] = data_in[i2 * 4 + 3]; + } + data_in += stride_in; + data_out += stride_out; + } + } +} + +void postprocess__bgr_stride__word(int width, int height, uint16_t* data, int stride_in, int stride_out, + int channels, int shift_size) { + uint16_t *data_in = data, *data_out = data, tmp; + if (channels == 3) { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data_in[i2 * 3 + 0]; + data_out[i2 * 3 + 0] = data_in[i2 * 3 + 2] << 4; + data_out[i2 * 3 + 1] = data_in[i2 * 3 + 1] << 4; + data_out[i2 * 3 + 2] = tmp << 4; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + else if (shift_size == 6) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data_in[i2 * 3 + 0]; + data_out[i2 * 3 + 0] = data_in[i2 * 3 + 2] << 6; + data_out[i2 * 3 + 1] = data_in[i2 * 3 + 1] << 6; + data_out[i2 * 3 + 2] = tmp << 6; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data_in[i2 * 3 + 0]; + data_out[i2 * 3 + 0] = data_in[i2 * 3 + 2]; + data_out[i2 * 3 + 1] = data_in[i2 * 3 + 1]; + data_out[i2 * 3 + 2] = tmp; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + } + else { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data_in[i2 * 4 + 0]; + data_out[i2 * 4 + 0] = data_in[i2 * 4 + 2] << 4; + data_out[i2 * 4 + 1] = data_in[i2 * 4 + 1] << 4; + data_out[i2 * 4 + 2] = tmp << 4; + data_out[i2 * 4 + 3] = data_in[i2 * 4 + 3] << 4; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + else if (shift_size == 6) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data_in[i2 * 4 + 0]; + data_out[i2 * 4 + 0] = data_in[i2 * 4 + 2] << 6; + data_out[i2 * 4 + 1] = data_in[i2 * 4 + 1] << 6; + data_out[i2 * 4 + 2] = tmp << 6; + data_out[i2 * 4 + 3] = data_in[i2 * 4 + 3] << 6; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + tmp = data_in[i2 * 4 + 0]; + data_out[i2 * 4 + 0] = data_in[i2 * 4 + 2]; + data_out[i2 * 4 + 1] = data_in[i2 * 4 + 1]; + data_out[i2 * 4 + 2] = tmp; + data_out[i2 * 4 + 3] = data_in[i2 * 4 + 3]; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + } +} + +void postprocess__word(int width, int height, uint16_t* data, int stride_elements, int channels, int shift_size) { + if (channels == 1) { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data[i2 * 1 + 0] = data[i2 * 1 + 0] << 4; + } + data += stride_elements; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data[i2 * 1 + 0] = data[i2 * 1 + 0] << 6; + } + data += stride_elements; + } + } + } + else if (channels == 3) { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data[i2 * 3 + 0] = data[i2 * 3 + 0] << 4; + data[i2 * 3 + 1] = data[i2 * 3 + 1] << 4; + data[i2 * 3 + 2] = data[i2 * 3 + 2] << 4; + } + data += stride_elements; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data[i2 * 3 + 0] = data[i2 * 3 + 0] << 6; + data[i2 * 3 + 1] = data[i2 * 3 + 1] << 6; + data[i2 * 3 + 2] = data[i2 * 3 + 2] << 6; + } + data += stride_elements; + } + } + } + else { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data[i2 * 4 + 0] = data[i2 * 4 + 0] << 4; + data[i2 * 4 + 1] = data[i2 * 4 + 1] << 4; + data[i2 * 4 + 2] = data[i2 * 4 + 2] << 4; + data[i2 * 4 + 3] = data[i2 * 4 + 3] << 4; + } + data += stride_elements; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data[i2 * 4 + 0] = data[i2 * 4 + 0] << 6; + data[i2 * 4 + 1] = data[i2 * 4 + 1] << 6; + data[i2 * 4 + 2] = data[i2 * 4 + 2] << 6; + data[i2 * 4 + 3] = data[i2 * 4 + 3] << 6; + } + data += stride_elements; + } + } + } +} + +void postprocess__stride__byte(int width, int height, uint8_t* data, int stride_in, int stride_out) { + uint8_t *data_in = data, *data_out = data; + for (int i = 0; i < height; i++) { + memmove(data_out, data_in, stride_out); // possible will change to memcpy and set -D_FORTIFY_SOURCE=0 + data_in += stride_in; + data_out += stride_out; + } +} + +void postprocess__stride__word(int width, int height, uint16_t* data, int stride_in, int stride_out, + int channels, int shift_size) { + uint16_t *data_in = data, *data_out = data; + if (shift_size == 0) { + for (int i = 0; i < height; i++) { + memmove(data_out, data_in, stride_out); // possible will change to memcpy and set -D_FORTIFY_SOURCE=0 + data_in += stride_in / 2; + data_out += stride_out / 2; + } + return; + } + + if (channels == 1) { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data_out[i2 * 1 + 0] = data_in[i2 * 1 + 0] << 4; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data_out[i2 * 1 + 0] = data_in[i2 * 1 + 0] << 6; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + } + else if (channels == 3) { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data_out[i2 * 3 + 0] = data_in[i2 * 3 + 0] << 4; + data_out[i2 * 3 + 1] = data_in[i2 * 3 + 1] << 4; + data_out[i2 * 3 + 2] = data_in[i2 * 3 + 2] << 4; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data_out[i2 * 3 + 0] = data_in[i2 * 3 + 0] << 6; + data_out[i2 * 3 + 1] = data_in[i2 * 3 + 1] << 6; + data_out[i2 * 3 + 2] = data_in[i2 * 3 + 2] << 6; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + } + else { + if (shift_size == 4) { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data_out[i2 * 4 + 0] = data_in[i2 * 4 + 0] << 4; + data_out[i2 * 4 + 1] = data_in[i2 * 4 + 1] << 4; + data_out[i2 * 4 + 2] = data_in[i2 * 4 + 2] << 4; + data_out[i2 * 4 + 3] = data_in[i2 * 4 + 3] << 4; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + else { + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + data_out[i2 * 4 + 0] = data_in[i2 * 4 + 0] << 6; + data_out[i2 * 4 + 1] = data_in[i2 * 4 + 1] << 6; + data_out[i2 * 4 + 2] = data_in[i2 * 4 + 2] << 6; + data_out[i2 * 4 + 3] = data_in[i2 * 4 + 3] << 6; + } + data_in += stride_in / 2; + data_out += stride_out / 2; + } + } + } +} + +// Top Level Postprocess Functions + +void postprocess__bgr(int width, int height, void* data, int stride, + int bytes_in_cc, int channels, int shift_size) { + Py_BEGIN_ALLOW_THREADS + if (bytes_in_cc == 1) + postprocess__bgr__byte(width, height, (uint8_t*)data, stride, channels); + else + postprocess__bgr__word(width, height, (uint16_t*)data, stride, channels, shift_size); + Py_END_ALLOW_THREADS +} + +void postprocess__bgr_stride(int width, int height, void* data, int stride_in, int stride_out, + int bytes_in_cc, int channels, int shift_size) { + Py_BEGIN_ALLOW_THREADS + if (bytes_in_cc == 1) + postprocess__bgr_stride__byte(width, height, (uint8_t*)data, stride_in, stride_out, channels); + else + postprocess__bgr_stride__word(width, height, (uint16_t*)data, stride_in, stride_out, channels, shift_size); + Py_END_ALLOW_THREADS +} + +void postprocess(int width, int height, void* data, int stride, + int bytes_in_cc, int channels, int shift_size) { + if ((bytes_in_cc == 1) || (shift_size == 0)) + return; + Py_BEGIN_ALLOW_THREADS + postprocess__word(width, height, (uint16_t*)data, stride / 2, channels, shift_size); + Py_END_ALLOW_THREADS +} + +void postprocess__stride(int width, int height, void* data, int stride_in, int stride_out, + int bytes_in_cc, int channels, int shift_size) { + Py_BEGIN_ALLOW_THREADS + if (bytes_in_cc == 1) + postprocess__stride__byte(width, height, (uint8_t*)data, stride_in, stride_out); + else + postprocess__stride__word(width, height, (uint16_t*)data, stride_in, stride_out, channels, shift_size); + Py_END_ALLOW_THREADS +} diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index 78349d40..4ddf24b0 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -2,6 +2,7 @@ #include "Python.h" #include "libheif/heif.h" +#include "_ph_postprocess.h" /* =========== Common stuff ======== */ @@ -68,18 +69,19 @@ typedef struct { PyObject_HEAD int width; // size[0]; int height; // size[1]; - int bits; // on of: 8, 10, 12. - int alpha; // on of: 0, 1. - char mode[8]; // one of: RGB, RGBA, RGBa, RGB;16, RGBA;16, RGBa;16 - int primary; // on of: 0, 1. + int bits; // one of: 8, 10, 12. + int alpha; // one of: 0, 1. + char mode[8]; // one of: RGB, RGBA, RGBa, BGR, BGRA, BGRa + Optional[;10/12/16] + int primary; // one of: 0, 1. int hdr_to_8bit; // private. decode option. int bgr_mode; // private. decode option. - int postprocess; // private. decode option. + int remove_stride; // private. decode option. + int hdr_to_16bit; // private. decode option. int reload_size; // private. decode option. struct heif_image_handle *handle; // private struct heif_image *heif_image; // private uint8_t *data; // pointer to data after decoding - int stride; // time when it get filled depends on `postprocess` value + int stride; // time when it get filled depends on `remove_stride` value PyObject *file_bytes; // private } CtxImageObject; @@ -212,6 +214,27 @@ static PyObject* _CtxWriteImage_add_plane(CtxWriteImageObject* self, PyObject* a in += stride_in; out += stride_out; } + else if ((depth_in == depth) && (!with_alpha)) + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + out_word[i2 * 3 + 0] = in_word[i2 * 3 + 2]; + out_word[i2 * 3 + 1] = in_word[i2 * 3 + 1]; + out_word[i2 * 3 + 2] = in_word[i2 * 3 + 0]; + } + in_word += stride_in / 2; + out_word += stride_out / 2; + } + else if ((depth_in == depth) && (with_alpha)) + for (int i = 0; i < height; i++) { + for (int i2 = 0; i2 < width; i2++) { + out_word[i2 * 4 + 0] = in_word[i2 * 4 + 2]; + out_word[i2 * 4 + 1] = in_word[i2 * 4 + 1]; + out_word[i2 * 4 + 2] = in_word[i2 * 4 + 0]; + out_word[i2 * 4 + 3] = in_word[i2 * 4 + 3]; + } + in_word += stride_in / 2; + out_word += stride_out / 2; + } else if ((depth_in == 16) && (depth == 10) && (!with_alpha)) for (int i = 0; i < height; i++) { for (int i2 = 0; i2 < width; i2++) { @@ -680,7 +703,8 @@ static void _CtxImage_destructor(CtxImageObject* self) { PyObject_Del(self); } -PyObject* _CtxImage(struct heif_image_handle* handle, int hdr_to_8bit, int bgr_mode, int postprocess, +PyObject* _CtxImage(struct heif_image_handle* handle, int hdr_to_8bit, + int bgr_mode, int remove_stride, int hdr_to_16bit, int reload_size, int primary, PyObject* file_bytes) { CtxImageObject *ctx_image = PyObject_New(CtxImageObject, &CtxImage_Type); if (!ctx_image) { @@ -694,14 +718,24 @@ PyObject* _CtxImage(struct heif_image_handle* handle, int hdr_to_8bit, int bgr_m if (ctx_image->alpha) strcat(ctx_image->mode, heif_image_handle_is_premultiplied_alpha(handle) ? "a" : "A"); ctx_image->bits = heif_image_handle_get_luma_bits_per_pixel(handle); - if ((ctx_image->bits > 8) && (!hdr_to_8bit)) - strcat(ctx_image->mode, ";16"); + if ((ctx_image->bits > 8) && (!hdr_to_8bit)) { + if (hdr_to_16bit) { + strcat(ctx_image->mode, ";16"); + } + else if (ctx_image->bits == 10) { + strcat(ctx_image->mode, ";10"); + } + else { + strcat(ctx_image->mode, ";12"); + } + } ctx_image->hdr_to_8bit = hdr_to_8bit; ctx_image->bgr_mode = bgr_mode; ctx_image->handle = handle; ctx_image->heif_image = NULL; ctx_image->data = NULL; - ctx_image->postprocess = postprocess; + ctx_image->remove_stride = remove_stride; + ctx_image->hdr_to_16bit = hdr_to_16bit; ctx_image->reload_size = reload_size; ctx_image->primary = primary; ctx_image->file_bytes = file_bytes; @@ -862,15 +896,33 @@ static PyObject* _CtxImage_thumbnails(CtxImageObject* self, void* closure) { int decode_image(CtxImageObject* self) { struct heif_error error; + int chroma, channels, bytes_in_cc; Py_BEGIN_ALLOW_THREADS struct heif_decoding_options *decode_options = heif_decoding_options_alloc(); decode_options->convert_hdr_to_8bit = self->hdr_to_8bit; - int chroma; - if ((self->bits == 8) || (self->hdr_to_8bit)) - chroma = self->alpha ? heif_chroma_interleaved_RGBA : heif_chroma_interleaved_RGB; - else - chroma = self->alpha ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBB_LE; + if ((self->bits == 8) || (self->hdr_to_8bit)) { + bytes_in_cc = 1; + if (self->alpha) { + chroma = heif_chroma_interleaved_RGBA; + channels = 4; + } + else { + chroma = heif_chroma_interleaved_RGB; + channels = 3; + } + } + else { + bytes_in_cc = 2; + if (self->alpha) { + chroma = heif_chroma_interleaved_RRGGBBAA_LE; + channels = 4; + } + else { + chroma = heif_chroma_interleaved_RRGGBB_LE; + channels = 3; + } + } error = heif_decode_image(self->handle, &self->heif_image, heif_colorspace_RGB, chroma, decode_options); heif_decoding_options_free(decode_options); Py_END_ALLOW_THREADS @@ -897,157 +949,31 @@ int decode_image(CtxImageObject* self) { self->heif_image = NULL; PyErr_Format(PyExc_ValueError, "corrupted image(dimensions in header: (%d, %d), decoded dimensions: (%d, %d)). " - "Set ALLOW_INCORRECT_HEADERS to True if you need to load it.", + "Set ALLOW_INCORRECT_HEADERS to True if you need to load them.", self->width, self->height, decoded_width, decoded_height); return 0; } - if (!self->postprocess) { - self->stride = stride; - return 1; - } - self->stride = get_stride(self); - - if ((self->bgr_mode) || (self->stride != stride) || ((self->bits > 8) && (!self->hdr_to_8bit))) { - int invalid_mode = 0; - Py_BEGIN_ALLOW_THREADS - if ((self->hdr_to_8bit) || (self->bits == 8)) { - uint8_t *in = (uint8_t*)self->data; - uint8_t *out = (uint8_t*)self->data; - if (!self->bgr_mode) // just remove stride - for (int i = 0; i < self->height; i++) { - memmove(out, in, self->stride); // possible will change to memcpy and set -D_FORTIFY_SOURCE=0 - in += stride; - out += self->stride; - } - else { // remove stride && convert to BGR(A) - uint8_t tmp; - if (!self->alpha) - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - tmp = in[i2 * 3 + 0]; - out[i2 * 3 + 0] = in[i2 * 3 + 2]; - out[i2 * 3 + 1] = in[i2 * 3 + 1]; - out[i2 * 3 + 2] = tmp; - } - in += stride; - out += self->stride; - } - else - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - tmp = in[i2 * 4 + 0]; - out[i2 * 4 + 0] = in[i2 * 4 + 2]; - out[i2 * 4 + 1] = in[i2 * 4 + 1]; - out[i2 * 4 + 2] = tmp; - out[i2 * 4 + 3] = in[i2 * 4 + 3]; - } - in += stride; - out += self->stride; - } - } - } - else { - uint16_t *in = (uint16_t*)self->data; - uint16_t *out = (uint16_t*)self->data; - uint16_t tmp; - if ((self->bits == 10) && (self->alpha) && (!self->bgr_mode)) - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - out[i2 * 4 + 0] = in[i2 * 4 + 0] << 6; - out[i2 * 4 + 1] = in[i2 * 4 + 1] << 6; - out[i2 * 4 + 2] = in[i2 * 4 + 2] << 6; - out[i2 * 4 + 3] = in[i2 * 4 + 3] << 6; - } - in += stride / 2; - out += self->stride / 2; - } - else if ((self->bits == 10) && (self->alpha) && (self->bgr_mode)) - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - tmp = in[i2 * 4 + 0]; - out[i2 * 4 + 0] = in[i2 * 4 + 2] << 6; - out[i2 * 4 + 1] = in[i2 * 4 + 1] << 6; - out[i2 * 4 + 2] = tmp << 6; - out[i2 * 4 + 3] = in[i2 * 4 + 3] << 6; - } - in += stride / 2; - out += self->stride / 2; - } - else if ((self->bits == 10) && (!self->alpha) && (!self->bgr_mode)) - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - out[i2 * 3 + 0] = in[i2 * 3 + 0] << 6; - out[i2 * 3 + 1] = in[i2 * 3 + 1] << 6; - out[i2 * 3 + 2] = in[i2 * 3 + 2] << 6; - } - in += stride / 2; - out += self->stride / 2; - } - else if ((self->bits == 10) && (!self->alpha) && (self->bgr_mode)) - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - tmp = in[i2 * 3 + 0]; - out[i2 * 3 + 0] = in[i2 * 3 + 2] << 6; - out[i2 * 3 + 1] = in[i2 * 3 + 1] << 6; - out[i2 * 3 + 2] = tmp << 6; - } - in += stride / 2; - out += self->stride / 2; - } - else if ((self->bits == 12) && (self->alpha) && (!self->bgr_mode)) - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - out[i2 * 4 + 0] = in[i2 * 4 + 0] << 4; - out[i2 * 4 + 1] = in[i2 * 4 + 1] << 4; - out[i2 * 4 + 2] = in[i2 * 4 + 2] << 4; - out[i2 * 4 + 3] = in[i2 * 4 + 3] << 4; - } - in += stride / 2; - out += self->stride / 2; - } - else if ((self->bits == 12) && (self->alpha) && (self->bgr_mode)) { - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - tmp = in[i2 * 4 + 0]; - out[i2 * 4 + 0] = in[i2 * 4 + 2] << 4; - out[i2 * 4 + 1] = in[i2 * 4 + 1] << 4; - out[i2 * 4 + 2] = tmp << 4; - out[i2 * 4 + 3] = in[i2 * 4 + 3] << 4; - } - in += stride / 2; - out += self->stride / 2; - } - } - else if ((self->bits == 12) && (!self->alpha) && (!self->bgr_mode)) - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - out[i2 * 3 + 0] = in[i2 * 3 + 0] << 4; - out[i2 * 3 + 1] = in[i2 * 3 + 1] << 4; - out[i2 * 3 + 2] = in[i2 * 3 + 2] << 4; - } - in += stride / 2; - out += self->stride / 2; - } - else if ((self->bits == 12) && (!self->alpha) && (self->bgr_mode)) - for (int i = 0; i < self->height; i++) { - for (int i2 = 0; i2 < self->width; i2++) { - tmp = in[i2 * 3 + 0]; - out[i2 * 3 + 0] = in[i2 * 3 + 2] << 4; - out[i2 * 3 + 1] = in[i2 * 3 + 1] << 4; - out[i2 * 3 + 2] = tmp << 4; - } - in += stride / 2; - out += self->stride / 2; - } - else - invalid_mode = 1; - } - Py_END_ALLOW_THREADS - if (invalid_mode) { - PyErr_SetString(PyExc_ValueError, "invalid plane mode value"); - return 0; - } + self->stride = self->remove_stride ? get_stride(self) : stride; + + int remove_stride = ((self->remove_stride) && (self->stride != stride)); + int shift_size = ((self->hdr_to_16bit) && (self->bits > 8) && (!self->hdr_to_8bit)) ? 16 - self->bits : 0; + + if ((self->bgr_mode) && (!remove_stride)) + postprocess__bgr(self->width, self->height, self->data, stride, + bytes_in_cc, channels, shift_size); + else if ((self->bgr_mode) && (remove_stride)) + postprocess__bgr_stride(self->width, self->height, self->data, stride, self->stride, + bytes_in_cc, channels, shift_size); + else if ((!self->bgr_mode) && (!remove_stride)) + postprocess(self->width, self->height, self->data, stride, + bytes_in_cc, channels, shift_size); + else if ((!self->bgr_mode) && (remove_stride)) + postprocess__stride(self->width, self->height, self->data, stride, self->stride, + bytes_in_cc, channels, shift_size); + else { + PyErr_SetString(PyExc_ValueError, "internal error, invalid postprocess condition"); + return 0; } return 1; } @@ -1121,16 +1047,17 @@ static PyObject* _CtxWrite(PyObject* self, PyObject* args) { } static PyObject* _load_file(PyObject* self, PyObject* args) { - int hdr_to_8bit, threads_count, bgr_mode, postprocess, reload_size; + int hdr_to_8bit, threads_count, bgr_mode, remove_stride, hdr_to_16bit, reload_size; PyObject *heif_bytes; if (!PyArg_ParseTuple(args, - "Oiiiii", + "Oiiiiii", &heif_bytes, &threads_count, &hdr_to_8bit, &bgr_mode, - &postprocess, + &remove_stride, + &hdr_to_16bit, &reload_size)) return NULL; @@ -1180,7 +1107,8 @@ static PyObject* _load_file(PyObject* self, PyObject* args) { if (error.code == heif_error_Ok) PyList_SET_ITEM(images_list, i, - _CtxImage(handle, hdr_to_8bit, bgr_mode, postprocess, reload_size, primary, heif_bytes)); + _CtxImage(handle, hdr_to_8bit, + bgr_mode, remove_stride, hdr_to_16bit, reload_size, primary, heif_bytes)); else { Py_INCREF(Py_None); PyList_SET_ITEM(images_list, i, Py_None); diff --git a/pillow_heif/as_plugin.py b/pillow_heif/as_plugin.py index 87586f75..2b525249 100644 --- a/pillow_heif/as_plugin.py +++ b/pillow_heif/as_plugin.py @@ -37,7 +37,9 @@ def __init__(self, *args, **kwargs): def _open(self): try: - _heif_file = HeifFile(self.fp, convert_hdr_to_8bit=True, postprocess=False) + # when Pillow starts supporting 16-bit images: + # set `convert_hdr_to_8bit` to False and `convert_hdr_to_8bit` to True + _heif_file = HeifFile(self.fp, convert_hdr_to_8bit=True, remove_stride=False) except (OSError, ValueError, SyntaxError, RuntimeError, EOFError) as exception: raise SyntaxError(str(exception)) from None self.custom_mimetype = _heif_file.mimetype diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 304135dd..ae16a7d7 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -173,9 +173,8 @@ class HeifFile: `ValueError`, `EOFError`, `SyntaxError`, `RuntimeError`, `OSError`""" def __init__(self, fp=None, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs): - postprocess: bool = kwargs.get("postprocess", True) - if bgr_mode and not postprocess: - raise ValueError("BGR mode does not work when post-processing is disabled.") + remove_stride: bool = kwargs.get("remove_stride", True) + hdr_to_16bit: bool = kwargs.get("hdr_to_16bit", True) if hasattr(fp, "seek"): fp.seek(0, SEEK_SET) @@ -187,7 +186,13 @@ def __init__(self, fp=None, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs): mimetype = get_file_mimetype(fp_bytes) reload_size: bool = kwargs.get("reload_size", options.ALLOW_INCORRECT_HEADERS) images = load_file( - fp_bytes, options.DECODE_THREADS, convert_hdr_to_8bit, bgr_mode, postprocess, reload_size + fp_bytes, + options.DECODE_THREADS, + convert_hdr_to_8bit, + bgr_mode, + remove_stride, + hdr_to_16bit, + reload_size, ) self.mimetype = mimetype self._images: List[HeifImage] = [HeifImage(i) for i in images if i is not None] diff --git a/tests/modes_test.py b/tests/modes_test.py new file mode 100644 index 00000000..a81a1001 --- /dev/null +++ b/tests/modes_test.py @@ -0,0 +1,145 @@ +import os +from io import BytesIO + +import pytest +from helpers import compare_hashes, compare_heif_files_fields, hevc_enc +from PIL import Image + +from pillow_heif import HeifImagePlugin # noqa +from pillow_heif import from_bytes, open_heif + +np = pytest.importorskip("numpy", reason="NumPy not installed") + +os.chdir(os.path.dirname(os.path.abspath(__file__))) + + +@pytest.mark.parametrize( + "img,expected_mode", + ( + ("images/heif/RGB_8__29x100.heif", "BGR"), + ("images/heif/RGB_10__29x100.heif", "BGR;16"), + ("images/heif/RGB_12__29x100.heif", "BGR;16"), + ("images/heif/RGBA_8__29x100.heif", "BGRA"), + ("images/heif/RGBA_10__29x100.heif", "BGRA;16"), + ("images/heif/RGBA_12__29x100.heif", "BGRA;16"), + ), +) +def test_open_heif(img, expected_mode): + im = open_heif(img, convert_hdr_to_8bit=False, bgr_mode=True) + assert im.mode == expected_mode + + +@pytest.mark.parametrize( + "img,expected_mode", + ( + ("images/heif/RGB_8__29x100.heif", "BGR"), + ("images/heif/RGB_10__29x100.heif", "BGR;10"), + ("images/heif/RGB_12__29x100.heif", "BGR;12"), + ("images/heif/RGBA_8__29x100.heif", "BGRA"), + ("images/heif/RGBA_10__29x100.heif", "BGRA;10"), + ("images/heif/RGBA_12__29x100.heif", "BGRA;12"), + ), +) +def test_open_heif_bgr_mode_disable_16bit(img, expected_mode): + im = open_heif(img, convert_hdr_to_8bit=False, bgr_mode=True, hdr_to_16bit=False) + assert im.mode == expected_mode + + +@pytest.mark.parametrize( + "img,expected_mode", + ( + ("images/heif/RGB_8__29x100.heif", "RGB"), + ("images/heif/RGB_10__29x100.heif", "RGB;10"), + ("images/heif/RGB_12__29x100.heif", "RGB;12"), + ("images/heif/RGBA_8__29x100.heif", "RGBA"), + ("images/heif/RGBA_10__29x100.heif", "RGBA;10"), + ("images/heif/RGBA_12__29x100.heif", "RGBA;12"), + ), +) +def test_open_heif_disable_16bit(img, expected_mode): + im = open_heif(img, convert_hdr_to_8bit=False, hdr_to_16bit=False) + assert im.mode == expected_mode + + +@pytest.mark.skipif(not hevc_enc(), reason="Requires HEVC encoder.") +@pytest.mark.parametrize( + "img", + ( + "images/heif/RGB_8__29x100.heif", + "images/heif/RGB_8__128x128.heif", + "images/heif/RGB_10__29x100.heif", + "images/heif/RGB_10__128x128.heif", + "images/heif/RGB_12__29x100.heif", + "images/heif/RGB_12__128x128.heif", + "images/heif/RGBA_8__29x100.heif", + "images/heif/RGBA_8__128x128.heif", + "images/heif/RGBA_10__29x100.heif", + "images/heif/RGBA_10__128x128.heif", + "images/heif/RGBA_12__29x100.heif", + "images/heif/RGBA_12__128x128.heif", + ), +) +def test_open_heif_compare_non_standard_modes_data(img): + im_info = open_heif(img) + im_pillow = Image.open(img) + # ======= + rgb = open_heif(img, convert_hdr_to_8bit=False) + rgb_stride = open_heif(img, convert_hdr_to_8bit=False, remove_stride=False) + assert rgb.mode == rgb_stride.mode + np__rgb = np.asarray(rgb) + np__rgb_stride = np.asarray(rgb_stride)[:, : im_info.size[0], :] + np.testing.assert_array_equal(np__rgb, np__rgb_stride) + # ======= + rgb_no16 = open_heif(img, convert_hdr_to_8bit=False, hdr_to_16bit=False) + rgb_no16_stride = open_heif(img, convert_hdr_to_8bit=False, hdr_to_16bit=False, remove_stride=False) + assert rgb_no16.mode == rgb_no16_stride.mode + np__rgb_no16 = np.asarray(rgb_no16) + np__rgb_no16_stride = np.asarray(rgb_no16_stride)[:, : im_info.size[0], :] + np.testing.assert_array_equal(np__rgb_no16, np__rgb_no16_stride) + # ======= + bgr = open_heif(img, convert_hdr_to_8bit=False, bgr_mode=True) + bgr_stride = open_heif(img, convert_hdr_to_8bit=False, bgr_mode=True, remove_stride=False) + assert bgr.mode == bgr_stride.mode + np__bgr = np.asarray(bgr) + np__bgr_stride = np.asarray(bgr_stride)[:, : im_info.size[0], :] + np.testing.assert_array_equal(np__bgr, np__bgr_stride) + # ======= + bgr_no16 = open_heif(img, convert_hdr_to_8bit=False, bgr_mode=True, hdr_to_16bit=False) + bgr_no16_stride = open_heif(img, convert_hdr_to_8bit=False, bgr_mode=True, hdr_to_16bit=False, remove_stride=False) + assert bgr_no16.mode == bgr_no16_stride.mode + np__bgr_no16 = np.asarray(bgr_no16) + np__bgr_no16_stride = np.asarray(bgr_no16_stride)[:, : im_info.size[0], :] + np.testing.assert_array_equal(np__bgr_no16, np__bgr_no16_stride) + # encode test + buf = BytesIO() + for i in (rgb, rgb_stride, rgb_no16, rgb_no16_stride, bgr, bgr_stride, bgr_no16, bgr_no16_stride): + _ = from_bytes(i.mode, i.size, i.data, stride=i.stride) + _.save(buf, chroma=444, quality=-1) + compare_hashes([Image.open(buf), im_pillow], hash_size=24) + + +@pytest.mark.skipif(not hevc_enc(), reason="Requires HEVC encoder.") +@pytest.mark.parametrize( + "img", + ( + "images/heif/L_10__29x100.heif", + "images/heif/L_10__128x128.heif", + "images/heif/L_12__29x100.heif", + "images/heif/L_12__128x128.heif", + "images/heif/RGB_10__29x100.heif", + "images/heif/RGB_10__128x128.heif", + "images/heif/RGB_12__29x100.heif", + "images/heif/RGB_12__128x128.heif", + "images/heif/RGBA_10__29x100.heif", + "images/heif/RGBA_10__128x128.heif", + "images/heif/RGBA_12__29x100.heif", + "images/heif/RGBA_12__128x128.heif", + ), +) +@pytest.mark.parametrize("remove_stride", (True, False)) +def test_open_save_disable_16bit(img, remove_stride): + im = open_heif(img, convert_hdr_to_8bit=False, hdr_to_16bit=False, remove_stride=remove_stride) + buf = BytesIO() + im.save(buf) + im_out = open_heif(buf, convert_hdr_to_8bit=False, hdr_to_16bit=False, remove_stride=remove_stride) + compare_heif_files_fields(im, im_out) diff --git a/tests/numpy_test.py b/tests/numpy_test.py index 87a753d5..791acd9e 100644 --- a/tests/numpy_test.py +++ b/tests/numpy_test.py @@ -59,7 +59,7 @@ def test_numpy_array_invalid_ispe(im_path, bgr_mode): @pytest.mark.parametrize("mode", ("L", "LA", "RGB", "RGBA")) -def test_rgb_array_with_custom_stride(mode): +def test_from_bytes_array_with_custom_stride(mode): im_data = np.asarray(helpers.gradient_rgb().convert(mode=mode)) im = pillow_heif.from_bytes(mode, (127, 256), bytes(im_data), stride=256 * len(mode)) np.testing.assert_array_equal(im_data, im) @@ -68,3 +68,36 @@ def test_rgb_array_with_custom_stride(mode): buf = BytesIO() im.save(buf, quality=-1, chroma=444) helpers.compare_hashes([Image.open(buf), Image.fromarray(im_data)], hash_size=32) + + +@pytest.mark.parametrize( + "img", + ( + "images/heif/RGB_10__29x100.heif", + "images/heif/RGB_12__29x100.heif", + "images/heif/RGBA_10__29x100.heif", + "images/heif/RGBA_12__29x100.heif", + ), +) +def test_numpy_disable_hdr16bit(img): + heif_file = pillow_heif.open_heif(img, convert_hdr_to_8bit=False, hdr_to_16bit=False) + heif_array = np.asarray(heif_file) + assert heif_array.dtype == np.uint16 + assert heif_array.shape == (100, 29, 4 if heif_file.has_alpha else 3) + + +@pytest.mark.parametrize( + "img", + ( + "images/heif/RGB_8__29x100.heif", + "images/heif/RGB_10__29x100.heif", + "images/heif/RGB_12__29x100.heif", + "images/heif/RGBA_8__29x100.heif", + "images/heif/RGBA_10__29x100.heif", + "images/heif/RGBA_12__29x100.heif", + ), +) +def test_numpy_disable_stride_removal(img): + heif_file = pillow_heif.open_heif(img, convert_hdr_to_8bit=False, remove_stride=False) + heif_array = np.asarray(heif_file) + assert heif_array.shape == (100, 64, 4 if heif_file.has_alpha else 3) diff --git a/tests/opencv_test.py b/tests/opencv_test.py index 679beebe..17c3ad6e 100644 --- a/tests/opencv_test.py +++ b/tests/opencv_test.py @@ -17,22 +17,6 @@ os.chdir(os.path.dirname(os.path.abspath(__file__))) -@pytest.mark.parametrize( - "img,expected_mode", - ( - ("images/heif/RGB_8__29x100.heif", "BGR"), - ("images/heif/RGB_10__29x100.heif", "BGR;16"), - ("images/heif/RGB_12__29x100.heif", "BGR;16"), - ("images/heif/RGBA_8__29x100.heif", "BGRA"), - ("images/heif/RGBA_10__29x100.heif", "BGRA;16"), - ("images/heif/RGBA_12__29x100.heif", "BGRA;16"), - ), -) -def test_open_bgr_mode(img, expected_mode): - im = open_heif(img, convert_hdr_to_8bit=False, bgr_mode=True) - assert im.mode == expected_mode - - @pytest.mark.skipif(not hevc_enc(), reason="Requires HEVC encoder.") @pytest.mark.parametrize("enc_bits", (10, 12)) @pytest.mark.parametrize( diff --git a/tests/read_test.py b/tests/read_test.py index fa9da734..58554392 100644 --- a/tests/read_test.py +++ b/tests/read_test.py @@ -79,11 +79,6 @@ def test_native_deepcopy_pillow(img_path): helpers.assert_image_equal(im, im_deepcopy) -def test_bgr_mode_with_disabled_postprocess(): - with pytest.raises(ValueError): - pillow_heif.open_heif(Path("images/heif/RGB_8__29x100.heif"), bgr_mode=True, postprocess=False) - - def test_add_empty_from_pillow(): im = Image.new(mode="L", size=(1, 0)) heif = pillow_heif.HeifFile()