Merge pull request #3186 from danpla/tga-write-rle
TGA: Add support for writing RLE data
BIN
Tests/images/tga/common/1x1_l.png
Normal file
After Width: | Height: | Size: 67 B |
BIN
Tests/images/tga/common/1x1_l_bl_raw.tga
Normal file
BIN
Tests/images/tga/common/1x1_l_bl_rle.tga
Normal file
BIN
Tests/images/tga/common/1x1_l_tl_raw.tga
Normal file
BIN
Tests/images/tga/common/1x1_l_tl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_l.png
Normal file
After Width: | Height: | Size: 385 B |
BIN
Tests/images/tga/common/200x32_l_bl_raw.tga
Normal file
BIN
Tests/images/tga/common/200x32_l_bl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_l_tl_raw.tga
Normal file
BIN
Tests/images/tga/common/200x32_l_tl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_la.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Tests/images/tga/common/200x32_la_bl_raw.tga
Normal file
BIN
Tests/images/tga/common/200x32_la_bl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_la_tl_raw.tga
Normal file
BIN
Tests/images/tga/common/200x32_la_tl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_p.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
Tests/images/tga/common/200x32_p_bl_raw.tga
Normal file
BIN
Tests/images/tga/common/200x32_p_bl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_p_tl_raw.tga
Normal file
BIN
Tests/images/tga/common/200x32_p_tl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_rgb.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
Tests/images/tga/common/200x32_rgb_bl_raw.tga
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
Tests/images/tga/common/200x32_rgb_bl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_rgb_tl_raw.tga
Normal file
BIN
Tests/images/tga/common/200x32_rgb_tl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_rgba.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
Tests/images/tga/common/200x32_rgba_bl_raw.tga
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
Tests/images/tga/common/200x32_rgba_bl_rle.tga
Normal file
BIN
Tests/images/tga/common/200x32_rgba_tl_raw.tga
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
Tests/images/tga/common/200x32_rgba_tl_rle.tga
Normal file
12
Tests/images/tga/common/readme.txt
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
Images in this directory were created with GIMP.
|
||||||
|
|
||||||
|
TGAs have names in the following format:
|
||||||
|
|
||||||
|
{width}x{height}_{mode}_{origin}_{compression}.tga
|
||||||
|
|
||||||
|
Where:
|
||||||
|
mode is PIL mode in lower case (L, P, RGB, etc.)
|
||||||
|
origin:
|
||||||
|
"bl" - bottom left
|
||||||
|
"tl" - top left
|
||||||
|
compression is either "raw" or "rle"
|
|
@ -1,10 +1,76 @@
|
||||||
|
import os
|
||||||
|
from glob import glob
|
||||||
|
from itertools import product
|
||||||
|
|
||||||
from helper import unittest, PillowTestCase
|
from helper import unittest, PillowTestCase
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
_TGA_DIR = os.path.join("Tests", "images", "tga")
|
||||||
|
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
|
||||||
|
|
||||||
|
|
||||||
class TestFileTga(PillowTestCase):
|
class TestFileTga(PillowTestCase):
|
||||||
|
|
||||||
|
_MODES = ("L", "P", "RGB", "RGBA")
|
||||||
|
_ORIGINS = ("tl", "bl")
|
||||||
|
|
||||||
|
_ORIGIN_TO_ORIENTATION = {
|
||||||
|
"tl": 1,
|
||||||
|
"bl": -1
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_sanity(self):
|
||||||
|
for mode in self._MODES:
|
||||||
|
png_paths = glob(
|
||||||
|
os.path.join(
|
||||||
|
_TGA_DIR_COMMON, "*x*_{}.png".format(mode.lower())))
|
||||||
|
|
||||||
|
for png_path in png_paths:
|
||||||
|
reference_im = Image.open(png_path)
|
||||||
|
self.assertEqual(reference_im.mode, mode)
|
||||||
|
|
||||||
|
path_no_ext = os.path.splitext(png_path)[0]
|
||||||
|
for origin, rle in product(self._ORIGINS, (True, False)):
|
||||||
|
tga_path = "{}_{}_{}.tga".format(
|
||||||
|
path_no_ext, origin, "rle" if rle else "raw")
|
||||||
|
|
||||||
|
original_im = Image.open(tga_path)
|
||||||
|
if rle:
|
||||||
|
self.assertEqual(
|
||||||
|
original_im.info["compression"], "tga_rle")
|
||||||
|
self.assertEqual(
|
||||||
|
original_im.info["orientation"],
|
||||||
|
self._ORIGIN_TO_ORIENTATION[origin])
|
||||||
|
if mode == "P":
|
||||||
|
self.assertEqual(
|
||||||
|
original_im.getpalette(),
|
||||||
|
reference_im.getpalette())
|
||||||
|
|
||||||
|
self.assert_image_equal(original_im, reference_im)
|
||||||
|
|
||||||
|
# Generate a new test name every time so the
|
||||||
|
# test will not fail with permission error
|
||||||
|
# on Windows.
|
||||||
|
test_file = self.tempfile("temp.tga")
|
||||||
|
|
||||||
|
original_im.save(test_file, rle=rle)
|
||||||
|
saved_im = Image.open(test_file)
|
||||||
|
if rle:
|
||||||
|
self.assertEqual(
|
||||||
|
saved_im.info["compression"],
|
||||||
|
original_im.info["compression"])
|
||||||
|
self.assertEqual(
|
||||||
|
saved_im.info["orientation"],
|
||||||
|
original_im.info["orientation"])
|
||||||
|
if mode == "P":
|
||||||
|
self.assertEqual(
|
||||||
|
saved_im.getpalette(),
|
||||||
|
original_im.getpalette())
|
||||||
|
|
||||||
|
self.assert_image_equal(saved_im, original_im)
|
||||||
|
|
||||||
def test_id_field(self):
|
def test_id_field(self):
|
||||||
# tga file with id field
|
# tga file with id field
|
||||||
test_file = "Tests/images/tga_id_field.tga"
|
test_file = "Tests/images/tga_id_field.tga"
|
||||||
|
|
8
setup.py
|
@ -46,10 +46,10 @@ _LIB_IMAGING = (
|
||||||
"Negative", "Offset", "Pack", "PackDecode", "Palette", "Paste", "Quant",
|
"Negative", "Offset", "Pack", "PackDecode", "Palette", "Paste", "Quant",
|
||||||
"QuantOctree", "QuantHash", "QuantHeap", "PcdDecode", "PcxDecode",
|
"QuantOctree", "QuantHash", "QuantHeap", "PcdDecode", "PcxDecode",
|
||||||
"PcxEncode", "Point", "RankFilter", "RawDecode", "RawEncode", "Storage",
|
"PcxEncode", "Point", "RankFilter", "RawDecode", "RawEncode", "Storage",
|
||||||
"SgiRleDecode", "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC",
|
"SgiRleDecode", "SunRleDecode", "TgaRleDecode", "TgaRleEncode", "Unpack",
|
||||||
"UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", "ZipEncode",
|
"UnpackYCC", "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode",
|
||||||
"TiffDecode", "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur", "QuantPngQuant",
|
"ZipEncode", "TiffDecode", "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur",
|
||||||
"codec_fd")
|
"QuantPngQuant", "codec_fd")
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
|
|
|
@ -151,6 +151,11 @@ def _save(im, fp, filename):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise IOError("cannot write mode %s as TGA" % im.mode)
|
raise IOError("cannot write mode %s as TGA" % im.mode)
|
||||||
|
|
||||||
|
rle = im.encoderinfo.get("rle", False)
|
||||||
|
|
||||||
|
if rle:
|
||||||
|
imagetype += 8
|
||||||
|
|
||||||
if colormaptype:
|
if colormaptype:
|
||||||
colormapfirst, colormaplength, colormapentry = 0, 256, 24
|
colormapfirst, colormaplength, colormapentry = 0, 256, 24
|
||||||
else:
|
else:
|
||||||
|
@ -181,6 +186,12 @@ def _save(im, fp, filename):
|
||||||
if colormaptype:
|
if colormaptype:
|
||||||
fp.write(im.im.getpalette("RGB", "BGR"))
|
fp.write(im.im.getpalette("RGB", "BGR"))
|
||||||
|
|
||||||
|
if rle:
|
||||||
|
ImageFile._save(
|
||||||
|
im,
|
||||||
|
fp,
|
||||||
|
[("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))])
|
||||||
|
else:
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))])
|
im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))])
|
||||||
|
|
||||||
|
|
|
@ -3664,6 +3664,7 @@ extern PyObject* PyImaging_JpegEncoderNew(PyObject* self, PyObject* args);
|
||||||
extern PyObject* PyImaging_Jpeg2KEncoderNew(PyObject* self, PyObject* args);
|
extern PyObject* PyImaging_Jpeg2KEncoderNew(PyObject* self, PyObject* args);
|
||||||
extern PyObject* PyImaging_PcxEncoderNew(PyObject* self, PyObject* args);
|
extern PyObject* PyImaging_PcxEncoderNew(PyObject* self, PyObject* args);
|
||||||
extern PyObject* PyImaging_RawEncoderNew(PyObject* self, PyObject* args);
|
extern PyObject* PyImaging_RawEncoderNew(PyObject* self, PyObject* args);
|
||||||
|
extern PyObject* PyImaging_TgaRleEncoderNew(PyObject* self, PyObject* args);
|
||||||
extern PyObject* PyImaging_XbmEncoderNew(PyObject* self, PyObject* args);
|
extern PyObject* PyImaging_XbmEncoderNew(PyObject* self, PyObject* args);
|
||||||
extern PyObject* PyImaging_ZipEncoderNew(PyObject* self, PyObject* args);
|
extern PyObject* PyImaging_ZipEncoderNew(PyObject* self, PyObject* args);
|
||||||
extern PyObject* PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args);
|
extern PyObject* PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args);
|
||||||
|
@ -3731,6 +3732,7 @@ static PyMethodDef functions[] = {
|
||||||
{"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, 1},
|
{"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, 1},
|
||||||
{"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, 1},
|
{"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, 1},
|
||||||
{"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, 1},
|
{"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, 1},
|
||||||
|
{"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, 1},
|
||||||
{"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, 1},
|
{"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, 1},
|
||||||
{"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, 1},
|
{"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, 1},
|
||||||
#ifdef HAVE_LIBZ
|
#ifdef HAVE_LIBZ
|
||||||
|
|
32
src/encode.c
|
@ -492,6 +492,38 @@ PyImaging_RawEncoderNew(PyObject* self, PyObject* args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/* TGA */
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
PyObject*
|
||||||
|
PyImaging_TgaRleEncoderNew(PyObject* self, PyObject* args)
|
||||||
|
{
|
||||||
|
ImagingEncoderObject* encoder;
|
||||||
|
|
||||||
|
char *mode;
|
||||||
|
char *rawmode;
|
||||||
|
int ystep = 1;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &ystep))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
encoder = PyImaging_EncoderNew(0);
|
||||||
|
if (encoder == NULL)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if (get_packer(encoder, mode, rawmode) < 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
encoder->encode = ImagingTgaRleEncode;
|
||||||
|
|
||||||
|
encoder->state.ystep = ystep;
|
||||||
|
|
||||||
|
return (PyObject*) encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
/* XBM */
|
/* XBM */
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
|
|
|
@ -473,6 +473,8 @@ extern int ImagingSunRleDecode(Imaging im, ImagingCodecState state,
|
||||||
UINT8* buffer, int bytes);
|
UINT8* buffer, int bytes);
|
||||||
extern int ImagingTgaRleDecode(Imaging im, ImagingCodecState state,
|
extern int ImagingTgaRleDecode(Imaging im, ImagingCodecState state,
|
||||||
UINT8* buffer, int bytes);
|
UINT8* buffer, int bytes);
|
||||||
|
extern int ImagingTgaRleEncode(Imaging im, ImagingCodecState state,
|
||||||
|
UINT8* buffer, int bytes);
|
||||||
extern int ImagingXbmDecode(Imaging im, ImagingCodecState state,
|
extern int ImagingXbmDecode(Imaging im, ImagingCodecState state,
|
||||||
UINT8* buffer, int bytes);
|
UINT8* buffer, int bytes);
|
||||||
extern int ImagingXbmEncode(Imaging im, ImagingCodecState state,
|
extern int ImagingXbmEncode(Imaging im, ImagingCodecState state,
|
||||||
|
|
153
src/libImaging/TgaRleEncode.c
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
|
||||||
|
#include "Imaging.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
static int comparePixels(const UINT8* buf, int x, int bytesPerPixel)
|
||||||
|
{
|
||||||
|
buf += x * bytesPerPixel;
|
||||||
|
return memcmp(buf, buf + bytesPerPixel, bytesPerPixel) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes)
|
||||||
|
{
|
||||||
|
UINT8* dst;
|
||||||
|
int bytesPerPixel;
|
||||||
|
|
||||||
|
if (state->state == 0) {
|
||||||
|
if (state->ystep < 0) {
|
||||||
|
state->ystep = -1;
|
||||||
|
state->y = state->ysize - 1;
|
||||||
|
} else
|
||||||
|
state->ystep = 1;
|
||||||
|
|
||||||
|
state->state = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
dst = buf;
|
||||||
|
bytesPerPixel = (state->bits + 7) / 8;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
int flushCount;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* state->count is the numbers of bytes in the packet,
|
||||||
|
* excluding the 1-byte descriptor.
|
||||||
|
*/
|
||||||
|
if (state->count == 0) {
|
||||||
|
UINT8* row;
|
||||||
|
UINT8 descriptor;
|
||||||
|
int startX;
|
||||||
|
|
||||||
|
assert(state->x <= state->xsize);
|
||||||
|
|
||||||
|
/* Make sure we have space for the descriptor. */
|
||||||
|
if (bytes < 1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (state->x == state->xsize) {
|
||||||
|
state->x = 0;
|
||||||
|
|
||||||
|
state->y += state->ystep;
|
||||||
|
if (state->y < 0 || state->y >= state->ysize) {
|
||||||
|
state->errcode = IMAGING_CODEC_END;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state->x == 0)
|
||||||
|
state->shuffle(
|
||||||
|
state->buffer,
|
||||||
|
(UINT8*)im->image[state->y + state->yoff]
|
||||||
|
+ state->xoff * im->pixelsize,
|
||||||
|
state->xsize);
|
||||||
|
|
||||||
|
row = state->buffer;
|
||||||
|
|
||||||
|
/* Start with a raw packet for 1 px. */
|
||||||
|
descriptor = 0;
|
||||||
|
startX = state->x;
|
||||||
|
state->count = bytesPerPixel;
|
||||||
|
|
||||||
|
if (state->x + 1 < state->xsize) {
|
||||||
|
int maxLookup;
|
||||||
|
int isRaw;
|
||||||
|
|
||||||
|
isRaw = !comparePixels(row, state->x, bytesPerPixel);
|
||||||
|
++state->x;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A packet can contain up to 128 pixels;
|
||||||
|
* 2 are already behind (state->x points to
|
||||||
|
* the second one).
|
||||||
|
*/
|
||||||
|
maxLookup = state->x + 126;
|
||||||
|
/* A packet must not span multiple rows. */
|
||||||
|
if (maxLookup > state->xsize - 1)
|
||||||
|
maxLookup = state->xsize - 1;
|
||||||
|
|
||||||
|
if (isRaw) {
|
||||||
|
while (state->x < maxLookup)
|
||||||
|
if (!comparePixels(row, state->x, bytesPerPixel))
|
||||||
|
++state->x;
|
||||||
|
else {
|
||||||
|
/* Two identical pixels will go to RLE packet. */
|
||||||
|
--state->x;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->count += (state->x - startX) * bytesPerPixel;
|
||||||
|
} else {
|
||||||
|
descriptor |= 0x80;
|
||||||
|
|
||||||
|
while (state->x < maxLookup)
|
||||||
|
if (comparePixels(row, state->x, bytesPerPixel))
|
||||||
|
++state->x;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* state->x currently points to the last pixel to be
|
||||||
|
* included in the packet. The pixel count in the
|
||||||
|
* descriptor is 1 less than actual number of pixels in
|
||||||
|
* the packet, that is, state->x == startX if we encode
|
||||||
|
* only 1 pixel.
|
||||||
|
*/
|
||||||
|
descriptor += state->x - startX;
|
||||||
|
*dst++ = descriptor;
|
||||||
|
--bytes;
|
||||||
|
|
||||||
|
/* Advance to past-the-last encoded pixel. */
|
||||||
|
++state->x;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(bytes >= 0);
|
||||||
|
assert(state->count > 0);
|
||||||
|
assert(state->x > 0);
|
||||||
|
assert(state->count <= state->x * bytesPerPixel);
|
||||||
|
|
||||||
|
if (bytes == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
flushCount = state->count;
|
||||||
|
if (flushCount > bytes)
|
||||||
|
flushCount = bytes;
|
||||||
|
|
||||||
|
memcpy(
|
||||||
|
dst,
|
||||||
|
state->buffer + (state->x * bytesPerPixel - state->count),
|
||||||
|
flushCount);
|
||||||
|
dst += flushCount;
|
||||||
|
bytes -= flushCount;
|
||||||
|
|
||||||
|
state->count -= flushCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst - buf;
|
||||||
|
}
|