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 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"
|
||||
|
|
8
setup.py
|
@ -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
|
||||
|
||||
|
|
|
@ -147,6 +147,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:
|
||||
|
@ -177,8 +182,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")
|
||||
|
|
|
@ -3594,6 +3594,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);
|
||||
|
@ -3661,6 +3662,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
|
||||
|
|
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 */
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
|
|
@ -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,
|
||||
|
|
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;
|
||||
}
|