Merge from master

This commit is contained in:
wiredfool 2013-11-30 09:57:00 -08:00
commit 543f96de61
24 changed files with 340 additions and 51 deletions

View File

@ -6,7 +6,7 @@ python:
- 3.2
- 3.3
install: "sudo apt-get -qq install libfreetype6-dev liblcms2-dev libwebp-dev"
install: "sudo apt-get -qq install libfreetype6-dev liblcms2-dev libwebp-dev ghostscript"
script:
- python setup.py clean

View File

@ -4,6 +4,36 @@ Changelog (Pillow)
2.3.0 (2014-01-01)
------------------
- Quote filenames and title before using on command line
[tmccombs]
- Fixed Viewer.show to return properly
[tmccombs]
- Documentation fixes
[wiredfool]
- Fixed memory leak saving images as webp when webpmux is available
[cezarsa]
- Fix compiling with FreeType 2.5.1
[stromnov]
- Adds directories for NetBSD.
[deepy]
- Support RGBA TIFF with missing ExtraSamples tag
[cgohlke]
- Lossless WEBP Support
[wiredfool]
- Take compression as an option in the save call for tiffs
[wiredfool]
- Add support for saving lossless WebP. Just pass 'lossless=True' to save()
[liftoff]
- LCMS support upgraded from version 1 to version 2, fixes #343
[wiredfool]

View File

@ -50,14 +50,22 @@ if sys.platform.startswith('win'):
else:
gs_windows_binary = False
def Ghostscript(tile, size, fp):
def Ghostscript(tile, size, fp, scale=1):
"""Render an image using Ghostscript"""
# Unpack decoder tile
decoder, tile, offset, data = tile[0]
length, bbox = data
import tempfile, os
#Hack to support hi-res rendering
scale = int(scale) or 1
orig_size = size
orig_bbox = bbox
size = (size[0] * scale, size[1] * scale)
bbox = [bbox[0], bbox[1], bbox[2] * scale, bbox[3] * scale]
#print("Ghostscript", scale, size, orig_size, bbox, orig_bbox)
import tempfile, os, subprocess
file = tempfile.mktemp()
@ -65,33 +73,32 @@ def Ghostscript(tile, size, fp):
command = ["gs",
"-q", # quite mode
"-g%dx%d" % size, # set output geometry (pixels)
"-r%d" % (72*scale), # set input DPI (dots per inch)
"-dNOPAUSE -dSAFER", # don't pause between pages, safe mode
"-sDEVICE=ppmraw", # ppm driver
"-sOutputFile=%s" % file,# output file
"- >/dev/null 2>/dev/null"]
]
if gs_windows_binary is not None:
if gs_windows_binary is False:
raise WindowsError('Unable to locate Ghostscript on paths')
command[0] = gs_windows_binary
command[-1] = '- >nul 2>nul'
command = " ".join(command)
# push data through ghostscript
try:
gs = os.popen(command, "w")
gs = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
# adjust for image origin
if bbox[0] != 0 or bbox[1] != 0:
gs.write("%d %d translate\n" % (-bbox[0], -bbox[1]))
gs.stdin.write(("%d %d translate\n" % (-bbox[0], -bbox[1])).encode('ascii'))
fp.seek(offset)
while length > 0:
s = fp.read(8192)
if not s:
break
length = length - len(s)
gs.write(s)
status = gs.close()
gs.stdin.write(s)
gs.stdin.close()
status = gs.wait()
if status:
raise IOError("gs failed (status %d)" % status)
im = Image.core.open_ppm(file)
@ -304,11 +311,11 @@ class EpsImageFile(ImageFile.ImageFile):
if not box:
raise IOError("cannot determine EPS bounding box")
def load(self):
def load(self, scale=1):
# Load EPS via Ghostscript
if not self.tile:
return
self.im = Ghostscript(self.tile, self.size, self.fp)
self.im = Ghostscript(self.tile, self.size, self.fp, scale)
self.mode = self.im.mode
self.size = self.im.size
self.tile = []

View File

