Skip to content

Commit

Permalink
Accept a PIL Image as input to draw_image()
Browse files Browse the repository at this point in the history
  • Loading branch information
jwiggins committed Feb 16, 2021
1 parent cbaf6b0 commit eab208f
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 134 deletions.
24 changes: 9 additions & 15 deletions kiva/cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,13 +844,10 @@ def set_alpha(self, alpha):

def draw_image(self, img, rect=None):
"""
img is either a N*M*3 or N*M*4 numpy array, or a Kiva image
rect - what is this? assume it's a tuple (x,y, w, h)
Only works with numpy arrays. What is a "Kiva Image" anyway?
Not Yet Tested.
`img` is either a N*M*3 or N*M*4 numpy array, or a PIL Image
`rect` is a tuple (x, y, w, h)
"""
from kiva import agg
from PIL import Image

if isinstance(img, numpy.ndarray):
# Numeric array
Expand All @@ -862,18 +859,15 @@ def draw_image(self, img, rect=None):
img_surface = cairo.ImageSurface.create_for_data(
img.astype(numpy.uint8), format, img_width, img_height
)
elif isinstance(img, agg.GraphicsContextArray):
converted_img = img.convert_pixel_format("rgba32", inplace=0)
flipped_array = numpy.flipud(converted_img.bmp_array)
img_width, img_height = (
converted_img.width(),
converted_img.height(),
)
elif isinstance(img, Image.Image):
converted_img = img.convert("RGBA")
flipped_array = numpy.flipud(numpy.array(converted_img))
img_width, img_height = img.width, img.height
img_surface = cairo.ImageSurface.create_for_data(
flipped_array.flatten(),
cairo.FORMAT_RGB24,
img_width,
img_height,
img.width,
img.height,
)
elif isinstance(img, GraphicsContext):
# Another cairo kiva context
Expand Down
35 changes: 22 additions & 13 deletions kiva/celiagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,31 +545,40 @@ def radial_gradient(self, cx, cy, r, fx, fy, stops, spread_method,

def draw_image(self, img, rect=None):
"""
img is either a N*M*3 or N*M*4 numpy array, or a Kiva image
img is either a N*M*3 or N*M*4 numpy array, or a PIL Image
rect - a tuple (x, y, w, h)
"""
from PIL import Image

def get_format(array):
if array.shape[2] == 3:
return agg.PixelFormat.RGB24
elif array.shape[2] == 4:
return agg.PixelFormat.RGBA32
def normalize_image(img):
if not img.mode.startswith('RGB'):
img = img.convert('RGB')

if img.mode == 'RGB':
return img, agg.PixelFormat.RGB24
elif img.mode == 'RGBA':
return img, agg.PixelFormat.RGBA32

img_format = agg.PixelFormat.RGB24
if isinstance(img, np.ndarray):
# Numeric array
img_array = img.astype(np.uint8)
img_format = get_format(img_array)
elif isinstance(img, GraphicsContext):
img_array = img.gc.array
img_format = pix_formats[img.pix_format]
img = Image.fromarray(img)
img, img_format = normalize_image(img)
img_array = np.array(img)
elif isinstance(img, Image.Image):
img, img_format = normalize_image(img)
img_array = np.array(img)
elif hasattr(img, 'bmp_array'):
# An offscreen kiva context
# XXX: Use a copy to kill the read-only flag which plays havoc
# with the Cython memoryviews used by celiagg
img_array = img.bmp_array.copy()
img_format = get_format(img_array)
img = Image.fromarray(img.bmp_array)
img, img_format = normalize_image(img)
img_array = np.array(img)
elif isinstance(img, GraphicsContext):
img_array = img.gc.array
img_format = pix_formats[img.pix_format]
else:
msg = "Cannot render image of type '{}' into celiagg context."
warnings.warn(msg.format(type(img)))
Expand Down
13 changes: 11 additions & 2 deletions kiva/gl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,24 @@ def image_as_array(img):
Typically, this is used to adapt an agg GraphicsContextArray which has been
used for image storage in Kiva applications.
"""
from PIL import Image

if hasattr(img, "bmp_array"):
# Yup, a GraphicsContextArray.
return img.bmp_array
img = Image.fromarray(img.bmp_array)
elif isinstance(img, ndarray):
return img
img = Image.fromarray(img)
elif isinstance(img, Image.Image):
pass
else:
msg = "can't convert %r into a numpy array" % (img,)
raise NotImplementedError(msg)

# Ensure RGB or RGBA formats
if not img.mode.startswith("RGB"):
img = img.convert("RGB")
return array(img)


def get_dpi():
""" Returns the appropriate DPI setting for the system"""
Expand Down
22 changes: 7 additions & 15 deletions kiva/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,7 @@ def draw_image(self, img, rect=None):
pixel size. If 'rect' is provided, then the image is resized
into the (w, h) given and drawn into this GC at point (x, y).
img_gc is either a Numeric array (WxHx3 or WxHx4) or a GC from Kiva's
Agg backend (kiva.agg.GraphicsContextArray).
img_gc is either a Numeric array (WxHx3 or WxHx4) or a PIL Image.
Requires the Python Imaging Library (PIL).
"""
Expand All @@ -579,22 +578,15 @@ def draw_image(self, img, rect=None):
# it brute-force using Agg.
from reportlab.lib.utils import ImageReader
from PIL import Image
from kiva import agg

if isinstance(img, ndarray):
# Conversion from numpy array
pil_img = Image.fromarray(img, "RGBA")
elif isinstance(img, agg.GraphicsContextArray):
converted_img = img
if img.format().startswith("RGBA"):
pilformat = "RGBA"
elif img.format().startswith("RGB"):
pilformat = "RGB"
else:
converted_img = img.convert_pixel_format("rgba32", inplace=0)
pilformat = "RGBA"
# Conversion from GraphicsContextArray
pil_img = Image.fromarray(converted_img.bmp_array, pilformat)
pil_img = Image.fromarray(img)
elif isinstance(img, Image.Image):
pil_img = img
elif hasattr(img, "bmp_array"):
# An offscreen kiva agg context
pil_img = Image.fromarray(img.bmp_array)
else:
warnings.warn(
"Cannot render image of type %r into PDF context." % type(img)
Expand Down
34 changes: 12 additions & 22 deletions kiva/ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from numpy import arange, ndarray, ravel

# Local, relative Kiva imports
from kiva import agg
from . import affine
from . import basecore2d
from . import constants
Expand Down Expand Up @@ -186,30 +185,23 @@ def device_draw_image(self, img, rect):
pixel size. If 'rect' is provided, then the image is resized
into the (w,h) given and drawn into this GC at point (x,y).
img_gc is either a Numeric array (WxHx3 or WxHx4) or a GC from Kiva's
Agg backend (kiva.agg.GraphicsContextArray).
img_gc is either a Numeric array (WxHx3 or WxHx4) or a PIL Image.
Requires the Python Imaging Library (PIL).
"""
from PIL import Image as PilImage
from PIL import Image

if isinstance(img, ndarray):
# From numpy array
pilformat = "RGBA"
pil_img = PilImage.fromarray(img, pilformat)
elif isinstance(img, agg.GraphicsContextArray):
converted_img = img
if img.format().startswith("RGBA"):
pilformat = "RGBA"
elif img.format().startswith("RGB"):
pilformat = "RGB"
else:
converted_img = img.convert_pixel_format("rgba32", inplace=0)
pilformat = "RGBA"
# Should probably take this into account
# interp = img.get_image_interpolation()
# Conversion from GraphicsContextArray
pil_img = PilImage.fromarray(converted_img.bmp_array, pilformat)
pil_img = Image.fromarray(img)
pilformat = pil_img.mode
elif isinstance(img, Image.Image):
pil_img = img
pilformat = img.mode
elif hasattr(img, "bmp_array"):
# An offscreen kiva agg context
pil_img = Image.fromarray(img.bmp_array)
pilformat = img.mode
else:
warnings.warn(
"Cannot render image of type %r into EPS context." % type(img)
Expand All @@ -226,9 +218,7 @@ def device_draw_image(self, img, rect):
left, top, width, height = rect
if width != pil_img.width or height != pil_img.height:
# This is not strictly required.
pil_img = pil_img.resize(
(int(width), int(height)), PilImage.NEAREST
)
pil_img = pil_img.resize((int(width), int(height)), Image.NEAREST)

self.contents.write("gsave\n")
self.contents.write("initmatrix\n")
Expand Down
49 changes: 14 additions & 35 deletions kiva/qpainter.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,48 +553,27 @@ def radial_gradient(self, cx, cy, r, fx, fy, stops, spread_method,

def draw_image(self, img, rect=None):
"""
img is either a N*M*3 or N*M*4 numpy array, or a Kiva image
img is either a N*M*3 or N*M*4 numpy array, or a PIL Image
rect - a tuple (x, y, w, h)
"""
from kiva import agg

def copy_padded(array):
""" Pad image width to a multiple of 4 pixels, and minimum dims of
12x12. QImage is very particular about its data.
"""
y, x, d = array.shape
pad = (lambda v: (4 - (v % 4)) % 4)
nx = max(x + pad(x), 12)
ny = max(y, 12)
if x == nx and y == ny:
return array
ret = np.zeros((ny, nx, d), dtype=np.uint8)
ret[:y, :x] = array[:]
return ret
from PIL import Image, ImageQt

if isinstance(img, np.ndarray):
# Numeric array
if img.shape[2] == 3:
format = QtGui.QImage.Format_RGB888
elif img.shape[2] == 4:
format = QtGui.QImage.Format_RGB32
width, height = img.shape[:2]
copy_array = copy_padded(img)
draw_img = QtGui.QImage(
img.astype(np.uint8), copy_array.shape[1], height, format
)
pilimg = Image.fromarray(img)
width, height = pilimg.width, pilimg.height
draw_img = ImageQt.ImageQt(pilimg)
pixmap = QtGui.QPixmap.fromImage(draw_img)
elif isinstance(img, agg.GraphicsContextArray):
converted_img = img.convert_pixel_format("bgra32", inplace=0)
copy_array = copy_padded(converted_img.bmp_array)
width, height = img.width(), img.height()
draw_img = QtGui.QImage(
copy_array.flatten(),
copy_array.shape[1],
height,
QtGui.QImage.Format_RGB32,
)
elif isinstance(img, Image.Image):
width, height = img.width, img.height
draw_img = ImageQt.ImageQt(img)
pixmap = QtGui.QPixmap.fromImage(draw_img)
elif hasattr(img, "bmp_array"):
# An offscreen kiva agg context
pilimg = Image.fromarray(img.bmp_array)
width, height = pilimg.width, pilimg.height
draw_img = ImageQt.ImageQt(pilimg)
pixmap = QtGui.QPixmap.fromImage(draw_img)
elif (isinstance(img, GraphicsContext)
and isinstance(img.qt_dc, QtGui.QPixmap)
Expand Down
4 changes: 4 additions & 0 deletions kiva/quartz/ABCGI.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -732,10 +732,14 @@ cdef class CGContext:
def draw_image(self, object image, object rect=None):
""" Draw an image or another CGContext onto a region.
"""
from PIL import Image

if rect is None:
rect = (0, 0, self.width(), self.height())
if isinstance(image, numpy.ndarray):
self._draw_cgimage(CGImage(image), rect)
elif isinstance(image, Image.Image):
self._draw_cgimage(CGImage(numpy.array(image)), rect)
elif isinstance(image, CGImage):
self._draw_cgimage(image, rect)
elif hasattr(image, 'bmp_array'):
Expand Down
37 changes: 10 additions & 27 deletions kiva/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
from numpy import arange, ndarray, ravel

# Local, relative Kiva imports
from kiva import agg
from . import affine
from . import basecore2d
from . import constants
Expand Down Expand Up @@ -237,36 +236,22 @@ def device_draw_image(self, img, rect):
pixel size. If 'rect' is provided, then the image is resized
into the (w,h) given and drawn into this GC at point (x,y).
img_gc is either a Numeric array (WxHx3 or WxHx4) or a GC from Kiva's
Agg backend (kiva.agg.GraphicsContextArray).
img_gc is either a Numeric array (WxHx3 or WxHx4) or a PIL Image.
Requires the Python Imaging Library (PIL).
"""
from PIL import Image as PilImage
from PIL import Image

# We turn img into a PIL object, since that is what ReportLab
# requires. To do this, we first determine if the input image
# GC needs to be converted to RGBA/RGB. If so, we see if we can
# do it nicely (using convert_pixel_format), and if not, we do
# it brute-force using Agg.

# requires.
if isinstance(img, ndarray):
# From numpy array
pilformat = "RGBA"
pil_img = PilImage.fromarray(img, pilformat)
elif isinstance(img, agg.GraphicsContextArray):
converted_img = img
if img.format().startswith("RGBA"):
pilformat = "RGBA"
elif img.format().startswith("RGB"):
pilformat = "RGB"
else:
converted_img = img.convert_pixel_format("rgba32", inplace=0)
pilformat = "RGBA"
# Should probably take this into account
# interp = img.get_image_interpolation()
# Conversion from GraphicsContextArray
pil_img = PilImage.fromarray(converted_img.bmp_array, pilformat)
pil_img = Image.fromarray(img)
elif isinstance(img, Image.Image):
pil_img = img
elif hasattr(img, "bmp_array"):
# An offscreen kiva agg context
pil_img = Image.fromarray(img.bmp_array)
else:
warnings.warn(
"Cannot render image of type %r into SVG context." % type(img)
Expand All @@ -279,9 +264,7 @@ def device_draw_image(self, img, rect):
left, top, width, height = rect
if width != pil_img.width or height != pil_img.height:
# This is not strictly required.
pil_img = pil_img.resize(
(int(width), int(height)), PilImage.NEAREST
)
pil_img = pil_img.resize((int(width), int(height)), Image.NEAREST)

png_buffer = BytesIO()
pil_img.save(png_buffer, "png")
Expand Down
5 changes: 0 additions & 5 deletions kiva/tests/test_qpainter_drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@ def create_graphics_context(self, width, height):

return GraphicsContext((width, height))

@unittest.expectedFailure
def test_image(self):
""" QPainter interprets images as BGRA. """
super().test_image()

@unittest.skipIf(is_qt5 and is_linux, "Currently segfaulting")
def test_text(self):
super().test_text()
Expand Down

0 comments on commit eab208f

Please sign in to comment.