diff --git a/MANIFEST.in b/MANIFEST.in index 09f0a133e..a298f3b2e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -21,6 +21,7 @@ prune docs/_static # build/src control detritus exclude .coveragerc +exclude codecov.yml exclude .editorconfig exclude .landscape.yaml exclude .travis diff --git a/PIL/SgiImagePlugin.py b/PIL/SgiImagePlugin.py index d5db91c2f..e5066ca6f 100644 --- a/PIL/SgiImagePlugin.py +++ b/PIL/SgiImagePlugin.py @@ -9,6 +9,7 @@ # # # History: +# 2017-22-07 mb Add RLE decompression # 2016-16-10 mb Add save method without compression # 1995-09-10 fl Created # @@ -22,9 +23,11 @@ from . import Image, ImageFile -from ._binary import i8, o8, i16be as i16 +from ._binary import i8, o8, i16be as i16, o16be as o16 import struct import os +import sys + __version__ = "0.3" @@ -33,9 +36,20 @@ def _accept(prefix): return len(prefix) >= 2 and i16(prefix) == 474 +MODES = { + (1, 1, 1): "L", + (1, 2, 1): "L", + (2, 1, 1): "L;16B", + (2, 2, 1): "L;16B", + (1, 3, 3): "RGB", + (2, 3, 3): "RGB;16B", + (1, 3, 4): "RGBA", + (2, 3, 4): "RGBA;16B" +} + + ## # Image plugin for SGI images. - class SgiImageFile(ImageFile.ImageFile): format = "SGI" @@ -44,54 +58,89 @@ class SgiImageFile(ImageFile.ImageFile): def _open(self): # HEAD - s = self.fp.read(512) + headlen = 512 + s = self.fp.read(headlen) + + # magic number : 474 if i16(s) != 474: raise ValueError("Not an SGI image file") - # relevant header entries + # compression : verbatim or RLE compression = i8(s[2]) - # bytes, dimension, zsize - layout = i8(s[3]), i16(s[4:]), i16(s[10:]) + # bpc : 1 or 2 bytes (8bits or 16bits) + bpc = i8(s[3]) - # determine mode from bytes/zsize - if layout == (1, 2, 1) or layout == (1, 1, 1): - self.mode = "L" - elif layout == (1, 3, 3): - self.mode = "RGB" - elif layout == (1, 3, 4): - self.mode = "RGBA" - else: + # dimension : 1, 2 or 3 (depending on xsize, ysize and zsize) + dimension = i16(s[4:]) + + # xsize : width + xsize = i16(s[6:]) + + # ysize : height + ysize = i16(s[8:]) + + # zsize : channels count + zsize = i16(s[10:]) + + # layout + layout = bpc, dimension, zsize + + # determine mode from bits/zsize + rawmode = "" + try: + rawmode = MODES[layout] + except KeyError: + pass + + if rawmode == "": raise ValueError("Unsupported SGI image mode") - # size - self.size = i16(s[6:]), i16(s[8:]) + self.size = xsize, ysize + self.mode = rawmode.split(";")[0] + + # orientation -1 : scanlines begins at the bottom-left corner + orientation = -1 # decoder info if compression == 0: - offset = 512 - pagesize = self.size[0]*self.size[1]*layout[0] - self.tile = [] - for layer in self.mode: - self.tile.append( - ("raw", (0, 0)+self.size, offset, (layer, 0, -1))) - offset = offset + pagesize + pagesize = xsize * ysize * bpc + if bpc == 2: + self.tile = [("SGI16", (0, 0) + self.size, + headlen, (self.mode, 0, orientation))] + else: + self.tile = [] + offset = headlen + for layer in self.mode: + self.tile.append( + ("raw", (0, 0) + self.size, + offset, (layer, 0, orientation))) + offset += pagesize elif compression == 1: - raise ValueError("SGI RLE encoding not supported") + self.tile = [("sgi_rle", (0, 0) + self.size, + headlen, (rawmode, orientation, bpc))] def _save(im, fp, filename): if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L": raise ValueError("Unsupported SGI image mode") + # Get the keyword arguments + info = im.encoderinfo + + # Byte-per-pixel precision, 1 = 8bits per pixel + bpc = info.get("bpc", 1) + + if bpc != 1 and bpc != 2: + raise ValueError("Unsupported number of bytes per pixel") + # Flip the image, since the origin of SGI file is the bottom-left corner im = im.transpose(Image.FLIP_TOP_BOTTOM) # Define the file as SGI File Format magicNumber = 474 # Run-Length Encoding Compression - Unsupported at this time rle = 0 - # Byte-per-pixel precision, 1 = 8bits per pixel - bpc = 1 + # Number of dimensions (x,y,z) dim = 3 # X Dimension = width / Y Dimension = height @@ -102,6 +151,11 @@ def _save(im, fp, filename): dim = 2 # Z Dimension: Number of channels z = len(im.mode) + + # assert we've got the right number of bands. + if len(im.getbands()) != z: + raise ValueError("incorrect number of bands in SGI write: %s vs %s" % + (z, len(im.getbands()))) if dim == 1 or dim == 2: z = 1 # assert we've got the right number of bands. @@ -128,23 +182,59 @@ def _save(im, fp, filename): fp.write(struct.pack('>H', z)) fp.write(struct.pack('>l', pinmin)) fp.write(struct.pack('>l', pinmax)) - fp.write(struct.pack('4s', b'')) # dummy fp.write(struct.pack('79s', imgName)) # truncates to 79 chars fp.write(struct.pack('s', b'')) # force null byte after imgname fp.write(struct.pack('>l', colormap)) - fp.write(struct.pack('404s', b'')) # dummy for channel in im.split(): - fp.write(channel.tobytes()) + rawchannel = channel.tobytes() + if bpc == 1: + fp.write(rawchannel) + else: + for pixel in rawchannel: + fp.write(o16(i8(pixel) * 256)) fp.close() +class SGI16Decoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer): + rawmode, stride, orientation = self.args + pagesize = self.state.xsize * self.state.ysize + zsize = len(self.mode) + data = bytearray(pagesize * zsize) + self.fd.seek(512) + s = self.fd.read(2 * pagesize * zsize) + print(len(s)) + i = 0 + y = 0 + if orientation < 0: + y = self.state.ysize - 1 + while y >= 0 and y < self.state.ysize: + for x in range(self.state.xsize): + for z in range(zsize): + bi = (x + y * self.state.xsize + + y * stride + z * pagesize) * 2 + pixel = i16(s, o=bi) + pixel = int(pixel // 256) + if sys.version_info.major == 3: + data[i] = pixel + else: + data[i] = o8(pixel) + i += 1 + y += orientation + self.set_as_raw(bytes(data)) + return -1, 0 + # # registry + +Image.register_decoder("SGI16", SGI16Decoder) Image.register_open(SgiImageFile.format, SgiImageFile, _accept) Image.register_save(SgiImageFile.format, _save) Image.register_mime(SgiImageFile.format, "image/sgi") diff --git a/Tests/images/hopper16.rgb b/Tests/images/hopper16.rgb new file mode 100755 index 000000000..c3c7fb735 Binary files /dev/null and b/Tests/images/hopper16.rgb differ diff --git a/Tests/images/tv.rgb b/Tests/images/tv.rgb new file mode 100755 index 000000000..dcb2a99a2 Binary files /dev/null and b/Tests/images/tv.rgb differ diff --git a/Tests/images/tv16.sgi b/Tests/images/tv16.sgi new file mode 100755 index 000000000..432f03280 Binary files /dev/null and b/Tests/images/tv16.sgi differ diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index e78217f6f..c7a4c1945 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -13,6 +13,12 @@ class TestFileSgi(PillowTestCase): im = Image.open(test_file) self.assert_image_equal(im, hopper()) + def test_rgb16(self): + test_file = "Tests/images/hopper16.rgb" + + im = Image.open(test_file) + self.assert_image_equal(im, hopper()) + def test_l(self): # Created with ImageMagick # convert hopper.ppm -monochrome -compress None sgi:hopper.bw @@ -31,12 +37,20 @@ class TestFileSgi(PillowTestCase): self.assert_image_equal(im, target) def test_rle(self): + # Created with ImageMagick: # convert hopper.ppm hopper.sgi - # We don't support RLE compression, this should throw a value error test_file = "Tests/images/hopper.sgi" - with self.assertRaises(ValueError): - Image.open(test_file) + im = Image.open(test_file) + target = Image.open('Tests/images/hopper.rgb') + self.assert_image_equal(im, target) + + def test_rle16(self): + test_file = "Tests/images/tv16.sgi" + + im = Image.open(test_file) + target = Image.open('Tests/images/tv.rgb') + self.assert_image_equal(im, target) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" diff --git a/_imaging.c b/_imaging.c index d0777c73a..83c72f083 100644 --- a/_imaging.c +++ b/_imaging.c @@ -3354,6 +3354,7 @@ extern PyObject* PyImaging_PackbitsDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_PcdDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_PcxDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_RawDecoderNew(PyObject* self, PyObject* args); +extern PyObject* PyImaging_SgiRleDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_SunRleDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_TgaRleDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_XbmDecoderNew(PyObject* self, PyObject* args); @@ -3433,6 +3434,7 @@ static PyMethodDef functions[] = { {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, 1}, {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, 1}, {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, 1}, + {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, 1}, {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, 1}, {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, 1}, {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, 1}, diff --git a/decode.c b/decode.c index 022912ffb..bfd19faa4 100644 --- a/decode.c +++ b/decode.c @@ -38,6 +38,7 @@ #include "Lzw.h" #include "Raw.h" #include "Bit.h" +#include "Sgi.h" /* -------------------------------------------------------------------- */ @@ -671,6 +672,39 @@ PyImaging_RawDecoderNew(PyObject* self, PyObject* args) } +/* -------------------------------------------------------------------- */ +/* SGI RLE */ +/* -------------------------------------------------------------------- */ + +PyObject* +PyImaging_SgiRleDecoderNew(PyObject* self, PyObject* args) +{ + ImagingDecoderObject* decoder; + + char* mode; + char* rawmode; + int ystep = 1; + int bpc = 1; + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &bpc)) + return NULL; + + decoder = PyImaging_DecoderNew(sizeof(SGISTATE)); + if (decoder == NULL) + return NULL; + + if (get_unpacker(decoder, mode, rawmode) < 0) + return NULL; + + decoder->pulls_fd = 1; + decoder->decode = ImagingSgiRleDecode; + decoder->state.ystep = ystep; + + ((SGISTATE*)decoder->state.context)->bpc = bpc; + + return (PyObject*) decoder; +} + + /* -------------------------------------------------------------------- */ /* SUN RLE */ /* -------------------------------------------------------------------- */ diff --git a/libImaging/Imaging.h b/libImaging/Imaging.h index a48d373ee..02e95f930 100644 --- a/libImaging/Imaging.h +++ b/libImaging/Imaging.h @@ -446,6 +446,9 @@ extern int ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); +extern int ImagingSgiRleDecode(Imaging im, ImagingCodecState state, + UINT8* buffer, int bytes); +extern int ImagingSgiRleDecodeCleanup(ImagingCodecState state); extern int ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingTgaRleDecode(Imaging im, ImagingCodecState state, diff --git a/libImaging/Sgi.h b/libImaging/Sgi.h new file mode 100644 index 000000000..8015d6661 --- /dev/null +++ b/libImaging/Sgi.h @@ -0,0 +1,40 @@ +/* Sgi.h */ + +typedef struct { + + /* CONFIGURATION */ + + /* Number of bytes per channel per pixel */ + int bpc; + + /* RLE offsets table */ + UINT32 *starttab; + + /* RLE lengths table */ + UINT32 *lengthtab; + + /* current row offset */ + UINT32 rleoffset; + + /* current row length */ + UINT32 rlelength; + + /* RLE table size */ + int tablen; + + /* RLE table index */ + int tabindex; + + /* buffer index */ + int bufindex; + + /* current row index */ + int rowno; + + /* current channel index */ + int channo; + + /* image data size from file descriptor */ + long bufsize; + +} SGISTATE; \ No newline at end of file diff --git a/libImaging/SgiRleDecode.c b/libImaging/SgiRleDecode.c new file mode 100644 index 000000000..3c5e2f2ae --- /dev/null +++ b/libImaging/SgiRleDecode.c @@ -0,0 +1,168 @@ +/* + * The Python Imaging Library. + * $Id$ + * + * decoder for Sgi RLE data. + * + * history: + * 2017-07-28 mb fixed for images larger than 64KB + * 2017-07-20 mb created + * + * Copyright (c) Mickael Bonfill 2017. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" +#include "Sgi.h" + +#define SGI_HEADER_SIZE 512 +#define RLE_COPY_FLAG 0x80 +#define RLE_MAX_RUN 0x7f + +static void read4B(UINT32* dest, UINT8* buf) +{ + *dest = (UINT32)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]); +} + +static int expandrow(UINT8* dest, UINT8* src, int n, int z) +{ + UINT8 pixel, count; + + for (;n > 0; n--) + { + pixel = *src++; + if (n == 1 && pixel != 0) + return n; + count = pixel & RLE_MAX_RUN; + if (!count) + return count; + if (pixel & RLE_COPY_FLAG) { + while(count--) { + *dest = *src++; + dest += z; + } + + } + else { + pixel = *src++; + while (count--) { + *dest = pixel; + dest += z; + } + } + + } + return 0; +} + +static int expandrow2(UINT16* dest, UINT16* src, int n, int z) +{ + UINT8 pixel, count; + + + for (;n > 0; n--) + { + pixel = ((UINT8*)src)[1]; + ++src; + if (n == 1 && pixel != 0) + return n; + count = pixel & RLE_MAX_RUN; + if (!count) + return count; + if (pixel & RLE_COPY_FLAG) { + while(count--) { + *dest = *src++; + dest += z; + } + } + else { + while (count--) { + *dest = *src; + dest += z; + } + ++src; + } + } + return 0; +} + + +int +ImagingSgiRleDecode(Imaging im, ImagingCodecState state, + UINT8* buf, int bytes) +{ + UINT8 *ptr; + SGISTATE *c; + + /* Get all data from File descriptor */ + c = (SGISTATE*)state->context; + _imaging_seek_pyFd(state->fd, 0L, SEEK_END); + c->bufsize = _imaging_tell_pyFd(state->fd); + c->bufsize -= SGI_HEADER_SIZE; + ptr = malloc(sizeof(UINT8) * c->bufsize); + _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); + _imaging_read_pyFd(state->fd, (char*)ptr, c->bufsize); + + + /* decoder initialization */ + state->count = 0; + state->y = 0; + if (state->ystep < 0) + state->y = im->ysize - 1; + else + state->ystep = 1; + + /* Allocate memory for RLE tables and rows */ + free(state->buffer); + state->buffer = malloc(sizeof(UINT8) * 2 * im->xsize * im->bands); + c->tablen = im->bands * im->ysize; + c->starttab = calloc(c->tablen, sizeof(UINT32)); + c->lengthtab = calloc(c->tablen, sizeof(UINT32)); + + /* populate offsets table */ + for (c->tabindex = 0, c->bufindex = 0; c->tabindex < c->tablen; c->tabindex++, c->bufindex+=4) + read4B(&c->starttab[c->tabindex], &ptr[c->bufindex]); + /* populate lengths table */ + for (c->tabindex = 0, c->bufindex = c->tablen * sizeof(UINT32); c->tabindex < c->tablen; c->tabindex++, c->bufindex+=4) + read4B(&c->lengthtab[c->tabindex], &ptr[c->bufindex]); + + state->count += c->tablen * sizeof(UINT32) * 2; + + /* read compressed rows */ + for (c->rowno = 0; c->rowno < im->ysize; c->rowno++, state->y += state->ystep) + { + for (c->channo = 0; c->channo < im->bands; c->channo++) + { + c->rleoffset = c->starttab[c->rowno + c->channo * im->ysize]; + c->rlelength = c->lengthtab[c->rowno + c->channo * im->ysize]; + c->rleoffset -= SGI_HEADER_SIZE; + + /* row decompression */ + if (c->bpc ==1) { + if(expandrow(&state->buffer[c->channo], &ptr[c->rleoffset], c->rlelength, im->bands)) + goto sgi_finish_decode; + } + else { + if(expandrow2((UINT16*)&state->buffer[c->channo * 2], (UINT16*)&ptr[c->rleoffset], c->rlelength, im->bands)) + goto sgi_finish_decode; + } + + state->count += c->rlelength; + } + + /* store decompressed data in image */ + state->shuffle((UINT8*)im->image[state->y], state->buffer, im->xsize); + + } + + c->bufsize++; + +sgi_finish_decode: ; + + free(c->starttab); + free(c->lengthtab); + free(ptr); + + return state->count - c->bufsize; +} diff --git a/setup.py b/setup.py index f378362e6..1c2185300 100755 --- a/setup.py +++ b/setup.py @@ -35,8 +35,8 @@ _LIB_IMAGING = ( "Negative", "Offset", "Pack", "PackDecode", "Palette", "Paste", "Quant", "QuantOctree", "QuantHash", "QuantHeap", "PcdDecode", "PcxDecode", "PcxEncode", "Point", "RankFilter", "RawDecode", "RawEncode", "Storage", - "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC", "UnsharpMask", - "XbmDecode", "XbmEncode", "ZipDecode", "ZipEncode", "TiffDecode", + "SgiRleDecode", "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC", + "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", "ZipEncode", "TiffDecode", "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur", "QuantPngQuant", "codec_fd") DEBUG = False