diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5e619ed3e..06a8b9f79 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,10 @@ set(CELESTIA_LIBS cel3ds + celcompat celengine + celephem celestia + celimage celmath celmodel celttf @@ -14,10 +17,6 @@ endforeach() add_subdirectory(celscript) -# These compiled objects are merged with the celengine library -add_subdirectory(celephem) -add_subdirectory(celcompat) - if (ENABLE_TOOLS) add_subdirectory(tools) endif() diff --git a/src/celengine/CMakeLists.txt b/src/celengine/CMakeLists.txt index bf3a07936..67368e0fc 100644 --- a/src/celengine/CMakeLists.txt +++ b/src/celengine/CMakeLists.txt @@ -26,9 +26,6 @@ set(CELENGINE_SOURCES constellation.h curveplot.cpp curveplot.h - dds.cpp - dds_decompress.cpp - dds_decompress.h deepskyobj.cpp deepskyobj.h dispmap.cpp diff --git a/src/celengine/galaxy.cpp b/src/celengine/galaxy.cpp index ece25d7e3..4f9ed3660 100644 --- a/src/celengine/galaxy.cpp +++ b/src/celengine/galaxy.cpp @@ -588,8 +588,7 @@ GalacticForm* buildGalacticForms(const fs::path& filename) int width, height, rgb, j = 0, kmin = 9; unsigned char value; float h = 0.75f; - Image* img; - img = LoadPNGImage(filename); + Image* img = LoadImageFromFile(filename); if (img == nullptr) { cout<<"\nThe galaxy template *** "< #include #include -#include -#include -#include #include #include - -extern "C" { -#include -} -#include - -#include "glsupport.h" - +#include #include #include #include +#include #include "image.h" - using namespace std; - +namespace +{ // All rows are padded to a size that's a multiple of 4 bytes -static int pad(int n) +int pad(int n) { return (n + 3) & ~0x3; } - -static int formatComponents(int fmt) +int formatComponents(int fmt) { switch (fmt) { @@ -73,8 +62,7 @@ static int formatComponents(int fmt) } } - -static int calcMipLevelSize(int fmt, int w, int h, int mip) +int calcMipLevelSize(int fmt, int w, int h, int mip) { w = max(w >> mip, 1); h = max(h >> mip, 1); @@ -92,7 +80,7 @@ static int calcMipLevelSize(int fmt, int w, int h, int mip) return h * pad(w * formatComponents(fmt)); } } - +} // anonymous namespace Image::Image(int fmt, int w, int h, int mip) : width(w), @@ -111,61 +99,51 @@ Image::Image(int fmt, int w, int h, int mip) : pixels = new unsigned char[size]; } - Image::~Image() { delete[] pixels; } - int Image::getWidth() const { return width; } - int Image::getHeight() const { return height; } - int Image::getPitch() const { return pitch; } - int Image::getMipLevelCount() const { return mipLevels; } - int Image::getSize() const { return size; } - int Image::getFormat() const { return format; } - int Image::getComponents() const { return components; } - unsigned char* Image::getPixels() { return pixels; } - unsigned char* Image::getPixelRow(int mip, int row) { /*int w = max(width >> mip, 1); Unused*/ @@ -180,13 +158,11 @@ unsigned char* Image::getPixelRow(int mip, int row) return getMipLevel(mip) + row * pitch; } - unsigned char* Image::getPixelRow(int row) { return getPixelRow(0, row); } - unsigned char* Image::getMipLevel(int mip) { if (mip >= mipLevels) @@ -199,7 +175,6 @@ unsigned char* Image::getMipLevel(int mip) return pixels + offset; } - int Image::getMipLevelSize(int mip) const { if (mip >= mipLevels) @@ -208,7 +183,6 @@ int Image::getMipLevelSize(int mip) const return calcMipLevelSize(format, width, height, mip); } - bool Image::isCompressed() const { switch (format) @@ -222,7 +196,6 @@ bool Image::isCompressed() const } } - bool Image::hasAlpha() const { switch (format) @@ -239,11 +212,12 @@ bool Image::hasAlpha() const } } - -// Convert an input height map to a normal map. Ideally, a single channel -// input should be used. If not, the first color channel of the input image -// is the one only one used when generating normals. This produces the -// expected results for grayscale values in RGB images. +/** + * Convert an input height map to a normal map. Ideally, a single channel + * input should be used. If not, the first color channel of the input image + * is the one only one used when generating normals. This produces the + * expected results for grayscale values in RGB images. + */ Image* Image::computeNormalMap(float scale, bool wrap) const { // Can't do anything with compressed input; there are probably some other @@ -311,7 +285,6 @@ Image* Image::computeNormalMap(float scale, bool wrap) const return normalMap; } - Image* LoadImageFromFile(const fs::path& filename) { ContentType type = DetermineFileType(filename); @@ -341,465 +314,3 @@ Image* LoadImageFromFile(const fs::path& filename) return img; } - - - - - -struct my_error_mgr -{ - struct jpeg_error_mgr pub; // "public" fields - jmp_buf setjmp_buffer; // for return to caller -}; - -using my_error_ptr = struct my_error_mgr *; - -METHODDEF(void) my_error_exit(j_common_ptr cinfo) -{ - // cinfo->err really points to a my_error_mgr struct, so coerce pointer - auto myerr = (my_error_ptr) cinfo->err; - - // Always display the message. - // We could postpone this until after returning, if we chose. - (*cinfo->err->output_message) (cinfo); - - // Return control to the setjmp point - longjmp(myerr->setjmp_buffer, 1); -} - - -Image* LoadJPEGImage(const fs::path& filename, int /*unused*/) -{ - Image* img = nullptr; - - // This struct contains the JPEG decompression parameters and pointers to - // working space (which is allocated as needed by the JPEG library). - struct jpeg_decompress_struct cinfo; - - // We use our private extension JPEG error handler. - // Note that this struct must live as long as the main JPEG parameter - // struct, to avoid dangling-pointer problems. - struct my_error_mgr jerr; - // More stuff - JSAMPARRAY buffer; // Output row buffer - int row_stride; // physical row width in output buffer - long cont; - - // In this example we want to open the input file before doing anything else, - // so that the setjmp() error recovery below can assume the file is open. - // VERY IMPORTANT: use "b" option to fopen() if you are on a machine that - // requires it in order to read binary files. - - FILE *in; -#ifdef _WIN32 - in = _wfopen(filename.c_str(), L"rb"); -#else - in = fopen(filename.c_str(), "rb"); -#endif - if (!in) - return nullptr; - - // Step 1: allocate and initialize JPEG decompression object - // We set up the normal JPEG error routines, then override error_exit. - cinfo.err = jpeg_std_error(&jerr.pub); - jerr.pub.error_exit = my_error_exit; - // Establish the setjmp return context for my_error_exit to use. - if (setjmp(jerr.setjmp_buffer)) - { - // If we get here, the JPEG code has signaled an error. - // We need to clean up the JPEG object, close the input file, and return. - jpeg_destroy_decompress(&cinfo); - fclose(in); - delete img; - - return nullptr; - } - - // Now we can initialize the JPEG decompression object. - jpeg_create_decompress(&cinfo); - - // Step 2: specify data source (eg, a file) - jpeg_stdio_src(&cinfo, in); - - // Step 3: read file parameters with jpeg_read_header() - (void) jpeg_read_header(&cinfo, TRUE); - - // We can ignore the return value from jpeg_read_header since - // (a) suspension is not possible with the stdio data source, and - // (b) we passed TRUE to reject a tables-only JPEG file as an error. - - // Step 4: set parameters for decompression - - // In this example, we don't need to change any of the defaults set by - // jpeg_read_header(), so we do nothing here. - - // Step 5: Start decompressor - - (void) jpeg_start_decompress(&cinfo); - // We can ignore the return value since suspension is not possible - // with the stdio data source. - - // We may need to do some setup of our own at this point before reading - // the data. After jpeg_start_decompress() we have the correct scaled - // output image dimensions available, as well as the output colormap - // if we asked for color quantization. - // In this example, we need to make an output work buffer of the right size. - // JSAMPLEs per row in output buffer - row_stride = cinfo.output_width * cinfo.output_components; - // Make a one-row-high sample array that will go away when done with image - buffer = (*cinfo.mem->alloc_sarray) - ((j_common_ptr) & cinfo, JPOOL_IMAGE, row_stride, 1); - - // Step 6: while (scan lines remain to be read) - // jpeg_read_scanlines(...); - - // Here we use the library's state variable cinfo.output_scanline as the - // loop counter, so that we don't have to keep track ourselves. - - int format = GL_RGB; - if (cinfo.output_components == 1) - format = GL_LUMINANCE; - - img = new Image(format, cinfo.image_width, cinfo.image_height); - - // cont = cinfo.output_height - 1; - cont = 0; - while (cinfo.output_scanline < cinfo.output_height) - { - // jpeg_read_scanlines expects an array of pointers to scanlines. - // Here the array is only one element long, but you could ask for - // more than one scanline at a time if that's more convenient. - (void) jpeg_read_scanlines(&cinfo, buffer, 1); - - // Assume put_scanline_someplace wants a pointer and sample count. - // put_scanline_someplace(buffer[0], row_stride); - memcpy(img->getPixelRow(cont), buffer[0], row_stride); - cont++; - } - - // Step 7: Finish decompression - - (void) jpeg_finish_decompress(&cinfo); - // We can ignore the return value since suspension is not possible - // with the stdio data source. - - // Step 8: Release JPEG decompression object - - // This is an important step since it will release a good deal of memory. - jpeg_destroy_decompress(&cinfo); - - // After finish_decompress, we can close the input file. - // Here we postpone it until after no more JPEG errors are possible, - // so as to simplify the setjmp error logic above. (Actually, I don't - // think that jpeg_destroy can do an error exit, but why assume anything... - - fclose(in); - - // At this point you may want to check to see whether any corrupt-data - // warnings occurred (test whether jerr.pub.num_warnings is nonzero). - - return img; -} - - -void PNGReadData(png_structp png_ptr, png_bytep data, png_size_t length) -{ - auto* fp = (FILE*) png_get_io_ptr(png_ptr); - if (fread((void*) data, 1, length, fp) != length) - cerr << "Error reading PNG data"; -} - - -Image* LoadPNGImage(const fs::path& filename) -{ - char header[8]; - png_structp png_ptr; - png_infop info_ptr; - png_uint_32 width, height; - int bit_depth, color_type, interlace_type; - Image* img = nullptr; - png_bytep* row_pointers = nullptr; - -#ifdef _WIN32 - FILE *fp = _wfopen(filename.c_str(), L"rb"); -#else - FILE *fp = fopen(filename.c_str(), "rb"); -#endif - if (fp == nullptr) - { - fmt::fprintf(clog, _("Error opening image file %s\n"), filename); - return nullptr; - } - - size_t elements_read; - elements_read = fread(header, 1, sizeof(header), fp); - if (elements_read == 0 || png_sig_cmp((unsigned char*) header, 0, sizeof(header))) - { - fmt::fprintf(clog, _("Error: %s is not a PNG file.\n"), filename); - fclose(fp); - return nullptr; - } - - png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, - nullptr, nullptr, nullptr); - if (png_ptr == nullptr) - { - fclose(fp); - return nullptr; - } - - info_ptr = png_create_info_struct(png_ptr); - if (info_ptr == nullptr) - { - fclose(fp); - png_destroy_read_struct(&png_ptr, (png_infopp) nullptr, (png_infopp) nullptr); - return nullptr; - } - - if (setjmp(png_jmpbuf(png_ptr))) - { - fclose(fp); - delete img; - png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) nullptr); - fmt::fprintf(clog, _("Error reading PNG image file %s\n"), filename); - return nullptr; - } - - // png_init_io(png_ptr, fp); - png_set_read_fn(png_ptr, (void*) fp, PNGReadData); - png_set_sig_bytes(png_ptr, sizeof(header)); - - png_read_info(png_ptr, info_ptr); - - png_get_IHDR(png_ptr, info_ptr, - &width, &height, &bit_depth, - &color_type, &interlace_type, - nullptr, nullptr); - - GLenum glformat = GL_RGB; - switch (color_type) - { - case PNG_COLOR_TYPE_GRAY: - glformat = GL_LUMINANCE; - break; - case PNG_COLOR_TYPE_GRAY_ALPHA: - glformat = GL_LUMINANCE_ALPHA; - break; - case PNG_COLOR_TYPE_RGB: - glformat = GL_RGB; - break; - case PNG_COLOR_TYPE_PALETTE: - case PNG_COLOR_TYPE_RGB_ALPHA: - glformat = GL_RGBA; - break; - default: - // badness - break; - } - - img = new Image(glformat, width, height); - - // TODO: consider using paletted textures if they're available - if (color_type == PNG_COLOR_TYPE_PALETTE) - { - png_set_palette_to_rgb(png_ptr); - } - - if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) - { - png_set_expand_gray_1_2_4_to_8(png_ptr); - } - - if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) - { - png_set_tRNS_to_alpha(png_ptr); - } - - // TODO: consider passing images with < 8 bits/component to - // GL without expanding - if (bit_depth == 16) - png_set_strip_16(png_ptr); - else if (bit_depth < 8) - png_set_packing(png_ptr); - - row_pointers = new png_bytep[height]; - for (unsigned int i = 0; i < height; i++) - row_pointers[i] = (png_bytep) img->getPixelRow(i); - - png_read_image(png_ptr, row_pointers); - - delete[] row_pointers; - - png_read_end(png_ptr, nullptr); - png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); - - fclose(fp); - - return img; -} - - -// BMP file definitions--can't use windows.h because we might not be -// built on Windows! -typedef struct -{ - unsigned char b; - unsigned char m; - unsigned int size; - unsigned int reserved; - unsigned int offset; -} BMPFileHeader; - -typedef struct -{ - unsigned int size; - int width; - int height; - unsigned short planes; - unsigned short bpp; - unsigned int compression; - unsigned int imageSize; - int widthPPM; - int heightPPM; - unsigned int colorsUsed; - unsigned int colorsImportant; -} BMPImageHeader; - - -static int readInt(ifstream& in) -{ - unsigned char b[4]; - in.read(reinterpret_cast(b), 4); - return ((int) b[3] << 24) + ((int) b[2] << 16) - + ((int) b[1] << 8) + (int) b[0]; -} - -static short readShort(ifstream& in) -{ - unsigned char b[2]; - in.read(reinterpret_cast(b), 2); - return ((short) b[1] << 8) + (short) b[0]; -} - - -static Image* LoadBMPImage(ifstream& in) -{ - BMPFileHeader fileHeader; - BMPImageHeader imageHeader; - unsigned char* pixels; - - in >> fileHeader.b; - in >> fileHeader.m; - fileHeader.size = readInt(in); - fileHeader.reserved = readInt(in); - fileHeader.offset = readInt(in); - - if (fileHeader.b != 'B' || fileHeader.m != 'M') - return nullptr; - - imageHeader.size = readInt(in); - imageHeader.width = readInt(in); - imageHeader.height = readInt(in); - imageHeader.planes = readShort(in); - imageHeader.bpp = readShort(in); - imageHeader.compression = readInt(in); - imageHeader.imageSize = readInt(in); - imageHeader.widthPPM = readInt(in); - imageHeader.heightPPM = readInt(in); - imageHeader.colorsUsed = readInt(in); - imageHeader.colorsImportant = readInt(in); - - if (imageHeader.width <= 0 || imageHeader.height <= 0) - return nullptr; - - // We currently don't support compressed BMPs - if (imageHeader.compression != 0) - return nullptr; - // We don't handle 1-, 2-, or 4-bpp images - if (imageHeader.bpp != 8 && imageHeader.bpp != 24 && imageHeader.bpp != 32) - return nullptr; - - unsigned char* palette = nullptr; - if (imageHeader.bpp == 8) - { - fmt::fprintf(cout, "Reading %d color palette\n", imageHeader.colorsUsed); - palette = new unsigned char[imageHeader.colorsUsed * 4]; - in.read(reinterpret_cast(palette), imageHeader.colorsUsed * 4); - } - - in.seekg(fileHeader.offset, ios::beg); - - unsigned int bytesPerRow = - (imageHeader.width * imageHeader.bpp / 8 + 1) & ~1; - unsigned int imageBytes = bytesPerRow * imageHeader.height; - - // slurp the image data - pixels = new unsigned char[imageBytes]; - in.read(reinterpret_cast(pixels), imageBytes); - - // check for truncated file - - auto* img = new Image(GL_RGB, imageHeader.width, imageHeader.height); - - // Copy the image and perform any necessary conversions - for (int y = 0; y < imageHeader.height; y++) - { - unsigned char* src = &pixels[y * bytesPerRow]; - unsigned char* dst = img->getPixelRow(y); - - switch (imageHeader.bpp) - { - case 8: - for (int x = 0; x < imageHeader.width; x++) - { - unsigned char* color = palette + (*src << 2); - dst[0] = color[2]; - dst[1] = color[1]; - dst[2] = color[0]; - src++; - dst += 3; - } - break; - case 24: - for (int x = 0; x < imageHeader.width; x++) - { - dst[0] = src[2]; - dst[1] = src[1]; - dst[2] = src[0]; - src += 3; - dst += 3; - } - break; - case 32: - for (int x = 0; x < imageHeader.width; x++) - { - dst[0] = src[2]; - dst[1] = src[1]; - dst[2] = src[0]; - src += 4; - dst += 3; - } - break; - } - } - - delete[] pixels; - delete[] palette; - - return img; -} - - -Image* LoadBMPImage(const fs::path& filename) -{ - ifstream bmpFile(filename.string(), ios::in | ios::binary); - - if (bmpFile.good()) - { - Image* img = LoadBMPImage(bmpFile); - bmpFile.close(); - return img; - } - - return nullptr; -} diff --git a/src/celengine/image.h b/src/celengine/image.h index cd28a8b19..84460a29a 100644 --- a/src/celengine/image.h +++ b/src/celengine/image.h @@ -1,16 +1,15 @@ // image.h // -// Copyright (C) 2001, Chris Laurel +// Copyright (C) 2001-present, the Celestia Development Team +// Original version by Chris Laurel // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. -#ifndef _CELENGINE_IMAGE_H_ -#define _CELENGINE_IMAGE_H_ +#pragma once -#include #include // The image class supports multiple GL formats, including compressed ones. @@ -43,7 +42,8 @@ class Image Image* computeNormalMap(float scale, bool wrap) const; - enum { + enum + { ColorChannel = 1, AlphaChannel = 2 }; @@ -59,12 +59,4 @@ class Image unsigned char* pixels{ nullptr }; }; -extern Image* LoadJPEGImage(const fs::path& filename, - int channels = Image::ColorChannel); -extern Image* LoadBMPImage(const fs::path& filename); -extern Image* LoadPNGImage(const fs::path& filename); -extern Image* LoadDDSImage(const fs::path& filename); - -extern Image* LoadImageFromFile(const fs::path& filename); - -#endif // _CELENGINE_IMAGE_H_ +Image* LoadImageFromFile(const fs::path& filename); diff --git a/src/celestia/CMakeLists.txt b/src/celestia/CMakeLists.txt index 9a6299a09..0364b2b71 100644 --- a/src/celestia/CMakeLists.txt +++ b/src/celestia/CMakeLists.txt @@ -52,6 +52,7 @@ add_library(celestia SHARED ${CELESTIA_SOURCES} $ $ $ + $ $ $ $ diff --git a/src/celestia/celestiacore.cpp b/src/celestia/celestiacore.cpp index ce95875bf..728dce485 100644 --- a/src/celestia/celestiacore.cpp +++ b/src/celestia/celestiacore.cpp @@ -4688,22 +4688,11 @@ bool CelestiaCore::saveScreenShot(const fs::path& filename, ContentType type) co array viewport; getRenderer()->getViewport(viewport); - if (type == Content_JPEG) - { - return CaptureGLBufferToJPEG(filename, - viewport[0], viewport[1], - viewport[2], viewport[3], - getRenderer()); - } - if (type == Content_PNG) - { - return CaptureGLBufferToPNG(filename, - viewport[0], viewport[1], - viewport[2], viewport[3], - getRenderer()); - } - - return false; + return CaptureBufferToFile(filename, + viewport[0], viewport[1], + viewport[2], viewport[3], + getRenderer(), + type); } void CelestiaCore::setLogFile(fs::path &fn) diff --git a/src/celestia/imagecapture.cpp b/src/celestia/imagecapture.cpp index 6bcaa2084..ddac93e99 100644 --- a/src/celestia/imagecapture.cpp +++ b/src/celestia/imagecapture.cpp @@ -1,30 +1,37 @@ // imagecapture.cpp // -// Copyright (C) 2001, Chris Laurel +// Copyright (C) 2001-present, the Celestia Development Team +// Original version by Chris Laurel // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. -#include -#include +#include +#include +#include +#include +#include +#include #include "imagecapture.h" -extern "C" { -#include -} -#include -#include +using fmt::print; +using std::cerr; +using std::unique_ptr; -using namespace std; - - -bool CaptureGLBufferToJPEG(const fs::path& filename, - int x, int y, - int width, int height, - const Renderer *renderer) +bool CaptureBufferToFile(const fs::path& filename, + int x, int y, + int width, int height, + const Renderer *renderer, + ContentType type) { + if (type != Content_JPEG && type != Content_PNG) + { + print(cerr, "Unsupported image type: {}!\n", filename.string()); + return false; + } + #ifdef GL_ES int rowStride = width * 4; int imageSize = height * rowStride; @@ -34,196 +41,25 @@ bool CaptureGLBufferToJPEG(const fs::path& filename, int imageSize = height * rowStride; Renderer::PixelFormat format = Renderer::PixelFormat::RGB; #endif - auto* pixels = new unsigned char[imageSize]; + auto pixels = unique_ptr(new unsigned char[imageSize]); if (!renderer->captureFrame(x, y, width, height, - format, - pixels, true)) + format, pixels.get(), true)) { + print(cerr, _("Unable to capture a frame!\n")); return false; } - FILE* out; -#ifdef _WIN32 - out = _wfopen(filename.c_str(), L"wb"); -#else - out = fopen(filename.c_str(), "wb"); -#endif - if (out == nullptr) + bool removeAlpha = format == Renderer::PixelFormat::RGBA; + + switch (type) { - DPRINTF(LOG_LEVEL_ERROR, "Can't open screen capture file '%s'\n", filename); - delete[] pixels; - return false; + case Content_JPEG: + return SaveJPEGImage(filename, width, height, rowStride, pixels.get(), removeAlpha); + case Content_PNG: + return SavePNGImage(filename, width, height, rowStride, pixels.get(), removeAlpha); + default: + break; } - - struct jpeg_compress_struct cinfo; - - struct jpeg_error_mgr jerr; - JSAMPROW row[1]; - - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); - - jpeg_stdio_dest(&cinfo, out); - - cinfo.image_width = width; - cinfo.image_height = height; - cinfo.input_components = 3; - cinfo.in_color_space = JCS_RGB; - - jpeg_set_defaults(&cinfo); - - jpeg_set_quality(&cinfo, 90, TRUE); - - jpeg_start_compress(&cinfo, TRUE); - - while (cinfo.next_scanline < cinfo.image_height) - { - unsigned char *rowHead = &pixels[rowStride * (cinfo.image_height - cinfo.next_scanline - 1)]; - // Strip alpha values if we are in RGBA format - if (format == Renderer::PixelFormat::RGBA) - { - for (int x = 0; x < width; x++) - { - const unsigned char* pixelIn = &rowHead[x * 4]; - unsigned char* pixelOut = &rowHead[x * 3]; - pixelOut[0] = pixelIn[0]; - pixelOut[1] = pixelIn[1]; - pixelOut[2] = pixelIn[2]; - } - } - row[0] = rowHead; - (void) jpeg_write_scanlines(&cinfo, row, 1); - } - - jpeg_finish_compress(&cinfo); - fclose(out); - jpeg_destroy_compress(&cinfo); - - delete[] pixels; - - return true; -} - - -void PNGWriteData(png_structp png_ptr, png_bytep data, png_size_t length) -{ - auto* fp = (FILE*) png_get_io_ptr(png_ptr); - fwrite((void*) data, 1, length, fp); -} - - -bool CaptureGLBufferToPNG(const fs::path& filename, - int x, int y, - int width, int height, - const Renderer *renderer) -{ -#ifdef GL_ES - int rowStride = width * 4; - int imageSize = height * rowStride; - Renderer::PixelFormat format = Renderer::PixelFormat::RGBA; -#else - int rowStride = (width * 3 + 3) & ~0x3; - int imageSize = height * rowStride; - Renderer::PixelFormat format = Renderer::PixelFormat::RGB; -#endif - auto* pixels = new unsigned char[imageSize]; - - if (!renderer->captureFrame(x, y, width, height, - format, - pixels, true)) - { - return false; - } - -#ifdef _WIN32 - FILE* out = _wfopen(filename.c_str(), L"wb"); -#else - FILE* out = fopen(filename.c_str(), "wb"); -#endif - if (out == nullptr) - { - DPRINTF(LOG_LEVEL_ERROR, "Can't open screen capture file '%s'\n", filename); - delete[] pixels; - return false; - } - - auto* row_pointers = new png_bytep[height]; - for (int i = 0; i < height; i++) - { - unsigned char *rowHead = &pixels[rowStride * (height - i - 1)]; - // Strip alpha values if we are in RGBA format - if (format == Renderer::PixelFormat::RGBA) - { - for (int x = 0; x < width; x++) - { - const unsigned char* pixelIn = &rowHead[x * 4]; - unsigned char* pixelOut = &rowHead[x * 3]; - pixelOut[0] = pixelIn[0]; - pixelOut[1] = pixelIn[1]; - pixelOut[2] = pixelIn[2]; - } - } - row_pointers[i] = (png_bytep) rowHead; - } - - png_structp png_ptr; - png_infop info_ptr; - - png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, - nullptr, nullptr, nullptr); - - if (png_ptr == nullptr) - { - DPRINTF(LOG_LEVEL_ERROR, "Screen capture: error allocating png_ptr\n"); - fclose(out); - delete[] pixels; - delete[] row_pointers; - return false; - } - - info_ptr = png_create_info_struct(png_ptr); - if (info_ptr == nullptr) - { - DPRINTF(LOG_LEVEL_ERROR, "Screen capture: error allocating info_ptr\n"); - fclose(out); - delete[] pixels; - delete[] row_pointers; - png_destroy_write_struct(&png_ptr, (png_infopp) nullptr); - return false; - } - - if (setjmp(png_jmpbuf(png_ptr))) - { - DPRINTF(LOG_LEVEL_ERROR, "Error writing PNG file '%s'\n", filename); - fclose(out); - delete[] pixels; - delete[] row_pointers; - png_destroy_write_struct(&png_ptr, &info_ptr); - return false; - } - - // png_init_io(png_ptr, out); - png_set_write_fn(png_ptr, (void*) out, PNGWriteData, nullptr); - - png_set_compression_level(png_ptr, Z_BEST_COMPRESSION); - png_set_IHDR(png_ptr, info_ptr, - width, height, - 8, - PNG_COLOR_TYPE_RGB, - PNG_INTERLACE_NONE, - PNG_COMPRESSION_TYPE_DEFAULT, - PNG_FILTER_TYPE_DEFAULT); - - png_write_info(png_ptr, info_ptr); - png_write_image(png_ptr, row_pointers); - png_write_end(png_ptr, info_ptr); - - // Clean up everything . . . - png_destroy_write_struct(&png_ptr, &info_ptr); - delete[] row_pointers; - delete[] pixels; - fclose(out); - - return true; + return false; } diff --git a/src/celestia/imagecapture.h b/src/celestia/imagecapture.h index 8251e43b6..1f5e4904d 100644 --- a/src/celestia/imagecapture.h +++ b/src/celestia/imagecapture.h @@ -1,26 +1,22 @@ // imagecapture.h // -// Copyright (C) 2001, Chris Laurel +// Copyright (C) 2001-present, the Celestia Development Team +// Original version by Chris Laurel // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. -#ifndef _IMAGECAPTURE_H_ -#define _IMAGECAPTURE_H_ +#pragma once #include -#include +#include +class Renderer; -extern bool CaptureGLBufferToJPEG(const fs::path& filename, - int x, int y, - int width, int height, - const Renderer *renderer); -extern bool CaptureGLBufferToPNG(const fs::path& filename, - int x, int y, - int width, int height, - const Renderer *renderer); - -#endif // _IMAGECAPTURE_H_ +bool CaptureBufferToFile(const fs::path& filename, + int x, int y, + int width, int height, + const Renderer *renderer, + ContentType type); diff --git a/src/celimage/CMakeLists.txt b/src/celimage/CMakeLists.txt new file mode 100644 index 000000000..bfbcdee00 --- /dev/null +++ b/src/celimage/CMakeLists.txt @@ -0,0 +1,12 @@ +set(CELIMAGE_SOURCES + bmp.cpp + dds.cpp + dds_decompress.cpp + dds_decompress.h + imageformats.h + jpeg.cpp + png.cpp +) + +add_library(celimage OBJECT ${CELIMAGE_SOURCES}) +cotire(celimage) diff --git a/src/celimage/bmp.cpp b/src/celimage/bmp.cpp new file mode 100644 index 000000000..7430e2ccd --- /dev/null +++ b/src/celimage/bmp.cpp @@ -0,0 +1,192 @@ +// bmp.cpp +// +// Copyright (C) 2001-present, the Celestia Development Team +// Original version by Chris Laurel +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. + +#include +#include // ifstream +#include // ios +#include +#include +#include +#include + +using std::ifstream; +using std::ios; + +namespace +{ +// BMP file definitions--can't use windows.h because we might not be +// built on Windows! +typedef struct +{ + unsigned char magic[2]; + uint32_t size; + uint32_t reserved; + uint32_t offset; +} BMPFileHeader; + +typedef struct +{ + uint32_t size; + int32_t width; + int32_t height; + uint16_t planes; + uint16_t bpp; + uint32_t compression; + uint32_t imageSize; + int32_t widthPPM; + int32_t heightPPM; + uint32_t colorsUsed; + uint32_t colorsImportant; +} BMPImageHeader; + + +int32_t readInt32(ifstream& in) +{ + uint8_t b[4]; + in.read(reinterpret_cast(b), 4); + int32_t val = ((int32_t) b[3] << 24) + + ((int32_t) b[2] << 16) + + ((int32_t) b[1] << 8) + + ((int32_t) b[0]); + LE_TO_CPU_INT32(val, val); + return val; +} + +int16_t readInt16(ifstream& in) +{ + uint8_t b[2]; + in.read(reinterpret_cast(b), 2); + int16_t val = ((int16_t) b[1] << 8) + (int16_t) b[0]; + LE_TO_CPU_INT16(val, val); + return val; +} + + +Image* LoadBMPImage(ifstream& in) +{ + BMPFileHeader fileHeader; + BMPImageHeader imageHeader; + uint8_t* pixels; + + in >> fileHeader.magic[0]; + in >> fileHeader.magic[1]; + fileHeader.size = readInt32(in); + fileHeader.reserved = readInt32(in); + fileHeader.offset = readInt32(in); + + if (fileHeader.magic[0] != 'B' || fileHeader.magic[1] != 'M') + return nullptr; + + imageHeader.size = readInt32(in); + imageHeader.width = readInt32(in); + imageHeader.height = readInt32(in); + imageHeader.planes = readInt16(in); + imageHeader.bpp = readInt16(in); + imageHeader.compression = readInt32(in); + imageHeader.imageSize = readInt32(in); + imageHeader.widthPPM = readInt32(in); + imageHeader.heightPPM = readInt32(in); + imageHeader.colorsUsed = readInt32(in); + imageHeader.colorsImportant = readInt32(in); + + if (imageHeader.width <= 0 || imageHeader.height <= 0) + return nullptr; + + // We currently don't support compressed BMPs + if (imageHeader.compression != 0) + return nullptr; + // We don't handle 1-, 2-, or 4-bpp images + if (imageHeader.bpp != 8 && imageHeader.bpp != 24 && imageHeader.bpp != 32) + return nullptr; + + uint8_t* palette = nullptr; + if (imageHeader.bpp == 8) + { + DPRINTF(LOG_LEVEL_DEBUG, "Reading %u color palette\n", imageHeader.colorsUsed); + palette = new uint8_t[imageHeader.colorsUsed * 4]; + in.read(reinterpret_cast(palette), imageHeader.colorsUsed * 4); + } + + in.seekg(fileHeader.offset, ios::beg); + + uint32_t bytesPerRow = + (imageHeader.width * imageHeader.bpp / 8 + 1) & ~1; + uint32_t imageBytes = bytesPerRow * imageHeader.height; + + // slurp the image data + pixels = new uint8_t[imageBytes]; + in.read(reinterpret_cast(pixels), imageBytes); + + // check for truncated file + + auto* img = new Image(GL_RGB, imageHeader.width, imageHeader.height); + + // Copy the image and perform any necessary conversions + for (int32_t y = 0; y < imageHeader.height; y++) + { + uint8_t* src = &pixels[y * bytesPerRow]; + uint8_t* dst = img->getPixelRow(y); + + switch (imageHeader.bpp) + { + case 8: + for (int32_t x = 0; x < imageHeader.width; x++) + { + uint8_t* color = palette + (*src << 2); + dst[0] = color[2]; + dst[1] = color[1]; + dst[2] = color[0]; + src++; + dst += 3; + } + break; + case 24: + for (int32_t x = 0; x < imageHeader.width; x++) + { + dst[0] = src[2]; + dst[1] = src[1]; + dst[2] = src[0]; + src += 3; + dst += 3; + } + break; + case 32: + for (int32_t x = 0; x < imageHeader.width; x++) + { + dst[0] = src[2]; + dst[1] = src[1]; + dst[2] = src[0]; + src += 4; + dst += 3; + } + break; + } + } + + delete[] pixels; + delete[] palette; + + return img; +} +} // anonymous namespace + +Image* LoadBMPImage(const fs::path& filename) +{ + ifstream bmpFile(filename.string(), ios::in | ios::binary); + + if (bmpFile.good()) + { + Image* img = LoadBMPImage(bmpFile); + bmpFile.close(); + return img; + } + + return nullptr; +} diff --git a/src/celengine/dds.cpp b/src/celimage/dds.cpp similarity index 98% rename from src/celengine/dds.cpp rename to src/celimage/dds.cpp index e21e4f228..2801ba567 100644 --- a/src/celengine/dds.cpp +++ b/src/celimage/dds.cpp @@ -1,6 +1,7 @@ // dds.cpp // -// Copyright (C) 2001, Chris Laurel +// Copyright (C) 2001-present, the Celestia Development Team +// Original version by Chris Laurel // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License @@ -11,10 +12,10 @@ #include #include #include +#include +#include #include #include -#include -#include "glsupport.h" #include "dds_decompress.h" using namespace celestia; diff --git a/src/celengine/dds_decompress.cpp b/src/celimage/dds_decompress.cpp similarity index 100% rename from src/celengine/dds_decompress.cpp rename to src/celimage/dds_decompress.cpp diff --git a/src/celengine/dds_decompress.h b/src/celimage/dds_decompress.h similarity index 100% rename from src/celengine/dds_decompress.h rename to src/celimage/dds_decompress.h diff --git a/src/celimage/imageformats.h b/src/celimage/imageformats.h new file mode 100644 index 000000000..a109b3595 --- /dev/null +++ b/src/celimage/imageformats.h @@ -0,0 +1,30 @@ +// imageformats.h +// +// Copyright (C) 2001-present, the Celestia Development Team +// Original version by Chris Laurel +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. + +#pragma once + +#include + +Image* LoadJPEGImage(const fs::path& filename, + int channels = Image::ColorChannel); +Image* LoadBMPImage(const fs::path& filename); +Image* LoadPNGImage(const fs::path& filename); +Image* LoadDDSImage(const fs::path& filename); + +bool SaveJPEGImage(const fs::path& filename, + int width, int height, + int rowStride, + unsigned char* pixels, + bool stripAlpha = false); +bool SavePNGImage(const fs::path& filename, + int width, int height, + int rowStride, + unsigned char* pixels, + bool stripAlpha = false); diff --git a/src/celimage/jpeg.cpp b/src/celimage/jpeg.cpp new file mode 100644 index 000000000..e01fe40bb --- /dev/null +++ b/src/celimage/jpeg.cpp @@ -0,0 +1,240 @@ +// jpeg.cpp +// +// Copyright (C) 2001-present, the Celestia Development Team +// Original version by Chris Laurel +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. + +#include // fopen, fclose +#include // memcpy +extern "C" { +#include +} +#include +#include +#include + +namespace +{ +struct my_error_mgr +{ + struct jpeg_error_mgr pub; // "public" fields + jmp_buf setjmp_buffer; // for return to caller +}; + +using my_error_ptr = struct my_error_mgr *; + +METHODDEF(void) my_error_exit(j_common_ptr cinfo) +{ + // cinfo->err really points to a my_error_mgr struct, so coerce pointer + auto myerr = (my_error_ptr) cinfo->err; + + // Always display the message. + // We could postpone this until after returning, if we chose. + (*cinfo->err->output_message) (cinfo); + + // Return control to the setjmp point + longjmp(myerr->setjmp_buffer, 1); +} +} // anonymous namespace + +Image* LoadJPEGImage(const fs::path& filename, int /*unused*/) +{ + Image* img = nullptr; + + // This struct contains the JPEG decompression parameters and pointers to + // working space (which is allocated as needed by the JPEG library). + struct jpeg_decompress_struct cinfo; + + // We use our private extension JPEG error handler. + // Note that this struct must live as long as the main JPEG parameter + // struct, to avoid dangling-pointer problems. + struct my_error_mgr jerr; + // More stuff + JSAMPARRAY buffer; // Output row buffer + int row_stride; // physical row width in output buffer + long cont; + + // In this example we want to open the input file before doing anything else, + // so that the setjmp() error recovery below can assume the file is open. + // VERY IMPORTANT: use "b" option to fopen() if you are on a machine that + // requires it in order to read binary files. + + FILE *in; +#ifdef _WIN32 + in = _wfopen(filename.c_str(), L"rb"); +#else + in = fopen(filename.c_str(), "rb"); +#endif + if (!in) + return nullptr; + + // Step 1: allocate and initialize JPEG decompression object + // We set up the normal JPEG error routines, then override error_exit. + cinfo.err = jpeg_std_error(&jerr.pub); + jerr.pub.error_exit = my_error_exit; + // Establish the setjmp return context for my_error_exit to use. + if (setjmp(jerr.setjmp_buffer)) + { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + jpeg_destroy_decompress(&cinfo); + fclose(in); + delete img; + + return nullptr; + } + + // Now we can initialize the JPEG decompression object. + jpeg_create_decompress(&cinfo); + + // Step 2: specify data source (eg, a file) + jpeg_stdio_src(&cinfo, in); + + // Step 3: read file parameters with jpeg_read_header() + (void) jpeg_read_header(&cinfo, TRUE); + + // We can ignore the return value from jpeg_read_header since + // (a) suspension is not possible with the stdio data source, and + // (b) we passed TRUE to reject a tables-only JPEG file as an error. + + // Step 4: set parameters for decompression + + // In this example, we don't need to change any of the defaults set by + // jpeg_read_header(), so we do nothing here. + + // Step 5: Start decompressor + + (void) jpeg_start_decompress(&cinfo); + // We can ignore the return value since suspension is not possible + // with the stdio data source. + + // We may need to do some setup of our own at this point before reading + // the data. After jpeg_start_decompress() we have the correct scaled + // output image dimensions available, as well as the output colormap + // if we asked for color quantization. + // In this example, we need to make an output work buffer of the right size. + // JSAMPLEs per row in output buffer + row_stride = cinfo.output_width * cinfo.output_components; + // Make a one-row-high sample array that will go away when done with image + buffer = (*cinfo.mem->alloc_sarray) + ((j_common_ptr) & cinfo, JPOOL_IMAGE, row_stride, 1); + + // Step 6: while (scan lines remain to be read) + // jpeg_read_scanlines(...); + + // Here we use the library's state variable cinfo.output_scanline as the + // loop counter, so that we don't have to keep track ourselves. + + int format = GL_RGB; + if (cinfo.output_components == 1) + format = GL_LUMINANCE; + + img = new Image(format, cinfo.image_width, cinfo.image_height); + + // cont = cinfo.output_height - 1; + cont = 0; + while (cinfo.output_scanline < cinfo.output_height) + { + // jpeg_read_scanlines expects an array of pointers to scanlines. + // Here the array is only one element long, but you could ask for + // more than one scanline at a time if that's more convenient. + (void) jpeg_read_scanlines(&cinfo, buffer, 1); + + // Assume put_scanline_someplace wants a pointer and sample count. + // put_scanline_someplace(buffer[0], row_stride); + memcpy(img->getPixelRow(cont), buffer[0], row_stride); + cont++; + } + + // Step 7: Finish decompression + + (void) jpeg_finish_decompress(&cinfo); + // We can ignore the return value since suspension is not possible + // with the stdio data source. + + // Step 8: Release JPEG decompression object + + // This is an important step since it will release a good deal of memory. + jpeg_destroy_decompress(&cinfo); + + // After finish_decompress, we can close the input file. + // Here we postpone it until after no more JPEG errors are possible, + // so as to simplify the setjmp error logic above. (Actually, I don't + // think that jpeg_destroy can do an error exit, but why assume anything... + + fclose(in); + + // At this point you may want to check to see whether any corrupt-data + // warnings occurred (test whether jerr.pub.num_warnings is nonzero). + + return img; +} + +bool SaveJPEGImage(const fs::path& filename, + int width, int height, + int rowStride, + unsigned char *pixels, + bool removeAlpha) +{ + FILE* out; +#ifdef _WIN32 + out = _wfopen(filename.c_str(), L"wb"); +#else + out = fopen(filename.c_str(), "wb"); +#endif + if (out == nullptr) + { + DPRINTF(LOG_LEVEL_ERROR, "Can't open screen capture file '%s'\n", filename); + return false; + } + + struct jpeg_compress_struct cinfo; + + struct jpeg_error_mgr jerr; + JSAMPROW row[1]; + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + + jpeg_stdio_dest(&cinfo, out); + + cinfo.image_width = width; + cinfo.image_height = height; + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + + jpeg_set_defaults(&cinfo); + + jpeg_set_quality(&cinfo, 90, TRUE); + + jpeg_start_compress(&cinfo, TRUE); + + while (cinfo.next_scanline < cinfo.image_height) + { + unsigned char *rowHead = &pixels[rowStride * (cinfo.image_height - cinfo.next_scanline - 1)]; + // Strip alpha values if we are in RGBA format + if (removeAlpha) + { + for (int x = 0; x < width; x++) + { + const unsigned char* pixelIn = &rowHead[x * 4]; + unsigned char* pixelOut = &rowHead[x * 3]; + pixelOut[0] = pixelIn[0]; + pixelOut[1] = pixelIn[1]; + pixelOut[2] = pixelIn[2]; + } + } + row[0] = rowHead; + (void) jpeg_write_scanlines(&cinfo, row, 1); + } + + jpeg_finish_compress(&cinfo); + fclose(out); + jpeg_destroy_compress(&cinfo); + + return true; +} diff --git a/src/celimage/png.cpp b/src/celimage/png.cpp new file mode 100644 index 000000000..e04683afd --- /dev/null +++ b/src/celimage/png.cpp @@ -0,0 +1,258 @@ +// png.cpp +// +// Copyright (C) 2001-present, the Celestia Development Team +// Original version by Chris Laurel +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. + +#include +#include +#include +#include +#include +#include +#include +#include + +using std::cerr; +using std::clog; + +namespace +{ +void PNGReadData(png_structp png_ptr, png_bytep data, png_size_t length) +{ + auto* fp = (FILE*) png_get_io_ptr(png_ptr); + if (fread((void*) data, 1, length, fp) != length) + cerr << "Error reading PNG data"; +} + +void PNGWriteData(png_structp png_ptr, png_bytep data, png_size_t length) +{ + auto* fp = (FILE*) png_get_io_ptr(png_ptr); + fwrite((void*) data, 1, length, fp); +} +} // anonymous namespace + +Image* LoadPNGImage(const fs::path& filename) +{ + char header[8]; + png_structp png_ptr; + png_infop info_ptr; + png_uint_32 width, height; + int bit_depth, color_type, interlace_type; + Image* img = nullptr; + png_bytep* row_pointers = nullptr; + +#ifdef _WIN32 + FILE *fp = _wfopen(filename.c_str(), L"rb"); +#else + FILE *fp = fopen(filename.c_str(), "rb"); +#endif + if (fp == nullptr) + { + fmt::fprintf(clog, _("Error opening image file %s\n"), filename); + return nullptr; + } + + size_t elements_read; + elements_read = fread(header, 1, sizeof(header), fp); + if (elements_read == 0 || png_sig_cmp((unsigned char*) header, 0, sizeof(header))) + { + fmt::fprintf(clog, _("Error: %s is not a PNG file.\n"), filename); + fclose(fp); + return nullptr; + } + + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, + nullptr, nullptr, nullptr); + if (png_ptr == nullptr) + { + fclose(fp); + return nullptr; + } + + info_ptr = png_create_info_struct(png_ptr); + if (info_ptr == nullptr) + { + fclose(fp); + png_destroy_read_struct(&png_ptr, (png_infopp) nullptr, (png_infopp) nullptr); + return nullptr; + } + + if (setjmp(png_jmpbuf(png_ptr))) + { + fclose(fp); + delete img; + png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) nullptr); + fmt::fprintf(clog, _("Error reading PNG image file %s\n"), filename); + return nullptr; + } + + // png_init_io(png_ptr, fp); + png_set_read_fn(png_ptr, (void*) fp, PNGReadData); + png_set_sig_bytes(png_ptr, sizeof(header)); + + png_read_info(png_ptr, info_ptr); + + png_get_IHDR(png_ptr, info_ptr, + &width, &height, &bit_depth, + &color_type, &interlace_type, + nullptr, nullptr); + + GLenum glformat = GL_RGB; + switch (color_type) + { + case PNG_COLOR_TYPE_GRAY: + glformat = GL_LUMINANCE; + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + glformat = GL_LUMINANCE_ALPHA; + break; + case PNG_COLOR_TYPE_RGB: + glformat = GL_RGB; + break; + case PNG_COLOR_TYPE_PALETTE: + case PNG_COLOR_TYPE_RGB_ALPHA: + glformat = GL_RGBA; + break; + default: + // badness + break; + } + + img = new Image(glformat, width, height); + + // TODO: consider using paletted textures if they're available + if (color_type == PNG_COLOR_TYPE_PALETTE) + { + png_set_palette_to_rgb(png_ptr); + } + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) + { + png_set_expand_gray_1_2_4_to_8(png_ptr); + } + + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) + { + png_set_tRNS_to_alpha(png_ptr); + } + + // TODO: consider passing images with < 8 bits/component to + // GL without expanding + if (bit_depth == 16) + png_set_strip_16(png_ptr); + else if (bit_depth < 8) + png_set_packing(png_ptr); + + row_pointers = new png_bytep[height]; + for (unsigned int i = 0; i < height; i++) + row_pointers[i] = (png_bytep) img->getPixelRow(i); + + png_read_image(png_ptr, row_pointers); + + delete[] row_pointers; + + png_read_end(png_ptr, nullptr); + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + + fclose(fp); + + return img; +} + +bool SavePNGImage(const fs::path& filename, + int width, int height, + int rowStride, + unsigned char *pixels, + bool removeAlpha) +{ +#ifdef _WIN32 + FILE* out = _wfopen(filename.c_str(), L"wb"); +#else + FILE* out = fopen(filename.c_str(), "wb"); +#endif + if (out == nullptr) + { + DPRINTF(LOG_LEVEL_ERROR, "Can't open screen capture file '%s'\n", filename); + return false; + } + + auto* row_pointers = new png_bytep[height]; + for (int i = 0; i < height; i++) + { + unsigned char *rowHead = &pixels[rowStride * (height - i - 1)]; + // Strip alpha values if we are in RGBA format + if (removeAlpha) + { + for (int x = 0; x < width; x++) + { + const unsigned char* pixelIn = &rowHead[x * 4]; + unsigned char* pixelOut = &rowHead[x * 3]; + pixelOut[0] = pixelIn[0]; + pixelOut[1] = pixelIn[1]; + pixelOut[2] = pixelIn[2]; + } + } + row_pointers[i] = (png_bytep) rowHead; + } + + png_structp png_ptr; + png_infop info_ptr; + + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, + nullptr, nullptr, nullptr); + + if (png_ptr == nullptr) + { + DPRINTF(LOG_LEVEL_ERROR, "Screen capture: error allocating png_ptr\n"); + fclose(out); + delete[] row_pointers; + return false; + } + + info_ptr = png_create_info_struct(png_ptr); + if (info_ptr == nullptr) + { + DPRINTF(LOG_LEVEL_ERROR, "Screen capture: error allocating info_ptr\n"); + fclose(out); + delete[] row_pointers; + png_destroy_write_struct(&png_ptr, (png_infopp) nullptr); + return false; + } + + if (setjmp(png_jmpbuf(png_ptr))) + { + DPRINTF(LOG_LEVEL_ERROR, "Error writing PNG file '%s'\n", filename); + fclose(out); + delete[] row_pointers; + png_destroy_write_struct(&png_ptr, &info_ptr); + return false; + } + + // png_init_io(png_ptr, out); + png_set_write_fn(png_ptr, (void*) out, PNGWriteData, nullptr); + + png_set_compression_level(png_ptr, Z_BEST_COMPRESSION); + png_set_IHDR(png_ptr, info_ptr, + width, height, + 8, + PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + + png_write_info(png_ptr, info_ptr); + png_write_image(png_ptr, row_pointers); + png_write_end(png_ptr, info_ptr); + + // Clean up everything . . . + png_destroy_write_struct(&png_ptr, &info_ptr); + delete[] row_pointers; + fclose(out); + + return true; +}