diff --git a/kiva/agg/src/graphics_context.i b/kiva/agg/src/graphics_context.i index 5b7087bb3..7c89aaac5 100644 --- a/kiva/agg/src/graphics_context.i +++ b/kiva/agg/src/graphics_context.i @@ -833,6 +833,40 @@ namespace kiva { def get_empty_path(self): return CompiledPath() + def to_image(self): + """ Return the contents of the GraphicsContext as a PIL Image. + + Images are in RGB or RGBA format; if this GC is not in one of + these formats, it is automatically converted. + + Returns + ------- + img : Image + The contents of the context as a PIL/Pillow Image. + """ + from PIL import Image + size = (self.width(), self.height()) + fmt = self.format() + + # determine the output pixel format and PIL format + if fmt.endswith("32"): + pilformat = "RGBA" + pixelformat = "rgba32" + elif fmt.endswith("24"): + pilformat = "RGB" + pixelformat = "rgb24" + + # perform a conversion if necessary + if fmt != pixelformat: + newimg = GraphicsContextArray(size, fmt) + newimg.draw_image(self) + newimg.convert_pixel_format(pixelformat, 1) + bmp = newimg.bmp_array + else: + bmp = self.bmp_array + + return Image.fromarray(bmp, pilformat) + def save(self, filename, file_format=None, pil_options=None): """ Save the GraphicsContext to a file. Output files are always saved in RGB or RGBA format; if this GC is not in one of @@ -907,6 +941,29 @@ namespace kiva { def __exit__(self, type, value, traceback): self.restore_state() + #---------------------------------------------------------------- + # IPython/Jupyter support + #---------------------------------------------------------------- + + def _repr_png_(self): + """ Return a the current contents of the context as PNG image. + + This provides Jupyter and IPython compatibility, so that the graphics + context can be displayed in the Jupyter Notebook or the IPython Qt + console. + + Returns + ------- + data : bytes + The contents of the context as PNG-format bytes. + """ + from io import BytesIO + + img = self.to_image() + data = BytesIO() + img.save(data, format='png') + return data.getvalue() + %} //--------------------------------------------------------------------- diff --git a/kiva/celiagg.py b/kiva/celiagg.py index 63e8c8f7c..08a0421ea 100644 --- a/kiva/celiagg.py +++ b/kiva/celiagg.py @@ -8,6 +8,7 @@ # # Thanks for using Enthought open source! from collections import namedtuple +from io import BytesIO from math import fabs import os import sys @@ -828,32 +829,19 @@ def draw_path_at_points(self, points, path, mode=constants.FILL_STROKE): def save(self, filename, file_format=None, pil_options=None): """ Save the contents of the context to a file """ - try: - from PIL import Image - except ImportError: - raise ImportError("need Pillow to save images") if file_format is None: file_format = '' if pil_options is None: pil_options = {} - pixels = self.gc.array - if self.pix_format.startswith('bgra'): - # Data is BGRA; Convert to RGBA - data = np.empty(pixels.shape, dtype=np.uint8) - data[..., 0] = pixels[..., 2] - data[..., 1] = pixels[..., 1] - data[..., 2] = pixels[..., 0] - data[..., 3] = pixels[..., 3] - else: - data = pixels - img = Image.fromarray(data, 'RGBA') + img = self.to_image() ext = ( os.path.splitext(filename)[1][1:] if isinstance(filename, str) else '' ) + # Check the output format to see if it can handle an alpha channel. no_alpha_formats = ('jpg', 'bmp', 'eps', 'jpeg') if ext in no_alpha_formats or file_format.lower() in no_alpha_formats: @@ -868,6 +856,52 @@ def save(self, filename, file_format=None, pil_options=None): img.save(filename, format=file_format, **pil_options) + def to_image(self): + """ Return the contents of the context as a PIL Image. + + If the graphics context is in BGRA format, it will convert it to + RGBA for the image. + + Returns + ------- + img : Image + A PIL/Pillow Image object with the data in RGBA format. + """ + try: + from PIL import Image + except ImportError: + raise ImportError("need Pillow to save images") + + pixels = self.gc.array + if self.pix_format.startswith('bgra'): + # Data is BGRA; Convert to RGBA + data = np.empty(pixels.shape, dtype=np.uint8) + data[..., 0] = pixels[..., 2] + data[..., 1] = pixels[..., 1] + data[..., 2] = pixels[..., 0] + data[..., 3] = pixels[..., 3] + else: + data = pixels + + return Image.fromarray(data, 'RGBA') + + def _repr_png_(self): + """ Return a the current contents of the context as PNG image. + + This provides Jupyter and IPython compatibility, so that the graphics + context can be displayed in the Jupyter Notebook or the IPython Qt + console. + + Returns + ------- + data : bytes + The contents of the context as PNG-format bytes. + """ + img = self.to_image() + data = BytesIO() + img.save(data, format='png') + return data.getvalue() + class CompiledPath(object): def __init__(self): diff --git a/kiva/examples/kiva/Kiva Explorer.ipynb b/kiva/examples/kiva/Kiva Explorer.ipynb new file mode 100644 index 000000000..38bc5b3db --- /dev/null +++ b/kiva/examples/kiva/Kiva Explorer.ipynb @@ -0,0 +1,85 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Kiva API Explorer\n", + "\n", + "This notebook allows you to experiment with the K" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import pi\n", + "from kiva import constants\n", + "from kiva.fonttools import Font" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from kiva.celiagg import GraphicsContext\n", + "\n", + "gc = GraphicsContext((500, 500), pix_format='rgba32')\n", + "\n", + "with gc:\n", + " gc.set_fill_color((1.0, 1.0, 0.0, 1.0))\n", + " gc.arc(200, 200, 100, 0, 2*pi)\n", + " gc.fill_path()\n", + "\n", + " with gc:\n", + " gc.set_font(Font('Times New Roman', size=24))\n", + " gc.translate_ctm(200, 200)\n", + " for i in range(0, 12):\n", + " gc.set_fill_color((i/12.0, 0.0, 1.0-(i/12.0), 0.75))\n", + " gc.rotate_ctm(2*pi/12.0)\n", + " gc.show_text_at_point(\"Hello World\", 20, 0)\n", + "\n", + " gc.set_stroke_color((0.0, 0.0, 1.0, 1.0))\n", + " gc.set_line_width(7)\n", + " gc.set_line_join(constants.JOIN_ROUND)\n", + " gc.set_line_cap(constants.CAP_ROUND)\n", + " gc.rect(100, 400, 50, 50)\n", + " gc.stroke_path()\n", + "\n", + "gc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/kiva/svg.py b/kiva/svg.py index 70f93971c..5183e85c6 100644 --- a/kiva/svg.py +++ b/kiva/svg.py @@ -463,6 +463,20 @@ def device_update_line_state(self): def device_update_fill_state(self): pass + def _repr_svg_(self): + """ Return a the current contents of the context as SVG text. + + This provides Jupyter and IPython compatibility, so that the graphics + context can be displayed in the Jupyter Notebook or the IPython Qt + console. + + Returns + ------- + svg : str + The contents of the context as an SVG string. + """ + return self.render('svg') + def font_metrics_provider(): return GraphicsContext((1, 1)) diff --git a/kiva/tests/test_agg_drawing.py b/kiva/tests/test_agg_drawing.py index e052431c0..067f551c7 100644 --- a/kiva/tests/test_agg_drawing.py +++ b/kiva/tests/test_agg_drawing.py @@ -36,3 +36,13 @@ def test_unicode_gradient_args(self): 0, 0, w, 0, grad_stops, "pad", b"userSpaceOnUse" ) self.gc.fill_path() + + def test_ipython_repr_png(self): + self.gc.begin_path() + self.gc.rect(75, 75, 25, 25) + self.gc.fill_path() + stream = self.gc._repr_png_() + filename = "{0}.png".format(self.filename) + with open(filename, 'wb') as fp: + fp.write(stream) + self.assertImageSavedWithContent(filename) diff --git a/kiva/tests/test_celiagg_drawing.py b/kiva/tests/test_celiagg_drawing.py index 78ae464ab..559566107 100644 --- a/kiva/tests/test_celiagg_drawing.py +++ b/kiva/tests/test_celiagg_drawing.py @@ -27,3 +27,13 @@ def test_clip_rect_transform(self): self.gc.begin_path() self.gc.rect(75, 75, 25, 25) self.gc.fill_path() + + def test_ipython_repr_png(self): + self.gc.begin_path() + self.gc.rect(75, 75, 25, 25) + self.gc.fill_path() + stream = self.gc._repr_png_() + filename = "{0}.png".format(self.filename) + with open(filename, 'wb') as fp: + fp.write(stream) + self.assertImageSavedWithContent(filename) diff --git a/kiva/tests/test_svg_drawing.py b/kiva/tests/test_svg_drawing.py index 71b0c0dbf..3a5806a45 100644 --- a/kiva/tests/test_svg_drawing.py +++ b/kiva/tests/test_svg_drawing.py @@ -28,3 +28,16 @@ def draw_and_check(self): elements = [element for element in tree.iter()] if not len(elements) in [4, 7]: self.fail("The expected number of elements was not found") + + def test_ipython_repr_svg(self): + self.gc.begin_path() + self.gc.rect(75, 75, 25, 25) + self.gc.fill_path() + stream = self.gc._repr_svg_() + filename = "{0}.svg".format(self.filename) + with open(filename, 'w', encoding='utf8') as fp: + fp.write(stream) + tree = ElementTree.parse(filename) + elements = [element for element in tree.iter()] + if not len(elements) in [4, 7]: + self.fail("The expected number of elements was not found")