@ -475,7 +475,7 @@ class Image:
new.mode = im.mode
new.size = im.size
new.palette = self.palette
if im.mode == "P":
if im.mode == "P" and not new.palette:
from PIL import ImagePalette
new.palette = ImagePalette.ImagePalette()
try:
@ -675,15 +675,18 @@ class Image:
L = R * 299/1000 + G * 587/1000 + B * 114/1000
When translating a greyscale image into a bilevel image (mode
"1"), all non-zero values are set to 255 (white). To use other
thresholds, use the :py:meth:`~PIL.Image.Image.point` method.
The default method of converting a greyscale ("L") or "RGB"
image into a bilevel (mode "1") image uses Floyd-Steinberg
dither to approximate the original image luminosity levels. If
dither is NONE, all non-zero values are set to 255 (white). To
use other thresholds, use the :py:meth:`~PIL.Image.Image.point`
method.
:param mode: The requested mode.
:param matrix: An optional conversion matrix. If given, this
should be 4- or 16-tuple containing floating point values.
:param dither: Dithering method, used when converting from
mode "RGB" to "P".
mode "RGB" to "P" or from "RGB" or "L" to "1".
Available methods are NONE or FLOYDSTEINBERG (default).
:param palette: Palette to use when converting from mode "RGB"
to "P". Available palettes are WEB or ADAPTIVE.

View File

@ -17,6 +17,11 @@ from __future__ import print_function
from PIL import Image
import os, sys
if(sys.version_info >= (3, 3)):
from shlex import quote
else:
from pipes import quote
_viewers = []
def register(viewer, order=1):
@ -65,7 +70,7 @@ class Viewer:
if base != image.mode and image.mode != "1":
image = image.convert(base)
self.show_image(image, **options)
return self.show_image(image, **options)
# hook methods
@ -99,7 +104,7 @@ if sys.platform == "win32":
format = "BMP"
def get_command(self, file, **options):
return ("start /wait %s && ping -n 2 127.0.0.1 >NUL "
"&& del /f %s" % (file, file))
"&& del /f %s" % (quote(file), quote(file)))
register(WindowsViewer)
@ -111,7 +116,7 @@ elif sys.platform == "darwin":
# on darwin open returns immediately resulting in the temp
# file removal while app is opening
command = "open -a /Applications/Preview.app"
command = "(%s %s; sleep 20; rm -f %s)&" % (command, file, file)
command = "(%s %s; sleep 20; rm -f %s)&" % (command, quote(file), quote(file))
return command
register(MacViewer)
@ -134,7 +139,7 @@ else:
class UnixViewer(Viewer):
def show_file(self, file, **options):
command, executable = self.get_command_ex(file, **options)
command = "(%s %s; rm -f %s)&" % (command, file, file)
command = "(%s %s; rm -f %s)&" % (command, quote(file), quote(file))
os.system(command)
return 1
@ -154,8 +159,7 @@ else:
# imagemagick's display command instead.
command = executable = "xv"
if title:
# FIXME: do full escaping
command = command + " -name \"%s\"" % title
command = command + " -name %s" % quote(title)
return command, executable
if which("xv"):

View File

