Merge pull request #3186 from danpla/tga-write-rle

TGA: Add support for writing RLE data
This commit is contained in:
Hugo 2018-07-01 22:00:03 +03:00 committed by GitHub
commit af552801c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 284 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

View 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"

View File

@ -1,10 +1,76 @@
import os
from glob import glob
from itertools import product
from helper import unittest, PillowTestCase
from PIL import Image
_TGA_DIR = os.path.join("Tests", "images", "tga")
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
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):
# tga file with id field
test_file = "Tests/images/tga_id_field.tga"

View File

@ -46,10 +46,10 @@ _LIB_IMAGING = (
"Negative", "Offset", "Pack", "PackDecode", "Palette", "Paste", "Quant",
"QuantOctree", "QuantHash", "QuantHeap", "PcdDecode", "PcxDecode",
"PcxEncode", "Point", "RankFilter", "RawDecode", "RawEncode", "Storage",
"SgiRleDecode", "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC",
"UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", "ZipEncode",
"TiffDecode", "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur", "QuantPngQuant",
"codec_fd")
"SgiRleDecode", "SunRleDecode", "TgaRleDecode", "TgaRleEncode", "Unpack",
"UnpackYCC", "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode",
"ZipEncode", "TiffDecode", "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur",
"QuantPngQuant", "codec_fd")
DEBUG = False

View File

@ -151,6 +151,11 @@ def _save(im, fp, filename):
except KeyError:
raise IOError("cannot write mode %s as TGA" % im.mode)
rle = im.encoderinfo.get("rle", False)
if rle:
imagetype += 8
if colormaptype:
colormapfirst, colormaplength, colormapentry = 0, 256, 24
else:
@ -181,8 +186,14 @@ def _save(im, fp, filename):
if colormaptype:
fp.write(im.im.getpalette("RGB", "BGR"))
ImageFile._save(
im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))])
if rle:
ImageFile._save(
im,
fp,
[("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))])
else:
ImageFile._save(
im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))])
# write targa version 2 footer
fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000")

View File

@ -3664,6 +3664,7 @@ extern PyObject* PyImaging_JpegEncoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_Jpeg2KEncoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_PcxEncoderNew(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_ZipEncoderNew(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},
{"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, 1},
{"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, 1},
{"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, 1},
{"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, 1},
{"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, 1},
#ifdef HAVE_LIBZ

View File

@ -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 */
/* -------------------------------------------------------------------- */

View File

@ -473,6 +473,8 @@ extern int ImagingSunRleDecode(Imaging im, ImagingCodecState state,
UINT8* buffer, int bytes);
extern int ImagingTgaRleDecode(Imaging im, ImagingCodecState state,
UINT8* buffer, int bytes);
extern int ImagingTgaRleEncode(Imaging im, ImagingCodecState state,
UINT8* buffer, int bytes);
extern int ImagingXbmDecode(Imaging im, ImagingCodecState state,
UINT8* buffer, int bytes);
extern int ImagingXbmEncode(Imaging im, ImagingCodecState state,

View 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;
}