diff --git a/kiva/agg/src/graphics_context.i b/kiva/agg/src/graphics_context.i index fcebdc1f6..6556cafdc 100644 --- a/kiva/agg/src/graphics_context.i +++ b/kiva/agg/src/graphics_context.i @@ -370,6 +370,7 @@ namespace kiva { _swig_setattr(self, GraphicsContextArray, 'thisown2', 1) self.bmp_array = ary + self.base_scale = base_pixel_scale def __del__(self, destroy=_agg.destroy_graphics_context): try: @@ -839,15 +840,27 @@ namespace kiva { """ from PIL import Image + FmtsWithDpi = ('jpg', 'png', 'tiff', 'jpeg') FmtsWithoutAlpha = ('jpg', 'bmp', 'eps', "jpeg") size = (self.width(), self.height()) fmt = self.format() + if pil_options is None: + pil_options = {} + + file_ext = filename.rpartition(".")[-1].lower() if isinstance(filename, str) else "" + if (file_ext in FmtsWithDpi or + (file_format is not None and + file_format.lower() in FmtsWithDpi)): + # Assume 72dpi is 1x + dpi = int(72 * self.base_scale) + pil_options["dpi"] = (dpi, dpi) + # determine the output pixel format and PIL format if fmt.endswith("32"): pilformat = "RGBA" pixelformat = "rgba32" - if (isinstance(filename, str) and filename[-3:].lower() in FmtsWithoutAlpha) or \ + if file_ext in FmtsWithoutAlpha or \ (file_format is not None and file_format.lower() in FmtsWithoutAlpha): pilformat = "RGB" pixelformat = "rgb24" @@ -865,7 +878,7 @@ namespace kiva { bmp = self.bmp_array img = Image.fromarray(bmp, pilformat) - img.save(filename, format=file_format, options=pil_options) + img.save(filename, format=file_format, **pil_options) #---------------------------------------------------------------- diff --git a/kiva/cairo.py b/kiva/cairo.py index 6be58921d..491d62845 100644 --- a/kiva/cairo.py +++ b/kiva/cairo.py @@ -1250,7 +1250,7 @@ def render_component(self, component, container_coords=False): self.translate_ctm(x, y) component.draw(self, view_bounds=(0, 0, w, h)) - def save(self, filename, file_format=None): + def save(self, filename, file_format=None, pil_options=None): """ Save the GraphicsContext to a (PNG) file. file_format is ignored. """ diff --git a/kiva/celiagg.py b/kiva/celiagg.py index bf1b2b76d..c88829400 100644 --- a/kiva/celiagg.py +++ b/kiva/celiagg.py @@ -103,8 +103,8 @@ def __init__(self, size, *args, **kwargs): self.__state_stack = [] # For HiDPI support - base_scale = kwargs.pop('base_pixel_scale', 1) - self.transform.scale(base_scale, base_scale) + self.base_scale = kwargs.pop('base_pixel_scale', 1) + self.transform.scale(self.base_scale, self.base_scale) # ---------------------------------------------------------------- # Size info @@ -814,7 +814,7 @@ def draw_path_at_points(self, points, path, mode=constants.FILL_STROKE): fill=self.fill_paint, ) - def save(self, filename, file_format=None): + def save(self, filename, file_format=None, pil_options=None): """ Save the contents of the context to a file """ try: @@ -824,6 +824,8 @@ def save(self, filename, file_format=None): if file_format is None: file_format = '' + if pil_options is None: + pil_options = {} pixels = self.gc.array if self.pix_format.startswith('bgra'): @@ -837,14 +839,23 @@ def save(self, filename, file_format=None): data = pixels img = Image.fromarray(data, 'RGBA') + 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 ((isinstance(filename, str) and - os.path.splitext(filename)[1][1:] in no_alpha_formats) or - (file_format.lower() in no_alpha_formats)): + if ext in no_alpha_formats or file_format.lower() in no_alpha_formats: img = img.convert('RGB') - img.save(filename, format=file_format) + # Check the output format to see if it can handle DPI + dpi_formats = ('jpg', 'png', 'tiff', 'jpeg') + if ext in dpi_formats or file_format.lower() in dpi_formats: + # Assume 72dpi is 1x + dpi = int(72 * self.base_scale) + pil_options['dpi'] = (dpi, dpi) + + img.save(filename, format=file_format, **pil_options) class CompiledPath(object): diff --git a/kiva/pdf.py b/kiva/pdf.py index 01b11564e..6c5cea5ce 100644 --- a/kiva/pdf.py +++ b/kiva/pdf.py @@ -783,5 +783,5 @@ def draw_path(self, mode=constants.FILL_STROKE): # erase the current path. self.current_pdf_path = None - def save(self): + def save(self, filename='', file_format=None, pil_options=None): self.gc.save() diff --git a/kiva/ps.py b/kiva/ps.py index abf73bcde..b4df361a0 100644 --- a/kiva/ps.py +++ b/kiva/ps.py @@ -116,7 +116,7 @@ def width(self): def height(self): return self.size[1] - def save(self, filename): + def save(self, filename, file_format=None, pil_options=None): with open(filename, "w") as f: ext = os.path.splitext(filename)[1] if ext in (".eps", ".epsf"): diff --git a/kiva/qpainter.py b/kiva/qpainter.py index 6d39127af..1f92700b7 100644 --- a/kiva/qpainter.py +++ b/kiva/qpainter.py @@ -834,7 +834,7 @@ def _flip_y(self, y): "Converts between a Kiva and a Qt y coordinate" return self._height - y - 1 - def save(self, filename, file_format=None): + def save(self, filename, file_format=None, pil_options=None): """ Save the contents of the context to a file """ if isinstance(self.qt_dc, QtGui.QPixmap): diff --git a/kiva/quartz/ABCGI.pyx b/kiva/quartz/ABCGI.pyx index 12d0a21f1..fdf1aebd6 100644 --- a/kiva/quartz/ABCGI.pyx +++ b/kiva/quartz/ABCGI.pyx @@ -188,6 +188,7 @@ cdef class ShadingFunction cdef class CGContext: cdef CGContextRef context + cdef float base_scale cdef long can_release cdef object current_font cdef object current_style @@ -201,9 +202,10 @@ cdef class CGContext: self.can_release = 0 self.text_matrix = CGAffineTransformMake(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) - def __init__(self, size_t context, long can_release=0): + def __init__(self, size_t context, long can_release=0, base_pixel_scale=1.0): self.context = context + self.base_scale = base_pixel_scale self.can_release = can_release self.fill_color = (0.0, 0.0, 0.0, 1.0) self.stroke_color = (0.0, 0.0, 0.0, 1.0) @@ -1340,7 +1342,7 @@ cdef class CGLayerContext(CGContextInABox): # Create a CGBitmapContext from this layer, draw to it, then let it save # itself out. rect = (0, 0) + self.size - bmp = CGBitmapContext(self.size) + bmp = CGBitmapContext(self.size, base_pixel_scale=self.base_scale) CGContextDrawLayerInRect(bmp.context, CGRectMakeFromPython(rect), self.layer) bmp.save(filename, file_format=file_format, pil_options=pil_options) @@ -1411,12 +1413,14 @@ cdef class CGBitmapContext(CGContext): def __init__(self, object size_or_array, bool grey_scale=0, int bits_per_component=8, int bytes_per_row=-1, - alpha_info=kCGImageAlphaPremultipliedLast): + alpha_info=kCGImageAlphaPremultipliedLast, base_pixel_scale=1.0): cdef int bits_per_pixel cdef CGColorSpaceRef colorspace cdef void* dataptr + self.base_scale = base_pixel_scale + if hasattr(size_or_array, '__array_interface__'): # It's an array. arr = numpy.asarray(size_or_array, order='C') @@ -1609,18 +1613,31 @@ cdef class CGBitmapContext(CGContext): if file_format is None: file_format = '' + if pil_options is None: + pil_options = {} + + file_ext = ( + os.path.splitext(filename)[1][1:] if isinstance(filename, str) + else '' + ) + + # Check te output format to see if DPI can be passed + dpi_formats = ('jpg', 'png', 'tiff', 'jpeg') + if file_ext in dpi_formats or file_format.lower() in dpi_formats: + # Assume 72dpi is 1x + dpi = int(72 * self.base_scale) + pil_options['dpi'] = (dpi, dpi) img = PilImage.frombuffer(mode, (self.width(), self.height()), self, 'raw', mode, 0, 1) if 'A' in mode: # Check the output format to see if it can handle an alpha channel. no_alpha_formats = ('jpg', 'bmp', 'eps', 'jpeg') - if ((isinstance(filename, basestring) and - os.path.splitext(filename)[1][1:] in no_alpha_formats) or - (file_format.lower() in no_alpha_formats)): + if (file_ext in no_alpha_formats or + file_format.lower() in no_alpha_formats): img = img.convert('RGB') - img.save(filename, format=file_format, options=pil_options) + img.save(filename, format=file_format, **pil_options) cdef class CGImage: cdef CGImageRef image diff --git a/kiva/svg.py b/kiva/svg.py index 575fd1321..dc6ebf5b6 100644 --- a/kiva/svg.py +++ b/kiva/svg.py @@ -163,7 +163,7 @@ def width(self): def height(self): return self.size[1] - def save(self, filename): + def save(self, filename, file_format=None, pil_options=None): with open(filename, "w") as f: ext = os.path.splitext(filename)[1] if ext == ".svg": diff --git a/kiva/tests/drawing_tester.py b/kiva/tests/drawing_tester.py index 1badaaf2b..442a0c1ab 100644 --- a/kiva/tests/drawing_tester.py +++ b/kiva/tests/drawing_tester.py @@ -175,6 +175,19 @@ def create_graphics_context(self, width, length, pixel_scale): """ raise NotImplementedError() + def save_and_return_dpi(self): + """ Draw an image and save it. Then read it back and return the DPI + """ + self.gc.begin_path() + self.gc.arc(150, 150, 100, 0.0, 2 * numpy.pi) + self.gc.fill_path() + + filename = "{0}.png".format(self.filename) + self.gc.save(filename) + image = Image.open(filename) + dpi = image.info['dpi'] + return dpi[0] + @contextlib.contextmanager def draw_and_check(self): yield diff --git a/kiva/tests/test_agg_drawing.py b/kiva/tests/test_agg_drawing.py index f6be9d002..e052431c0 100644 --- a/kiva/tests/test_agg_drawing.py +++ b/kiva/tests/test_agg_drawing.py @@ -19,6 +19,10 @@ class TestAggDrawing(DrawingImageTester, unittest.TestCase): def create_graphics_context(self, width, height, pixel_scale): return GraphicsContext((width, height), base_pixel_scale=pixel_scale) + def test_save_dpi(self): + # Base DPI is 72, but our default pixel scale is 2x. + self.assertEqual(self.save_and_return_dpi(), 144) + def test_unicode_gradient_args(self): color_nodes = [(0.0, 1.0, 0.0, 0.0), (1.0, 0.0, 0.0, 0.0)] with self.draw_and_check(): diff --git a/kiva/tests/test_celiagg_drawing.py b/kiva/tests/test_celiagg_drawing.py index 6fe225494..78ae464ab 100644 --- a/kiva/tests/test_celiagg_drawing.py +++ b/kiva/tests/test_celiagg_drawing.py @@ -17,6 +17,10 @@ class TestCeliaggDrawing(DrawingImageTester, unittest.TestCase): def create_graphics_context(self, width, height, pixel_scale): return GraphicsContext((width, height), base_pixel_scale=pixel_scale) + def test_save_dpi(self): + # Base DPI is 72, but our default pixel scale is 2x. + self.assertEqual(self.save_and_return_dpi(), 144) + def test_clip_rect_transform(self): with self.draw_and_check(): self.gc.clip_to_rect(0, 0, 100, 100) diff --git a/kiva/tests/test_quartz_drawing.py b/kiva/tests/test_quartz_drawing.py index a7203e06a..3252aa5f9 100644 --- a/kiva/tests/test_quartz_drawing.py +++ b/kiva/tests/test_quartz_drawing.py @@ -20,4 +20,8 @@ class TestQuartzDrawing(DrawingImageTester, unittest.TestCase): def create_graphics_context(self, width, height, pixel_scale): from kiva.quartz import ABCGI - return ABCGI.CGBitmapContext((width, height)) + return ABCGI.CGBitmapContext((width, height), base_pixel_scale=pixel_scale) + + def test_save_dpi(self): + # Base DPI is 72, but our default pixel scale is 2x. + self.assertEqual(self.save_and_return_dpi(), 144)