@ -156,6 +156,7 @@ OPEN_INFO = {
(II, 1, 3, 1, (32,), ()): ("F", "F;32F"),
(II, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"),
(II, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"),
(II, 2, 1, 1, (8,8,8,8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
(II, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"),
(II, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"),
(II, 2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"),
@ -973,7 +974,7 @@ def _save(im, fp, filename):
ifd = ImageFileDirectory(prefix)
compression = im.info.get('compression','raw')
compression = im.encoderinfo.get('compression',im.info.get('compression','raw'))
libtiff = compression in ["tiff_ccitt", "group3", "group4",
"tiff_jpeg", "tiff_adobe_deflate",
"tiff_thunderscan", "tiff_deflate",

View File

@ -12,6 +12,7 @@ _VALID_WEBP_MODES = {
_VP8_MODES_BY_IDENTIFIER = {
b"VP8 ": "RGB",
b"VP8X": "RGBA",
b"VP8L": "RGBA", # lossless
}
@ -48,6 +49,7 @@ def _save(im, fp, filename):
if im.mode not in _VALID_WEBP_MODES:
raise IOError("cannot write mode %s as WEBP" % image_mode)
lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80)
icc_profile = im.encoderinfo.get("icc_profile", "")
exif = im.encoderinfo.get("exif", "")
@ -56,6 +58,7 @@ def _save(im, fp, filename):
im.tobytes(),
im.size[0],
im.size[1],
lossless,
float(quality),
im.mode,
icc_profile,

View File

@ -0,0 +1,30 @@
#!/usr/bin/gnuplot
#This is the script that was used to create our sample EPS files
#We used the following version of the gnuplot program
#G N U P L O T
#Version 4.6 patchlevel 3 last modified 2013-04-12
#Build System: Darwin x86_64
#This file will generate the non_zero_bb.eps variant, in order to get the
#zero_bb.eps variant you will need to edit line6 in the result file to
#be "%%BoundingBox: 0 0 460 352" instead of "%%BoundingBox: 50 50 410 302"
set t postscript eps color
set o "sample.eps"
set dummy u,v
set key bmargin center horizontal Right noreverse enhanced autotitles nobox
set parametric
set view 50, 30, 1, 1
set isosamples 10, 10
set hidden3d back offset 1 trianglepattern 3 undefined 1 altdiagonal bentover
set ticslevel 0
set title "Interlocking Tori"
set style line 1 lt 1 lw 1 pt 3 lc rgb "red"
set style line 2 lt 1 lw 1 pt 3 lc rgb "blue"
set urange [ -3.14159 : 3.14159 ] noreverse nowriteback
set vrange [ -3.14159 : 3.14159 ] noreverse nowriteback
splot cos(u)+.5*cos(u)*cos(v),sin(u)+.5*sin(u)*cos(v),.5*sin(v) ls 1,\
1+cos(u)+.5*cos(u)*cos(v),.5*sin(v),sin(u)+.5*sin(u)*cos(v) ls 2

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
Tests/images/zero_bb.eps Normal file

Binary file not shown.

BIN
Tests/images/zero_bb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

82
Tests/test_file_eps.py Normal file
View File

@ -0,0 +1,82 @@
from tester import *
from PIL import Image
#Our two EPS test files (they are identical except for their bounding boxes)
file1 = "Tests/images/zero_bb.eps"
file2 = "Tests/images/non_zero_bb.eps"
#Due to palletization, we'll need to convert these to RGB after load
file1_compare = "Tests/images/zero_bb.png"
file1_compare_scale2 = "Tests/images/zero_bb_scale2.png"
file2_compare = "Tests/images/non_zero_bb.png"
file2_compare_scale2 = "Tests/images/non_zero_bb_scale2.png"
def test_sanity():
#Regular scale
image1 = Image.open(file1)
image1.load()
assert_equal(image1.mode, "RGB")
assert_equal(image1.size, (460, 352))
assert_equal(image1.format, "EPS")
image2 = Image.open(file2)
image2.load()
assert_equal(image2.mode, "RGB")
assert_equal(image2.size, (360, 252))
assert_equal(image2.format, "EPS")
#Double scale
image1_scale2 = Image.open(file1)
image1_scale2.load(scale=2)
assert_equal(image1_scale2.mode, "RGB")
assert_equal(image1_scale2.size, (920, 704))
assert_equal(image1_scale2.format, "EPS")
image2_scale2 = Image.open(file2)
image2_scale2.load(scale=2)
assert_equal(image2_scale2.mode, "RGB")
assert_equal(image2_scale2.size, (720, 504))
assert_equal(image2_scale2.format, "EPS")
def test_render_scale1():
#We need png support for these render test
codecs = dir(Image.core)
if "zip_encoder" not in codecs or "zip_decoder" not in codecs:
skip("zip/deflate support not available")
#Zero bounding box
image1_scale1 = Image.open(file1)
image1_scale1.load()
image1_scale1_compare = Image.open(file1_compare).convert("RGB")
image1_scale1_compare.load()
assert_image_similar(image1_scale1, image1_scale1_compare, 5)
#Non-Zero bounding box
image2_scale1 = Image.open(file2)
image2_scale1.load()
image2_scale1_compare = Image.open(file2_compare).convert("RGB")
image2_scale1_compare.load()
assert_image_similar(image2_scale1, image2_scale1_compare, 10)
def test_render_scale2():
#We need png support for these render test
codecs = dir(Image.core)
if "zip_encoder" not in codecs or "zip_decoder" not in codecs:
skip("zip/deflate support not available")
#Zero bounding box
image1_scale2 = Image.open(file1)
image1_scale2.load(scale=2)
image1_scale2_compare = Image.open(file1_compare_scale2).convert("RGB")
image1_scale2_compare.load()
assert_image_similar(image1_scale2, image1_scale2_compare, 5)
#Non-Zero bounding box
image2_scale2 = Image.open(file2)
image2_scale2.load(scale=2)
image2_scale2_compare = Image.open(file2_compare_scale2).convert("RGB")
image2_scale2_compare.load()
assert_image_similar(image2_scale2, image2_scale2_compare, 10)

View File

@ -27,3 +27,22 @@ def test_optimize():
return len(file.getvalue())
assert_equal(test(0), 800)
assert_equal(test(1), 38)
def test_roundtrip():
out = tempfile('temp.gif')
im = lena()
im.save(out)
reread = Image.open(out)
assert_image_similar(reread.convert('RGB'), im, 50)
def test_roundtrip2():
#see https://github.com/python-imaging/Pillow/issues/403
out = 'temp.gif'#tempfile('temp.gif')
im = Image.open('Images/lena.gif')
im2 = im.copy()
im2.save(out)
reread = Image.open(out)
assert_image_similar(reread.convert('RGB'), lena(), 50)

View File

@ -92,6 +92,7 @@ def test_g4_write():
assert_equal(reread.size,(500,500))
_assert_noerr(reread)
assert_image_equal(reread, rot)
assert_equal(reread.info['compression'], 'group4')
assert_equal(reread.info['compression'], orig.info['compression'])
@ -106,7 +107,6 @@ def test_adobe_deflate_tiff():
assert_equal(im.tile[0][:3], ('tiff_adobe_deflate', (0, 0, 278, 374), 0))
assert_no_exception(lambda: im.load())
def test_write_metadata():
""" Test metadata writing through libtiff """
img = Image.open('Tests/images/lena_g4.tif')
@ -131,6 +131,15 @@ def test_write_metadata():
assert_equal(value, reloaded[tag], "%s didn't roundtrip" % tag)
def test_g3_compression():
i = Image.open('Tests/images/lena_g4_500.tif')
out = tempfile("temp.tif")
i.save(out, compression='group3')
reread = Image.open(out)
assert_equal(reread.info['compression'], 'group3')
assert_image_equal(reread, i)
def test_little_endian():
im = Image.open('Tests/images/12bit.deflate.tif')
assert_equal(im.getpixel((0,0)), 480)

View File

@ -66,6 +66,26 @@ def test_write_rgb():
assert_image_similar(image, target, 20.0)
def test_write_lossless_rgb():
temp_file = tempfile("temp.webp")
lena("RGB").save(temp_file, lossless=True)
image = Image.open(temp_file)
image.load()
assert_equal(image.mode, "RGB")
assert_equal(image.size, (128, 128))
assert_equal(image.format, "WEBP")
assert_no_exception(lambda: image.load())
assert_no_exception(lambda: image.getdata())
assert_image_equal(image, lena("RGB"))
def test_write_rgba():
"""
Can we write a RGBA mode file to webp without error. Does it have the bits we
@ -111,3 +131,27 @@ def test_read_rgba():
target = Image.open('Images/transparent.png')
assert_image_similar(image, target, 20.0)
def test_write_lossless_rgb():
temp_file = tempfile("temp.webp")
#temp_file = "temp.webp"
pil_image = lena('RGBA')
mask = Image.new("RGBA", (64, 64), (128,128,128,128))
pil_image.paste(mask, (0,0), mask) # add some partially transparent bits.
pil_image.save(temp_file, lossless=True)
image = Image.open(temp_file)
image.load()
assert_equal(image.mode, "RGBA")
assert_equal(image.size, pil_image.size)
assert_equal(image.format, "WEBP")
assert_no_exception(lambda: image.load())
assert_no_exception(lambda: image.getdata())
assert_image_equal(image, pil_image)

View File

@ -59,7 +59,11 @@ struct {
const char* message;
} ft_errors[] =
#if defined(USE_FREETYPE_2_1)
#include FT_ERRORS_H
#else
#include <freetype/fterrors.h>
#endif
/* -------------------------------------------------------------------- */
/* font objects */

50
_webp.c
View File

@ -13,6 +13,7 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
{
int width;
int height;
int lossless;
float quality_factor;
uint8_t *rgb;
uint8_t *icc_bytes;
@ -20,29 +21,36 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
uint8_t *output;
char *mode;
Py_ssize_t size;
Py_ssize_t icc_size;
Py_ssize_t icc_size;
Py_ssize_t exif_size;
size_t ret_size;
if (!PyArg_ParseTuple(args, "s#iifss#s#",
(char**)&rgb, &size, &width, &height, &quality_factor, &mode,
if (!PyArg_ParseTuple(args, "s#iiifss#s#",
(char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode,
&icc_bytes, &icc_size, &exif_bytes, &exif_size)) {
Py_RETURN_NONE;
}
if (strcmp(mode, "RGBA")==0){
if (size < width * height * 4){
Py_RETURN_NONE;
}
ret_size = WebPEncodeRGBA(rgb, width, height, 4* width, quality_factor, &output);
} else if (strcmp(mode, "RGB")==0){
if (size < width * height * 3){
Py_RETURN_NONE;
}
ret_size = WebPEncodeRGB(rgb, width, height, 3* width, quality_factor, &output);
} else {
Py_RETURN_NONE;
}
if (strcmp(mode, "RGBA")==0){
if (size < width * height * 4){
Py_RETURN_NONE;
}
if (lossless) {
ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4* width, &output);
} else {
ret_size = WebPEncodeRGBA(rgb, width, height, 4* width, quality_factor, &output);
}
} else if (strcmp(mode, "RGB")==0){
if (size < width * height * 3){
Py_RETURN_NONE;
}
if (lossless) {
ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3* width, &output);
} else {
ret_size = WebPEncodeRGB(rgb, width, height, 3* width, quality_factor, &output);
}
} else {
Py_RETURN_NONE;
}
#ifndef HAVE_WEBPMUX
if (ret_size > 0) {
@ -53,10 +61,10 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
#else
{
/* I want to truncate the *_size items that get passed into webp
data. Pypy2.1.0 had some issues where the Py_ssize_t items had
data. Pypy2.1.0 had some issues where the Py_ssize_t items had
data in the upper byte. (Not sure why, it shouldn't have been there)
*/
int i_icc_size = (int)icc_size;
int i_icc_size = (int)icc_size;
int i_exif_size = (int)exif_size;
WebPData output_data = {0};
WebPData image = { output, ret_size };
@ -105,11 +113,11 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
WebPMuxAssemble(mux, &output_data);
WebPMuxDelete(mux);
free(output);
output = (uint8_t*)output_data.bytes;
ret_size = output_data.size;
if (ret_size > 0) {
PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size);
PyObject *ret = PyBytes_FromStringAndSize((char*)output_data.bytes, ret_size);
WebPDataClear(&output_data);
return ret;
}

View File

@ -36,6 +36,20 @@ PIL identifies EPS files containing image data, and can read files that contain
embedded raster images (ImageData descriptors). If Ghostscript is available,
other EPS files can be read as well. The EPS driver can also write EPS images.
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
method with the following parameter to affect how Ghostscript renders the EPS
**scale**
Affects the scale of the resultant rasterized image. If the EPS suggests
that the image be rendered at 100px x 100px, setting this parameter to
2 will make the Ghostscript render a 200px x 200px image instead. The
relative position of the bounding box is maintained::
im = Image.open(...)
im.size #(100,100)
im.load(scale=2)
im.size #(200,200)
GIF
^^^
@ -322,6 +336,24 @@ WebP
PIL reads and writes WebP files. The specifics of PIL's capabilities with this
format are currently undocumented.
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**lossless**
If present, instructs the WEBP writer to use lossless
compression.
**quality**
Integer, 1-100, Defaults to 80. Sets the quality level for
lossy compression.
**icc_procfile**
The ICC Profile to include in the saved file. Only supported if
the system webp library was built with webpmux support.
**exif**
The exif data to include in the saved file. Only supported if
the system webp library was built with webpmux support.
XBM
^^^

View File

@ -1,10 +1,13 @@
Writing your own file decoder
=============================
The Python Imaging Library uses a plug-in model which allows you to add your
own decoders to the library, without any changes to the library itself. Such
plug-ins have names like :file:`XxxImagePlugin.py`, where ``Xxx`` is a unique
format name (usually an abbreviation).
The Python Imaging Library uses a plug-in model which allows you to
add your own decoders to the library, without any changes to the
library itself. Such plug-ins usually have names like
:file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name
(usually an abbreviation).
.. warning:: Pillow >= 2.1.0 no longer automatically imports any file in the Python path with a name ending in :file:`ImagePlugin.py`. You will need to import your decoder manually.
A decoder plug-in should contain a decoder class, based on the
:py:class:`PIL.ImageFile.ImageFile` base class. This class should provide an

View File

@ -15,3 +15,9 @@ to this::
The :py:mod:`_imaging` module has been moved. You can now import it like this::
from PIL.Image import core as _imaging
The image plugin loading mechanisim has changed. Pillow no longer
automatically imports any file in the Python path with a name ending
in :file:`ImagePlugin.py`. You will need to import your image plugin
manually.

View File

@ -211,6 +211,10 @@ class pil_build_ext(build_ext):
# work ;-)
self.add_multiarch_paths()
elif sys.platform.startswith("netbsd"):
_add_directory(library_dirs, "/usr/pkg/lib")
_add_directory(include_dirs, "/usr/pkg/include")
_add_directory(library_dirs, "/usr/local/lib")
# FIXME: check /opt/stuff directories here?