From 6eea13043f82c40f352e975206098937d8ce5e73 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 30 Sep 2021 16:28:05 -0500 Subject: [PATCH] Add support for PNG plots using libpng --- CMakeLists.txt | 10 +++ cmake/OpenMCConfig.cmake.in | 2 + include/openmc/plot.h | 13 ++- src/plot.cpp | 80 +++++++++++++++++-- tests/regression_tests/plot/results_true.dat | 2 +- tests/regression_tests/plot/test.py | 8 +- .../plot_overlaps/results_true.dat | 2 +- tests/regression_tests/plot_overlaps/test.py | 8 +- tests/regression_tests/plot_voxel/test.py | 2 +- 9 files changed, 107 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 75c66a233..d0c2569c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,11 @@ if(libmesh) find_package(LIBMESH REQUIRED) endif() +#=============================================================================== +# libpng +#=============================================================================== +find_package(PNG) + #=============================================================================== # HDF5 for binary output #=============================================================================== @@ -423,6 +428,11 @@ if(libmesh) target_link_libraries(libopenmc PkgConfig::LIBMESH) endif() +if (PNG_FOUND) + target_compile_definitions(libopenmc PRIVATE USE_LIBPNG) + target_link_libraries(libopenmc PNG::PNG) +endif() + #=============================================================================== # openmc executable #=============================================================================== diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index fa7ed7cc1..b12e95f23 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -16,6 +16,8 @@ if(@libmesh@) pkg_check_modules(LIBMESH REQUIRED @LIBMESH_PC_FILE@>=1.6.0 IMPORTED_TARGET) endif() +find_package(PNG) + if(NOT TARGET OpenMC::libopenmc) include("${OpenMC_CMAKE_DIR}/OpenMCTargets.cmake") endif() diff --git a/include/openmc/plot.h b/include/openmc/plot.h index aa15277aa..93a5f42ad 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -239,11 +239,18 @@ public: //! \param[out] image data associated with the plot object void draw_mesh_lines(Plot const& pl, ImageData& data); -//! Write a ppm image to file using a plot object's image data +//! Write a PPM image using a plot object's image data //! \param[in] plot object //! \param[out] image data associated with the plot object void output_ppm(Plot const& pl, const ImageData& data); +#ifdef USE_LIBPNG +//! Write a PNG image using a plot object's image data +//! \param[in] plot object +//! \param[out] image data associated with the plot object +void output_png(Plot const& pl, const ImageData& data); +#endif + //! Initialize a voxel file //! \param[in] id of an open hdf5 file //! \param[in] dimensions of the voxel file (dx, dy, dz) @@ -274,9 +281,9 @@ void voxel_finalize(hid_t dspace, hid_t dset, hid_t memspace); //! Read plot specifications from a plots.xml file void read_plots_xml(); -//! Create a ppm image for a plot object +//! Create an image for a plot object //! \param[in] plot object -void create_ppm(Plot const& pl); +void create_image(Plot const& pl); //! Create an hdf5 voxel file for a plot object //! \param[in] plot object diff --git a/src/plot.cpp b/src/plot.cpp index 789437691..27a01bc07 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1,12 +1,16 @@ #include "openmc/plot.h" #include +#include #include #include #include "xtensor/xview.hpp" #include #include +#ifdef USE_LIBPNG +#include +#endif #include "openmc/constants.h" #include "openmc/error.h" @@ -103,17 +107,19 @@ uint64_t plotter_seed = 1; extern "C" int openmc_plot_geometry() { + for (auto& pl : model::plots) { write_message(5, "Processing plot {}: {}...", pl.id_, pl.path_plot_); if (PlotType::slice == pl.type_) { // create 2D image - create_ppm(pl); + create_image(pl); } else if (PlotType::voxel == pl.type_) { // create voxel file for 3D viewing create_voxel(pl); } } + return 0; } @@ -139,11 +145,11 @@ void read_plots_xml() } //============================================================================== -// CREATE_PPM creates an image based on user input from a plots.xml -// specification in the portable pixmap format (PPM) +// CREATE_IMAGE creates an image based on user input from a plots.xml +// specification in the PNG/PPM format //============================================================================== -void create_ppm(Plot const& pl) +void create_image(Plot const& pl) { size_t width = pl.pixels_[0]; @@ -184,8 +190,12 @@ void create_ppm(Plot const& pl) draw_mesh_lines(pl, data); } - // write ppm data to file +// create image file +#ifdef USE_LIBPNG + output_png(pl, data); +#else output_ppm(pl, data); +#endif } void Plot::set_id(pugi::xml_node plot_node) @@ -238,7 +248,11 @@ void Plot::set_output_path(pugi::xml_node plot_node) // add appropriate file extension to name switch (type_) { case PlotType::slice: +#ifdef USE_LIBPNG + filename.append(".png"); +#else filename.append(".ppm"); +#endif break; case PlotType::voxel: filename.append(".h5"); @@ -673,6 +687,60 @@ void output_ppm(Plot const& pl, const ImageData& data) of << "\n"; } +//============================================================================== +// OUTPUT_PNG writes out a previously generated image to a PNG file +//============================================================================== + +#ifdef USE_LIBPNG +void output_png(Plot const& pl, const ImageData& data) +{ + // Open PNG file for writing + std::string fname = pl.path_plot_; + fname = strtrim(fname); + auto fp = std::fopen(fname.c_str(), "wb"); + + // Initialize write and info structures + auto png_ptr = + png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + auto info_ptr = png_create_info_struct(png_ptr); + + // Setup exception handling + if (setjmp(png_jmpbuf(png_ptr))) + fatal_error("Error during png creation"); + + png_init_io(png_ptr, fp); + + // Write header (8 bit colour depth) + int width = pl.pixels_[0]; + int height = pl.pixels_[1]; + png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + png_write_info(png_ptr, info_ptr); + + // Allocate memory for one row (3 bytes per pixel - RGB) + std::vector row(3 * width); + + // Write color for each pixel + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + RGBColor rgb = data(x, y); + row[3 * x] = rgb.red; + row[3 * x + 1] = rgb.green; + row[3 * x + 2] = rgb.blue; + } + png_write_row(png_ptr, row.data()); + } + + // End write + png_write_end(png_ptr, nullptr); + + // Clean up data structures + std::fclose(fp); + png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1); + png_destroy_write_struct(&png_ptr, nullptr); +} +#endif + //============================================================================== // DRAW_MESH_LINES draws mesh line boundaries on an image //============================================================================== @@ -777,7 +845,7 @@ void draw_mesh_lines(Plot const& pl, ImageData& data) //============================================================================== // CREATE_VOXEL outputs a binary file that can be input into silomesh for 3D -// geometry visualization. It works the same way as create_ppm by dragging a +// geometry visualization. It works the same way as create_image by dragging a // particle across the geometry for the specified number of voxels. The first 3 // int's in the binary are the number of x, y, and z voxels. The next 3 // double's are the widths of the voxels in the x, y, and z directions. The diff --git a/tests/regression_tests/plot/results_true.dat b/tests/regression_tests/plot/results_true.dat index 54b6a253f..9174202fb 100644 --- a/tests/regression_tests/plot/results_true.dat +++ b/tests/regression_tests/plot/results_true.dat @@ -1 +1 @@ -a8192f6029cf99748816fab2618fa48e180656eba313940ccdfff3e56890a5dadd13bf92cd7e3be6a4b535bca5e2a58a905fcfba018fb922273f8be6dfc19859 \ No newline at end of file +f85c20735a0c08525fe48b19a8e075c074539ee6c8860268fa0cb515d842496b7086e5b94305ef78dcf2106bb193abdf438259ac8ff1d0245a2782eb6f5af873 \ No newline at end of file diff --git a/tests/regression_tests/plot/test.py b/tests/regression_tests/plot/test.py index a6f7306a0..2fd6a5b79 100644 --- a/tests/regression_tests/plot/test.py +++ b/tests/regression_tests/plot/test.py @@ -19,7 +19,7 @@ class PlotTestHarness(TestHarness): openmc.plot_geometry(openmc_exec=config['exe']) def _test_output_created(self): - """Make sure *.ppm has been created.""" + """Make sure *.png has been created.""" for fname in self._plot_names: assert os.path.exists(fname), 'Plot output file does not exist.' @@ -34,8 +34,8 @@ class PlotTestHarness(TestHarness): outstr = bytes() for fname in self._plot_names: - if fname.endswith('.ppm'): - # Add PPM output to results + if fname.endswith('.png'): + # Add PNG output to results with open(fname, 'rb') as fh: outstr += fh.read() elif fname.endswith('.h5'): @@ -56,6 +56,6 @@ class PlotTestHarness(TestHarness): def test_plot(): - harness = PlotTestHarness(('plot_1.ppm', 'plot_2.ppm', 'plot_3.ppm', + harness = PlotTestHarness(('plot_1.png', 'plot_2.png', 'plot_3.png', 'plot_4.h5')) harness.main() diff --git a/tests/regression_tests/plot_overlaps/results_true.dat b/tests/regression_tests/plot_overlaps/results_true.dat index 90ec8924a..cb04daaa4 100644 --- a/tests/regression_tests/plot_overlaps/results_true.dat +++ b/tests/regression_tests/plot_overlaps/results_true.dat @@ -1 +1 @@ -566103831cb8273b0578565c39d30e479664e2b1783d877b45b42cf4f3af0b01671b6db423114b09a74bbe1ddf51a7db565ff2118d6d1ee987b52318773b719a \ No newline at end of file +926065ceb2a9b8292fe6270317c38c4373473cfea19d2a8392a32e5ece8e314c04b9f032921d987bd195ae4b6f674d359b0e38302e6ae4c93b4ac9573a384ac6 \ No newline at end of file diff --git a/tests/regression_tests/plot_overlaps/test.py b/tests/regression_tests/plot_overlaps/test.py index 62236081b..cf23abc27 100644 --- a/tests/regression_tests/plot_overlaps/test.py +++ b/tests/regression_tests/plot_overlaps/test.py @@ -19,7 +19,7 @@ class PlotTestHarness(TestHarness): openmc.plot_geometry(openmc_exec=config['exe']) def _test_output_created(self): - """Make sure *.ppm has been created.""" + """Make sure *.png has been created.""" for fname in self._plot_names: assert os.path.exists(fname), 'Plot output file does not exist.' @@ -34,8 +34,8 @@ class PlotTestHarness(TestHarness): outstr = bytes() for fname in self._plot_names: - if fname.endswith('.ppm'): - # Add PPM output to results + if fname.endswith('.png'): + # Add PNG output to results with open(fname, 'rb') as fh: outstr += fh.read() elif fname.endswith('.h5'): @@ -56,6 +56,6 @@ class PlotTestHarness(TestHarness): def test_plot_overlap(): - harness = PlotTestHarness(('plot_1.ppm', 'plot_2.ppm', 'plot_3.ppm', + harness = PlotTestHarness(('plot_1.png', 'plot_2.png', 'plot_3.png', 'plot_4.h5')) harness.main() diff --git a/tests/regression_tests/plot_voxel/test.py b/tests/regression_tests/plot_voxel/test.py index 3b7a6bef2..c4fa20c80 100644 --- a/tests/regression_tests/plot_voxel/test.py +++ b/tests/regression_tests/plot_voxel/test.py @@ -25,7 +25,7 @@ class PlotVoxelTestHarness(TestHarness): glob.glob('plot_4.h5')) def _test_output_created(self): - """Make sure *.ppm has been created.""" + """Make sure plots have been created.""" for fname in self._plot_names: assert os.path.exists(fname), 'Plot output file does not exist.'