Browse Source

Completed the image saving stub with BMP, Jpeg, Targa and Unicode filenames.
Always saving to a memory buffer internally before using the file API.
Everything in the framework that is loaded or saved can now be stored in memory buffers.
This allow creating systems for storing any asset in an archive of multiple files.

David Piuva 3 years ago
parent
commit
3d866c8117

+ 11 - 3
Source/DFPSR/api/fileAPI.cpp

@@ -122,19 +122,27 @@ Buffer file_loadBuffer(const ReadableString& filename, bool mustExist) {
 	}
 }
 
-void file_saveBuffer(const ReadableString& filename, Buffer buffer) {
+bool file_saveBuffer(const ReadableString& filename, Buffer buffer, bool mustWork) {
 	String modifiedFilename = file_optimizePath(filename, LOCAL_PATH_SYNTAX);
 	if (!buffer_exists(buffer)) {
-		throwError(U"buffer_save: Can't save a buffer that don't exist to a file.\n");
+		if (mustWork) {
+			throwError(U"buffer_save: Can't save a buffer that don't exist to a file.\n");
+		}
+		return false;
 	} else {
 		FILE *file = accessFile(modifiedFilename, true);
 		if (file != nullptr) {
 			fwrite((void*)buffer_dangerous_getUnsafeData(buffer), buffer_getSize(buffer), 1, file);
 			fclose(file);
 		} else {
-			throwError("Failed to save ", modifiedFilename, ".\n");
+			if (mustWork) {
+				throwError("Failed to save ", modifiedFilename, ".\n");
+			}
+			return false;
 		}
 	}
+	// Success if there are no errors.
+	return true;
 }
 
 const char32_t* file_separator() {

+ 5 - 2
Source/DFPSR/api/fileAPI.h

@@ -72,8 +72,11 @@ namespace dsr {
 
 	// Path-syntax: According to the local computer.
 	// Side-effect: Saves buffer to file_optimizePath(filename) as a binary file.
-	// Pre-condition: buffer exists
-	void file_saveBuffer(const ReadableString& filename, Buffer buffer);
+	// Pre-condition: buffer exists.
+	//   If mustWork is true, then failure to load will throw an exception.
+	//   If mustWork is false, then failure to load will return false.
+	// Post-condition: Returns true iff the buffer could be saved as a file.
+	bool file_saveBuffer(const ReadableString& filename, Buffer buffer, bool mustWork = true);
 
 	// Path-syntax: According to the local computer.
 	// Pre-condition: file_getEntryType(path) == EntryType::SymbolicLink

+ 86 - 9
Source/DFPSR/api/imageAPI.cpp

@@ -1,7 +1,7 @@
 
 // zlib open source license
 //
-// Copyright (c) 2017 to 2019 David Forsgren Piuva
+// Copyright (c) 2017 to 2022 David Forsgren Piuva
 // 
 // This software is provided 'as-is', without any express or implied
 // warranty. In no event will the authors be held liable for any damages
@@ -54,24 +54,101 @@ AlignedImageRgbaU8 dsr::image_create_RgbaU8_native(int32_t width, int32_t height
 }
 
 // Loading from data pointer
-OrderedImageRgbaU8 dsr::image_decode_RgbaU8(const SafePointer<uint8_t> data, int size, bool mustParse) {
-	return image_stb_decode_RgbaU8(data, size, mustParse);
+OrderedImageRgbaU8 dsr::image_decode_RgbaU8(const SafePointer<uint8_t> data, int size) {
+	if (data.isNotNull()) {
+		return image_stb_decode_RgbaU8(data, size);
+	} else {
+		return OrderedImageRgbaU8();
+	}
 }
 // Loading from buffer
-OrderedImageRgbaU8 dsr::image_decode_RgbaU8(const Buffer& fileContent, bool mustParse) {
-	return image_decode_RgbaU8(buffer_getSafeData<uint8_t>(fileContent, "image file buffer"), buffer_getSize(fileContent), mustParse);
+OrderedImageRgbaU8 dsr::image_decode_RgbaU8(const Buffer& fileContent) {
+	return image_decode_RgbaU8(buffer_getSafeData<uint8_t>(fileContent, "image file buffer"), buffer_getSize(fileContent));
 }
 // Loading from file
 OrderedImageRgbaU8 dsr::image_load_RgbaU8(const String& filename, bool mustExist) {
-	//return image_stb_load_RgbaU8(filename, mustExist);
+	OrderedImageRgbaU8 result;
 	Buffer fileContent = file_loadBuffer(filename, mustExist);
-	return image_decode_RgbaU8(fileContent, mustExist);
+	if (buffer_exists(fileContent)) {
+		result = image_decode_RgbaU8(fileContent);
+		if (mustExist && !image_exists(result)) {
+			throwError(U"buffer_save: Can't save a buffer that don't exist to a file.\n");
+		}
+	}
+	return result;
+}
+
+// Pre-condition: image exists.
+// Post-condition: Returns true if the stride is larger than the image's width.
+static bool imageIsPadded(const ImageRgbaU8 &image) {
+	return image_getWidth(image) * 4 < image_getStride(image);
+}
+
+Buffer dsr::image_encode(const ImageRgbaU8 &image, ImageFileFormat format, int quality) {
+	if (buffer_exists) {
+		ImageRgbaU8 orderedImage;
+		if (image_getPackOrderIndex(image) != PackOrderIndex::RGBA) {
+			// Repack into RGBA.
+			orderedImage = image_clone(image);
+		} else {
+			// Take the image handle as is.
+			orderedImage = image;
+		}
+		if (imageIsPadded(orderedImage) && format != ImageFileFormat::PNG) {
+			// If orderedImage is padded and it's not requested as PNG, the padding has to be removed first.
+			return image_stb_encode(image_removePadding(orderedImage), format, quality);
+		} else {
+			// Send orderedImage directly to encoding.
+			return image_stb_encode(orderedImage, format, quality);
+		}
+	} else {
+		return Buffer();
+	}
 }
 
+static ReadableString getFileExtension(const String& filename) {
+	int lastDotIndex = string_findLast(filename, U'.');
+	if (lastDotIndex != -1) {
+		return string_removeOuterWhiteSpace(string_after(filename, lastDotIndex));
+	} else {
+		return U"?";
+	}
+}
 
+static ImageFileFormat detectImageFileExtension(const String& filename) {
+	ImageFileFormat result = ImageFileFormat::Unknown;
+	int lastDotIndex = string_findLast(filename, U'.');
+	if (lastDotIndex != -1) {
+		ReadableString extension = string_upperCase(getFileExtension(filename));
+		if (string_match(extension, U"JPG") || string_match(extension, U"JPEG")) {
+			result = ImageFileFormat::JPG;
+		} else if (string_match(extension, U"PNG")) {
+			result = ImageFileFormat::PNG;
+		} else if (string_match(extension, U"TARGA") || string_match(extension, U"TGA")) {
+			result = ImageFileFormat::TGA;
+		} else if (string_match(extension, U"BMP")) {
+			result = ImageFileFormat::BMP;
+		}
+	}
+	return result;
+}
 
-bool dsr::image_save(const ImageRgbaU8 &image, const String& filename) {
-	return image_stb_save(image, filename);
+bool dsr::image_save(const ImageRgbaU8 &image, const String& filename, bool mustWork, int quality) {
+	ImageFileFormat extension = detectImageFileExtension(filename);
+	Buffer buffer;
+	if (extension == ImageFileFormat::Unknown) {
+		ReadableString extension = getFileExtension(filename);
+		if (mustWork) { throwError(U"The extension *.", extension, " in ", filename, " is not a supported image format.\n"); }
+		return false;
+	} else {
+		buffer = image_encode(image, extension, quality);
+	}
+	if (buffer_exists(buffer)) {
+		return file_saveBuffer(filename, buffer, mustWork);
+	} else {
+		if (mustWork) { throwError(U"Failed to encode an image that was going to be saved as ", filename, "\n"); }
+		return false;
+	}
 }
 
 #define GET_OPTIONAL(SOURCE,DEFAULT) \

+ 28 - 8
Source/DFPSR/api/imageAPI.h

@@ -1,7 +1,7 @@
 
 // zlib open source license
 //
-// Copyright (c) 2017 to 2019 David Forsgren Piuva
+// Copyright (c) 2017 to 2022 David Forsgren Piuva
 // 
 // This software is provided 'as-is', without any express or implied
 // warranty. In no event will the authors be held liable for any damages
@@ -137,17 +137,35 @@ namespace dsr {
 
 // Loading
 	// Load an image from a file by giving the filename including folder path and extension.
+	// If mustExist is true, an exception will be raised on failure.
+	// If mustExist is false, failure will return an empty handle.
 	OrderedImageRgbaU8 image_load_RgbaU8(const String& filename, bool mustExist = true);
 	// Load an image from a memory buffer, which can be loaded with file_loadBuffer to get the same result as loading directly from the file.
-	//   A convenient way of loading compressed images from larger files.
-	OrderedImageRgbaU8 image_decode_RgbaU8(const Buffer& fileContent, bool mustParse = true);
+	// A convenient way of loading compressed images from larger files.
+	// Failure will return an empty handle.
+	OrderedImageRgbaU8 image_decode_RgbaU8(const Buffer& fileContent);
 	// A faster and more flexible way to load compressed images from memory.
 	// If you just want to point directly to a memory location to avoid allocating many small buffers, you can use a safe pointer and a size in bytes.
-	OrderedImageRgbaU8 image_decode_RgbaU8(const SafePointer<uint8_t> data, int size, bool mustParse = true);
+	// Failure will return an empty handle.
+	OrderedImageRgbaU8 image_decode_RgbaU8(const SafePointer<uint8_t> data, int size);
 
 // Saving
 	// Save the image to the path specified by filename and return true iff the operation was successful.
-	bool image_save(const ImageRgbaU8 &image, const String& filename);
+	// The file extension is case insensitive after the last dot in filename.
+	//   Accepted file extensions:
+	//     *.jpg or *.jpeg
+	//     *.png
+	//     *.tga or *.targa
+	//     *.bmp
+	// If mustWork is true, an exception will be raised on failure.
+	// If mustWork is false, failure will return false.
+	// The optional quality setting goes from 1% to 100% and is at the maximum by default.
+	bool image_save(const ImageRgbaU8 &image, const String& filename, bool mustWork = true, int quality = 100);
+	// Save the image to a memory buffer.
+	// Post-condition: Returns a buffer with the encoded image format as it would be saved to a file, or empty on failure.
+	//                 No exceptions will be raised on failure, because an error message without a filename would not explain much.
+	// The optional quality setting goes from 1% to 100% and is at the maximum by default.
+	Buffer image_encode(const ImageRgbaU8 &image, ImageFileFormat format, int quality = 90);
 
 // Fill all pixels with a uniform color
 	void image_fill(ImageU8& image, int32_t color);
@@ -156,13 +174,15 @@ namespace dsr {
 	void image_fill(ImageRgbaU8& image, const ColorRgbaI32& color);
 
 // Clone
-	// Get a deep clone of an image's content while discarding any pack order, padding and texture pyramids
+	// Get a deep clone of an image's content while discarding any pack order, padding and texture pyramids.
+	// If the input image had a different pack order, it will automatically be converted into RGBA to preserve the colors.
 	AlignedImageU8 image_clone(const ImageU8& image);
 	AlignedImageU16 image_clone(const ImageU16& image);
 	AlignedImageF32 image_clone(const ImageF32& image);
 	OrderedImageRgbaU8 image_clone(const ImageRgbaU8& image);
-	// Returns a copy of the image without any padding, which means that alignment cannot be guaranteed
-	//   Used when external image libraries don't allow it
+	// Returns a copy of the image without any padding, which means that alignment cannot be guaranteed.
+	// The pack order is the same as the input, becuase it just copies the memory one row at a time to be fast.
+	// Used when external image libraries don't allow giving stride as a separate argument.
 	ImageRgbaU8 image_removePadding(const ImageRgbaU8& image);
 
 // Channel packing

+ 9 - 1
Source/DFPSR/api/types.h

@@ -1,7 +1,7 @@
 
 // zlib open source license
 //
-// Copyright (c) 2019 David Forsgren Piuva
+// Copyright (c) 2019 to 2022 David Forsgren Piuva
 // 
 // This software is provided 'as-is', without any express or implied
 // warranty. In no event will the authors be held liable for any damages
@@ -40,6 +40,14 @@
 
 namespace dsr {
 
+enum class ImageFileFormat {
+	Unknown, // Used as an error code for unidentified formats.
+	JPG, // Lossy compressed image format storing brightness separated from red and blue offsets using the discrete cosine transform of each block.
+	PNG, // Lossless compressed image format. Some image editors don't save RGB values where alpha is zero, which will bleed through black edges in bi-linear interpolation when the interpolated alpha is not zero.
+	TGA, // Lossless compressed format. Applications usually give Targa better control over the alpha channel than PNG, but it's more common that the Targa specification is interpreted in incompatible ways.
+	BMP // Uncompressed image format for storing data that does not really represent an image and you just want it to be exact.
+};
+
 enum class PackOrderIndex {
 	RGBA, // Windows
 	BGRA, // Ubuntu

+ 1 - 1
Source/DFPSR/image/ImageRgbaU8.cpp

@@ -62,7 +62,7 @@ ImageRgbaU8Impl ImageRgbaU8Impl::getWithoutPadding() const {
 		return *this;
 	} else {
 		// Copy each row without padding
-		ImageRgbaU8Impl result(this->width, this->height, 1);
+		ImageRgbaU8Impl result = ImageRgbaU8Impl(this->width, this->height, this->packOrder.packOrderIndex);
 		const SafePointer<uint8_t> sourceRow = imageInternal::getSafeData<uint8_t>(*this);
 		int32_t sourceStride = this->stride;
 		SafePointer<uint8_t> targetRow = imageInternal::getSafeData<uint8_t>(result);

+ 33 - 8
Source/DFPSR/image/stbImage/stbImageWrapper.cpp

@@ -9,7 +9,7 @@
 
 namespace dsr {
 
-OrderedImageRgbaU8 image_stb_decode_RgbaU8(const SafePointer<uint8_t> data, int size, bool mustParse) {
+OrderedImageRgbaU8 image_stb_decode_RgbaU8(const SafePointer<uint8_t> data, int size) {
 	#ifdef SAFE_POINTER_CHECKS
 		// If the safe pointer has debug information, use it to assert that size is within bound.
 		data.assertInside("image_stb_decode_RgbaU8 (data)", data.getUnsafe(), (size_t)size);
@@ -17,9 +17,6 @@ OrderedImageRgbaU8 image_stb_decode_RgbaU8(const SafePointer<uint8_t> data, int
 	int width, height, bpp;
 	uint8_t *rawPixelData = stbi_load_from_memory(data.getUnsafe(), size, &width, &height, &bpp, 4);
 	if (rawPixelData == nullptr) {
-		if (mustParse) {
-			throwError("An image could not be parsed!\n");
-		}
 		return OrderedImageRgbaU8(); // Return null
 	}
 	// Create a padded buffer
@@ -41,10 +38,38 @@ OrderedImageRgbaU8 image_stb_decode_RgbaU8(const SafePointer<uint8_t> data, int
 	return result;
 }
 
-bool image_stb_save(const ImageRgbaU8 &image, const String& filename) {
-	// Remove all padding before saving to avoid crashing
-	ImageRgbaU8 unpadded = ImageRgbaU8(image_removePadding(image));
-	return stbi_write_png(filename.toStdString().c_str(), image_getWidth(unpadded), image_getHeight(unpadded), 4, image_dangerous_getData(unpadded), image_getStride(unpadded)) != 0;
+// Pre-condition: Images that STB image don't have stride implementations for must be given unpadded images.
+Buffer image_stb_encode(const ImageRgbaU8 &image, ImageFileFormat format, int quality) {
+	int width = image_getWidth(image);
+	int height = image_getHeight(image);
+	List<uint8_t> targetList;
+	// Reserve enough memory for an uncompressed file to reduce the need for reallcation.
+	targetList.reserve(width * height * 4 + 2048);
+	stbi_write_func* writer = [](void* context, void* data, int size) {
+		List<uint8_t>* target = (List<uint8_t>*)context;
+		for (int i = 0; i < size; i++) {
+			target->push(((uint8_t*)data)[i]);
+		}
+	};
+	bool success = false;
+	if (format == ImageFileFormat::JPG) {
+		success = stbi_write_jpg_to_func(writer, &targetList, width, height, 4, image_dangerous_getData(image), quality);
+	} else if (format == ImageFileFormat::PNG) {
+		success = stbi_write_png_to_func(writer, &targetList, width, height, 4, image_dangerous_getData(image), image_getStride(image));
+	} else if (format == ImageFileFormat::TGA) {
+		success = stbi_write_tga_to_func(writer, &targetList, width, height, 4, image_dangerous_getData(image));
+	} else if (format == ImageFileFormat::BMP) {
+		success = stbi_write_bmp_to_func(writer, &targetList, width, height, 4, image_dangerous_getData(image));
+	}
+	if (success) {
+		// Copy data to a new buffer once the total size is known.
+		Buffer result = buffer_create(targetList.length());
+		uint8_t* targetData = buffer_dangerous_getUnsafeData(result);
+		memcpy(targetData, &targetList[0], targetList.length());
+		return result; // Return the buffer on success.
+	} else {
+		return Buffer(); // Return a null handle on failure.
+	}
 }
 
 }

+ 7 - 2
Source/DFPSR/image/stbImage/stbImageWrapper.h

@@ -4,11 +4,16 @@
 
 #include "../../api/imageAPI.h"
 #include "../../api/stringAPI.h"
+#include "../../api/types.h"
 
 namespace dsr {
 
-OrderedImageRgbaU8 image_stb_decode_RgbaU8(const SafePointer<uint8_t> data, int size, bool mustParse = true);
-bool image_stb_save(const ImageRgbaU8 &image, const String& filename);
+OrderedImageRgbaU8 image_stb_decode_RgbaU8(const SafePointer<uint8_t> data, int size);
+
+// Pre-conditions:
+// * The image must be packed in RGBA order at runtime, but can't be in the OrderedImageRgbaU8 format because Ordered inherits from Aligned.
+// * Only the PNG format may use padding in this call.
+Buffer image_stb_encode(const ImageRgbaU8 &image, ImageFileFormat format, int quality);
 
 }