From f41ebebf9c4f11106240d0a77f4ca71e4310ef8f Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 5 May 2019 15:00:44 +0200 Subject: [PATCH 1/2] Support pil_kwargs for jpeg/tiff. --- lib/mplcairo/base.py | 67 ++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/lib/mplcairo/base.py b/lib/mplcairo/base.py index e2dac453..2fd8555b 100644 --- a/lib/mplcairo/base.py +++ b/lib/mplcairo/base.py @@ -157,6 +157,13 @@ def tostring_rgba_minimized(self): # Needed for MixedModeRenderer. return img.tobytes(), bounds +def _check_print_extra_kwargs(*, + # These arguments are already taken care of by print_figure(). + dpi=72, facecolor=None, edgecolor=None, orientation="portrait", + dryrun=False, bbox_inches_restore=None): + pass + + class FigureCanvasCairo(FigureCanvasBase): # Although this attribute should semantically be set from __init__ (it is # purely an instance attribute), initializing it at the class level helps @@ -212,12 +219,9 @@ def restore_region(self, region): self.get_renderer().restore_region(region) super().draw() - def _print_method( - self, renderer_factory, - path_or_stream, *, metadata=None, dpi=72, - # These arguments are already taken care of by print_figure(). - facecolor=None, edgecolor=None, orientation="portrait", - dryrun=False, bbox_inches_restore=None): + def _print_method(self, renderer_factory, + path_or_stream, *, metadata=None, dpi=72, **kwargs): + _check_print_extra_kwargs(**kwargs) self.figure.set_dpi(72) with cbook.open_file_cm(path_or_stream, "wb") as stream: renderer = renderer_factory( @@ -289,11 +293,9 @@ def _get_fresh_straight_rgba8888(self): self._last_renderer_call = last_renderer_call return _util.cairo_to_straight_rgba8888(renderer._get_buffer()) - def print_rgba( - self, path_or_stream, *, metadata=None, - # These arguments are already taken care of by print_figure(). - dpi=72, facecolor=None, edgecolor=None, orientation="portrait", - dryrun=False, bbox_inches_restore=None): + def print_rgba(self, path_or_stream, *, dryrun=False, metadata=None, + **kwargs): + _check_print_extra_kwargs(**kwargs) img = self._get_fresh_straight_rgba8888() if dryrun: return @@ -302,11 +304,9 @@ def print_rgba( print_raw = print_rgba - def print_png( - self, path_or_stream, *, metadata=None, - # These arguments are already taken care of by print_figure(). - dpi=72, facecolor=None, edgecolor=None, orientation="portrait", - dryrun=False, bbox_inches_restore=None): + def print_png(self, path_or_stream, *, dryrun=False, metadata=None, + **kwargs): + _check_print_extra_kwargs(**kwargs) img = self._get_fresh_straight_rgba8888() if dryrun: return @@ -320,13 +320,16 @@ def print_png( if Image: - def print_jpeg( - self, path_or_stream, *, - # These arguments are already taken care of by print_figure(). - dpi=72, facecolor=None, edgecolor=None, orientation="portrait", - dryrun=False, bbox_inches_restore=None, - # Remaining kwargs are passed to PIL. - **kwargs): + def print_jpeg(self, path_or_stream, *, dryrun=False, pil_kwargs=None, + **kwargs): + if pil_kwargs is None: + pil_kwargs = {} + for k in ["quality", "optimize", "progressive"]: + if k in kwargs: + pil_kwargs.setdefault(k, kwargs.pop(k)) + pil_kwargs.setdefault("quality", rcParams["savefig.jpeg_quality"]) + pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi)) + _check_print_extra_kwargs(**kwargs) buf = self._get_fresh_straight_rgba8888() if dryrun: return @@ -339,24 +342,22 @@ def print_jpeg( (np.array(colors.to_rgb(facecolor)) * 255).astype(int)) composited = Image.new("RGB", buf.shape[:2][::-1], background) composited.paste(img, img) - kwargs.setdefault("quality", mpl.rcParams["savefig.jpeg_quality"]) - composited.save(path_or_stream, format="jpeg", - dpi=(self.figure.dpi, self.figure.dpi), **kwargs) + composited.save(path_or_stream, format="jpeg", **pil_kwargs) print_jpg = print_jpeg - def print_tiff( - self, path_or_stream, *, - # These arguments are already taken care of by print_figure(). - dpi=72, facecolor=None, edgecolor=None, orientation="portrait", - dryrun=False, bbox_inches_restore=None): + def print_tiff(self, path_or_stream, *, dryrun=False, pil_kwargs=None, + **kwargs): + if pil_kwargs is None: + pil_kwargs = {} + pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi)) + _check_print_extra_kwargs(**kwargs) buf = self._get_fresh_straight_rgba8888() if dryrun: return (Image.frombuffer( "RGBA", buf.shape[:2][::-1], buf, "raw", "RGBA", 0, 1) - .save(path_or_stream, format="tiff", - dpi=(self.figure.dpi, self.figure.dpi))) + .save(path_or_stream, format="tiff", **pil_kwargs)) print_tif = print_tiff From 6af8c6755d4e462b8b0c4eb2bcfaa1d329e69354 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 5 May 2019 15:23:14 +0200 Subject: [PATCH 2/2] Support EMF output on Windows. --- README.rst | 1 + lib/mplcairo/base.py | 25 ++++++++++++++++++++++++- setup.py | 2 +- src/_mplcairo.cpp | 41 +++++++++++++++++++++++++++++++++++++++++ src/_mplcairo.h | 8 ++++++++ src/_os.cpp | 5 ++--- src/_util.cpp | 2 ++ src/_util.h | 12 +++++++++++- 8 files changed, 90 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index f5730139..40848987 100644 --- a/README.rst +++ b/README.rst @@ -43,6 +43,7 @@ Noteworthy points include: - Support for multi-page output both for PDF and PS (Matplotlib only supports multi-page PDF). - Support for custom blend modes (see `examples/operators.py`_). +- On Windows, support for EMF output. .. _cairo: https://www.cairographics.org/ .. _Matplotlib: http://matplotlib.org/ diff --git a/lib/mplcairo/base.py b/lib/mplcairo/base.py index 2fd8555b..4e7bbeed 100644 --- a/lib/mplcairo/base.py +++ b/lib/mplcairo/base.py @@ -6,6 +6,7 @@ import os from pathlib import Path import shutil +import sys from tempfile import TemporaryDirectory from threading import RLock @@ -25,7 +26,7 @@ from . import _mplcairo, _util from ._backports import get_glyph_name -from ._mplcairo import _StreamSurfaceType +from ._mplcairo import _StreamSurfaceType, _EMFMarker _log = logging.getLogger() @@ -96,6 +97,13 @@ def _finish(): obj._finish = _finish return obj + @classmethod + def _for_emf_output(cls, path, width, height, dpi): + args = _EMFMarker.EMF, path, width, height, dpi + obj = _mplcairo.GraphicsContextRendererCairo.__new__(cls, *args) + _mplcairo.GraphicsContextRendererCairo.__init__(obj, *args) + return obj + def option_image_nocomposite(self): return (not mpl.rcParams["image.composite_image"] if self._has_vector_surface() else True) @@ -361,6 +369,21 @@ def print_tiff(self, path_or_stream, *, dryrun=False, pil_kwargs=None, print_tif = print_tiff + if sys.platform == "win32": + + def print_emf(self, path, *, dpi=72, metadata=None, **kwargs): + _check_print_extra_kwargs(**kwargs) + self.figure.set_dpi(72) + renderer = GraphicsContextRendererCairo._for_emf_output( + path, self.figure.bbox.width, self.figure.bbox.height, dpi) + if metadata: + _log.warning("No support for EMF metadata.") + with _LOCK: + self.figure.draw(renderer) + # _finish() corresponds finalize() in Matplotlib's PDF and SVG + # backends; it is inlined in Matplotlib's PS backend. + renderer._finish() + @_Backend.export class _BackendCairo(_Backend): diff --git a/setup.py b/setup.py index f69e1c21..0b8af0a4 100644 --- a/setup.py +++ b/setup.py @@ -171,7 +171,7 @@ def build_extensions(self): "/EHsc", "/D_USE_MATH_DEFINES", "/wd4244", "/wd4267"]) # cf. gcc -Wconversion. ext.libraries += ( - ["psapi", "cairo", "freetype"]) + ["psapi", "gdi32", "cairo", "freetype"]) ext.library_dirs += ( # Windows conda path for FreeType. [str(Path(sys.prefix, "Library/lib"))]) diff --git a/src/_mplcairo.cpp b/src/_mplcairo.cpp index 2fad3f88..a72d4362 100644 --- a/src/_mplcairo.cpp +++ b/src/_mplcairo.cpp @@ -342,6 +342,42 @@ GraphicsContextRenderer::GraphicsContextRenderer( cr_from_fileformat_args(type, file, width, height, dpi), width, height, 72} {} +#ifdef _WIN32 +cairo_t* GraphicsContextRenderer::cr_from_emf_args( + py::object path, double width, double height, double dpi) +{ + if (!detail::cairo_win32_printing_surface_create) { + throw std::runtime_error("cairo was built without Win32 support"); + } + auto const& path_s = +#if PY_VERSION_HEX >= 0x03060000 + py::reinterpret_steal(PY_CHECK(PyOS_FSPath, path.ptr())) +#else + path +#endif + .cast(); + auto rect = RECT{0, 0, LONG(width / dpi * 2540), LONG(height / dpi * 2540)}; + auto const& hdc = CreateEnhMetaFile(nullptr, path_s.c_str(), &rect, nullptr); + auto const& surface = detail::cairo_win32_printing_surface_create(hdc); + auto const& cr = cairo_create(surface); + cairo_surface_destroy(surface); + CAIRO_CHECK( + cairo_set_user_data, cr, &detail::REFS_KEY, hdc, + [](void* data) -> void { + if (!DeleteEnhMetaFile(CloseEnhMetaFile(static_cast(data)))) { + std::cerr << "Failed to close EMF.\n"; + } + }); + return cr; +} + +GraphicsContextRenderer::GraphicsContextRenderer( + EMFMarker, py::object path, double width, double height, double dpi) : + GraphicsContextRenderer{ + cr_from_emf_args(path, width, height, dpi), width, height, 72} +{} +#endif + GraphicsContextRenderer GraphicsContextRenderer::make_pattern_gcr( cairo_surface_t* surface) { @@ -1728,6 +1764,8 @@ Patch an artist to make it use this compositing operator for drawing. .value("EPS", StreamSurfaceType::EPS) .value("SVG", StreamSurfaceType::SVG) .value("Script", StreamSurfaceType::Script); + py::enum_(m, "_EMFMarker") + .value("EMF", EMFMarker::EMF); // Export functions. m.def( @@ -1821,6 +1859,9 @@ options. .def(py::init()) #endif .def(py::init()) +#ifdef _WIN32 + .def(py::init()) +#endif .def( py::pickle( [](GraphicsContextRenderer const& gcr) -> py::tuple { diff --git a/src/_mplcairo.h b/src/_mplcairo.h index b3161038..cae5916c 100644 --- a/src/_mplcairo.h +++ b/src/_mplcairo.h @@ -10,6 +10,10 @@ enum class StreamSurfaceType { PDF, PS, EPS, SVG, Script }; +enum class EMFMarker { + EMF +}; + struct Region { cairo_rectangle_int_t const bbox; std::unique_ptr buffer; // Not const, to allow move()ing. @@ -53,6 +57,10 @@ class GraphicsContextRenderer { GraphicsContextRenderer( StreamSurfaceType type, py::object file, double width, double height, double dpi); + static cairo_t* cr_from_emf_args( + py::object path, double width, double height, double dpi); + GraphicsContextRenderer( + EMFMarker, py::object path, double width, double height, double dpi); static GraphicsContextRenderer make_pattern_gcr(cairo_surface_t* cr); diff --git a/src/_os.cpp b/src/_os.cpp index b0fc996f..9e9f70ce 100644 --- a/src/_os.cpp +++ b/src/_os.cpp @@ -1,17 +1,16 @@ #include "_os.h" +#include + #if defined __linux__ || defined __APPLE__ #include #elif defined _WIN32 #include - #define NOMINMAX #include #include #endif -#include - namespace mplcairo::os { namespace py = pybind11; diff --git a/src/_util.cpp b/src/_util.cpp index 87ccd563..52e9c1ae 100644 --- a/src/_util.cpp +++ b/src/_util.cpp @@ -141,11 +141,13 @@ bool has_vector_surface(cairo_t* cr) switch (auto const& type = cairo_surface_get_type(cairo_get_target(cr))) { case CAIRO_SURFACE_TYPE_IMAGE: case CAIRO_SURFACE_TYPE_XLIB: + case CAIRO_SURFACE_TYPE_WIN32: return false; case CAIRO_SURFACE_TYPE_PDF: case CAIRO_SURFACE_TYPE_PS: case CAIRO_SURFACE_TYPE_SVG: case CAIRO_SURFACE_TYPE_RECORDING: + case CAIRO_SURFACE_TYPE_WIN32_PRINTING: return true; case CAIRO_SURFACE_TYPE_SCRIPT: switch (detail::MPLCAIRO_SCRIPT_SURFACE) { diff --git a/src/_util.h b/src/_util.h index 0575b126..ac0542c6 100644 --- a/src/_util.h +++ b/src/_util.h @@ -9,6 +9,11 @@ #include #include +#ifdef _WIN32 +#define NOMINMAX +#include +#endif + // Helper for std::visit. template struct overloaded : Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; @@ -52,6 +57,10 @@ extern void (*cairo_pdf_surface_set_metadata)( cairo_surface_t*, cairo_pdf_metadata_t, char const*); extern void (*cairo_ps_surface_set_eps)(cairo_surface_t*, cairo_bool_t); extern void (*cairo_ps_surface_dsc_comment)(cairo_surface_t*, char const*); +#ifndef _WIN32 +#define HDC void* +#endif +extern cairo_surface_t* (*cairo_win32_printing_surface_create)(HDC); #define ITER_CAIRO_OPTIONAL_API(_) \ _(cairo_tag_begin) \ @@ -63,7 +72,8 @@ extern void (*cairo_ps_surface_dsc_comment)(cairo_surface_t*, char const*); _(cairo_ps_surface_set_size) \ _(cairo_pdf_surface_set_metadata) \ _(cairo_ps_surface_set_eps) \ - _(cairo_ps_surface_dsc_comment) + _(cairo_ps_surface_dsc_comment) \ + _(cairo_win32_printing_surface_create) // Other useful values. extern std::unordered_map FONT_CACHE;