Raw File
imagealbum-dialog.cpp
/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "gui/imagealbum-dialog.h"

#include "gui/dialog.h"
#include "gui/filebrowser-dialog.h"
#include "gui/gui-manager.h"
#include "gui/ThemeEval.h"
#include "gui/widget.h"

#include "gui/widgets/scrollcontainer.h"

#include "image/bmp.h"
#include "image/png.h"

#include "common/dialogs.h"
#include "common/savefile.h"
#include "common/stream.h"
#include "common/translation.h"

namespace GUI {

ImageAlbumImageSupplier::~ImageAlbumImageSupplier() {
}

class ImageAlbumDialog : public Dialog {
public:
	ImageAlbumDialog(const Common::U32String &title, ImageAlbumImageSupplier *imageSupplier, uint initialSlot);
	~ImageAlbumDialog();

	void open() override;
	void close() override;

	void handleCommand(CommandSender *sender, uint32 cmd, uint32 data) override;

private:
	ImageAlbumDialog() = delete;
	ImageAlbumDialog(const ImageAlbumDialog &) = delete;

	void changeToSlot(uint slot);
	void saveImageInSlot(uint slot);

	ImageAlbumImageSupplier *_imageSupplier;
	uint _currentSlot;
	uint _numSlots;

	ButtonWidget *_prevButton;
	ButtonWidget *_nextButton;
	ButtonWidget *_saveButton;

	StaticTextWidget *_imageNumberLabel;

	ContainerWidget *_imageContainer;
	GraphicsWidget *_imageGraphic;

	bool _canAlwaysSaveImage;

	enum {
		kPrevCmd = 'PREV',
		kNextCmd = 'NEXT',
		kSaveCmd = 'SAVE',
	};
};


ImageAlbumDialog::ImageAlbumDialog(const Common::U32String &title, ImageAlbumImageSupplier *imageSupplier, uint initialSlot)
	: Dialog("ImageAlbum"), _imageSupplier(imageSupplier), _currentSlot(initialSlot), _numSlots(0), _imageGraphic(nullptr), _canAlwaysSaveImage(false) {

	_backgroundType = ThemeEngine::kDialogBackgroundSpecial;

	_numSlots = imageSupplier->getNumSlots();

	assert(_numSlots > 0);

	new StaticTextWidget(this, "ImageAlbum.Title", title);

	if (initialSlot >= _numSlots)
		initialSlot = _numSlots - 1;

	// Buttons
	if (_numSlots > 1) {
		_prevButton = new ButtonWidget(this, "ImageAlbum.Prev", _("Prev"), Common::U32String(), kPrevCmd);
		_nextButton = new ButtonWidget(this, "ImageAlbum.Next", _("Next"), Common::U32String(), kNextCmd);
		_imageNumberLabel = new StaticTextWidget(this, "ImageAlbum.ImageNumber", Common::U32String());
	} else {
		_prevButton = nullptr;
		_nextButton = nullptr;
		_imageNumberLabel = nullptr;
	}

	_saveButton = nullptr;


	bool canSaveAnyFormat = false;
	for (uint fmtID = Common::FormatInfo::kFirstImageFormat; fmtID <= Common::FormatInfo::kLastImageFormat; fmtID++) {
		Common::FormatInfo::FormatID format = static_cast<Common::FormatInfo::FormatID>(fmtID);

		if (Common::FormatInfo::getFormatSupportLevel(format) > Common::FormatInfo::kFormatSupportLevelNone) {
			canSaveAnyFormat = true;
			if (Common::FormatInfo::getImageSaveFunction(format) != nullptr) {
				_canAlwaysSaveImage = true;
				break;
			}
		}
	}

	if (canSaveAnyFormat) {
		_saveButton = new ButtonWidget(this, "ImageAlbum.Save", _("Save Image..."), Common::U32String(), kSaveCmd);
		_saveButton->setEnabled(!_canAlwaysSaveImage);
	}

	new ButtonWidget(this, "ImageAlbum.Close", _("Close"), Common::U32String(), kCloseCmd);

	_imageContainer = new ContainerWidget(this, "ImageAlbum.ImageContainer");

	_imageGraphic = nullptr;
}

ImageAlbumDialog::~ImageAlbumDialog() {
}

void ImageAlbumDialog::open() {
	Dialog::open();

	changeToSlot(_currentSlot);
}

void ImageAlbumDialog::changeToSlot(uint slot) {
	bool canSaveImage = _canAlwaysSaveImage;

	if (_imageGraphic) {
		_imageContainer->removeWidget(_imageGraphic);
		delete _imageGraphic;
		_imageGraphic = nullptr;
	}

	Common::Rect graphicRect = Common::Rect(0, 0, _imageContainer->getWidth(), _imageContainer->getHeight());

	int inset = g_gui.xmlEval()->getVar("Globals.ImageAlbum.ImageInset", 0);
	graphicRect.grow(-inset);

	if (graphicRect.isValidRect()) {
		uint32 graphicRectWidth = graphicRect.width();
		uint32 graphicRectHeight = graphicRect.height();

		const Graphics::Surface *surf = nullptr;
		bool hasPalette = false;
		byte palette[256 * 3];
		ImageAlbumImageMetadata metadata;

		for (byte &paletteByte : palette)
			paletteByte = 0;

		if (_imageSupplier->loadImageSlot(slot, surf, hasPalette, palette, metadata)) {
			if (!canSaveImage) {
				// If we can't always save the image (meaning we don't have an image write-out function) then see if we can
				// at least save this file in its native format.
				Common::FormatInfo::FormatID format = Common::FormatInfo::kFormatUnknown;
				if (_imageSupplier->getFileFormatForImageSlot(slot, format)) {
					if (Common::FormatInfo::getFormatSupportLevel(format) > Common::FormatInfo::kFormatSupportLevelNone)
						canSaveImage = true;
				}
			}

			uint32 imageWidth = surf->w;
			uint32 imageHeight = surf->h;

			uint32 scaledWidth = graphicRectWidth;
			uint32 scaledHeight = graphicRectHeight;

			bool needs90Rotate = (metadata._viewTransformation == kImageAlbumViewTransformationRotate90CCW || metadata._viewTransformation == kImageAlbumViewTransformationRotate90CW);

			uint32 imageRotatedWidth = imageWidth;
			uint32 imageRotatedHeight = imageHeight;
			if (needs90Rotate) {
				imageRotatedWidth = imageHeight;
				imageRotatedHeight = imageWidth;
			}

			// if (imageRotatedWidth / imageRotatedHeight > graphicRectWidth / graphicRectHeight)
			if (imageRotatedWidth * graphicRectHeight >= graphicRectWidth * imageRotatedHeight) {
				// Image aspect ratio is wider than the graphic space, or same
				scaledWidth = graphicRectWidth;
				scaledHeight = imageRotatedHeight * graphicRectWidth / imageRotatedWidth;
			} else {
				// Image aspect ratio is taller than the graphic space
				scaledWidth = imageRotatedWidth * graphicRectHeight / imageRotatedHeight;
				scaledHeight = graphicRectHeight;
			}

			if (scaledWidth < 1)
				scaledWidth = 1;
			if (scaledHeight < 1)
				scaledHeight = 1;

			Graphics::ManagedSurface rescaledGraphic;
			rescaledGraphic.create(scaledWidth, scaledHeight, surf->format);
			if (hasPalette)
				rescaledGraphic.setPalette(palette, 0, 256);

			if (needs90Rotate) {
				bool isClockwise = metadata._viewTransformation == kImageAlbumViewTransformationRotate90CW;

				for (uint32 destX = 0; destX < scaledWidth; destX++) {
					uint32 srcY = destX * imageHeight / scaledWidth;
					if (isClockwise)
						srcY = imageHeight - 1 - srcY;

					for (uint32 destY = 0; destY < scaledHeight; destY++) {
						uint32 srcX = destY * imageWidth / scaledHeight;

						if (!isClockwise)
							srcX = imageWidth - 1 - srcX;

						rescaledGraphic.setPixel(destX, destY, surf->getPixel(srcX, srcY));
					}
				}
			} else if (metadata._viewTransformation == kImageAlbumViewTransformationRotate180) {
				for (uint32 destX = 0; destX < scaledWidth; destX++) {
					uint32 srcX = (imageWidth - 1 - (destX * imageWidth / scaledWidth));

					for (uint32 destY = 0; destY < scaledHeight; destY++) {
						uint32 srcY = (imageHeight - 1 - (destY * imageHeight / scaledHeight));

						rescaledGraphic.setPixel(destX, destY, surf->getPixel(srcX, srcY));
					}
				}
			} else {
				rescaledGraphic.blitFrom(*surf, Common::Rect(0, 0, imageWidth, imageHeight), Common::Rect(0, 0, scaledWidth, scaledHeight));
			}

			_imageSupplier->releaseImageSlot(slot);

			if (rescaledGraphic.format.bytesPerPixel == 1)
				rescaledGraphic.convertToInPlace(Graphics::createPixelFormat<888>(), palette, 0, 256);

			int32 xCoord = (static_cast<int32>(_imageContainer->getWidth()) - static_cast<int32>(scaledWidth)) / 2u;
			int32 yCoord = (static_cast<int32>(_imageContainer->getHeight()) - static_cast<int32>(scaledHeight)) / 2u;

			_imageGraphic = new GraphicsWidget(_imageContainer, xCoord, yCoord, xCoord + static_cast<int32>(scaledWidth), yCoord + static_cast<int32>(scaledHeight));

			_imageGraphic->setGfx(&rescaledGraphic, false);

			if (_numSlots > 1) {
				_imageNumberLabel->setLabel(Common::U32String::format(_("%u of %u"), static_cast<uint>(slot + 1u), _numSlots));
				_prevButton->setEnabled(slot > 0);
				_nextButton->setEnabled(slot < _numSlots - 1u);
			}

			_currentSlot = slot;
		} else {
			warning("Image album failed to retrieve slot %u", slot);
		}
	}

	if (_saveButton)
		_saveButton->setEnabled(canSaveImage);
}

void ImageAlbumDialog::saveImageInSlot(uint slot) {
	Common::U32String defaultFileName = _imageSupplier->getDefaultFileNameForSlot(slot);

	Common::FormatInfo::FormatID nativeFormat = Common::FormatInfo::kFormatUnknown;

	Common::U32String fileExt;
	Common::U32String fileDesc;

	bool hasExtension = 0;
	uint extensionPos = 0;
	for (uint i = 0; i < defaultFileName.size(); i++) {
		if (defaultFileName[i] == '.') {
			hasExtension = true;
			extensionPos = i;
		}
	}
	Common::SaveFileManager *saveFileManager = g_system->getSavefileManager();

	Common::FormatInfo::FormatSupportLevel bestFormatSupportLevel = Common::FormatInfo::kFormatSupportLevelNone;
	Common::FormatInfo::FormatID bestFormat = Common::FormatInfo::kFormatUnknown;
	bool bestFormatIsLossy = true;

	// Find the best format we can write the image as
	for (uint fmtID = Common::FormatInfo::kFirstImageFormat; fmtID <= Common::FormatInfo::kLastImageFormat; fmtID++) {
		Common::FormatInfo::FormatID candidateFormat = static_cast<Common::FormatInfo::FormatID>(fmtID);

		if (!Common::FormatInfo::getImageSaveFunction(candidateFormat))
			continue;

		Common::FormatInfo::FormatSupportLevel supportLevel = Common::FormatInfo::getFormatSupportLevel(candidateFormat);

		bool formatIsLossy = false;
		Common::FormatInfo::ImageFormatCharacteristics characteristics;
		if (Common::FormatInfo::getImageFormatCharacteristics(candidateFormat, characteristics))
			formatIsLossy = (characteristics._lossiness == Common::FormatInfo::kLossinessLossy);

		bool isBetter = false;

		// If the best format we have chosen is lossy, and this is a lossless format that is at least supported, it is better
		// If this format is the same lossiness, but is better-supported, it is better
		if (bestFormatIsLossy && !formatIsLossy && supportLevel >= Common::FormatInfo::kFormatSupportLevelSupported)
			isBetter = true;
		else if (bestFormatIsLossy == formatIsLossy && supportLevel > bestFormatSupportLevel)
			isBetter = true;
		else if (bestFormat == Common::FormatInfo::kFormatUnknown && supportLevel >= Common::FormatInfo::kFormatSupportLevelNone)
			isBetter = true;

		if (isBetter) {
			bestFormatSupportLevel = supportLevel;
			bestFormat = candidateFormat;
			bestFormatIsLossy = formatIsLossy;
		}
	}

	assert(bestFormat != Common::FormatInfo::kFormatUnknown);

	if (_imageSupplier->getFileFormatForImageSlot(slot, nativeFormat)) {
		if (nativeFormat != bestFormat) {
			Common::FormatInfo::FormatSupportLevel nativeSupportLevel = Common::FormatInfo::getFormatSupportLevel(nativeFormat);

			if (nativeSupportLevel > Common::FormatInfo::kFormatSupportLevelNone) {
				bool nativeFormatIsLossy = false;

				Common::FormatInfo::ImageFormatCharacteristics characteristics;
				if (Common::FormatInfo::getImageFormatCharacteristics(nativeFormat, characteristics))
					nativeFormatIsLossy = (characteristics._lossiness == Common::FormatInfo::kLossinessLossy);

				// If the native format is lossy and is at least supported, prefer using it directly, otherwise only use it if it has a higher support level
				if ((nativeFormatIsLossy && nativeSupportLevel >= Common::FormatInfo::kFormatSupportLevelSupported) || nativeSupportLevel >= bestFormatSupportLevel) {
					bestFormat = nativeFormat;
					bestFormatSupportLevel = nativeSupportLevel;
				}
			}
		}
	}

	// This shouldn't be possible, the Save button should not be visible unless there is either a saveable format,
	// or the file's native format is saveable, and in either of those circumstances, a format should have been selected by this point.
	assert(bestFormatSupportLevel > Common::FormatInfo::kFormatSupportLevelNone);

	bool needsConversion = false;

	if (nativeFormat == bestFormat) {
		// Save in the preferred format
		if (hasExtension)
			fileExt = defaultFileName.substr(extensionPos + 1);
	} else {
		// Save in the preferred format
		needsConversion = true;

		fileExt = Common::U32String(Common::FormatInfo::getFormatExtension(bestFormat, true));

		if (hasExtension)
			defaultFileName = defaultFileName.substr(0, extensionPos) + Common::U32String(".") + fileExt;
	}

	fileDesc = Common::FormatInfo::getFormatSaveDescription(bestFormat);

	Common::U32String title = _("Save Image");

	if (needsConversion) {
		const Graphics::Surface *surf = nullptr;
		bool hasPalette = false;
		byte palette[256 * 3];
		ImageAlbumImageMetadata metadata;

		for (byte &paletteByte : palette)
			paletteByte = 0;

		if (_imageSupplier->loadImageSlot(slot, surf, hasPalette, palette, metadata)) {
			Common::ScopedPtr<Common::SeekableWriteStream> writeStream;

			GUI::FileBrowserDialog browser(title.encode(Common::kUtf8).c_str(), fileExt.encode(Common::kUtf8).c_str(), GUI::kFBModeSave, nullptr, defaultFileName.encode(Common::kUtf8).c_str());

			if (browser.runModal() > 0) {
				Common::String path = browser.getResult();
				writeStream.reset(saveFileManager->openForSaving(path, false));
				if (writeStream) {
					assert(writeStream);

					Common::FormatInfo::ImageSaveCallback_t saveCallback = Common::FormatInfo::getImageSaveFunction(bestFormat);
					assert(saveCallback);

					Common::FormatInfo::ImageSaveProperties saveProps;
					saveCallback(*writeStream, *surf, hasPalette ? palette : nullptr, saveProps);
				} else {
					warning("Failed to open image output stream");
				}
			}

			_imageSupplier->releaseImageSlot(slot);
		}
	} else {
		Common::ScopedPtr<Common::SeekableReadStream> readStream;
		readStream.reset(_imageSupplier->createReadStreamForSlot(slot));
		if (!readStream) {
			warning("Failed to open input stream for slot %u", slot);
			return;
		}

		Common::ScopedPtr<Common::SeekableWriteStream> writeStream;

		GUI::FileBrowserDialog browser(title.encode(Common::kUtf8).c_str(), fileExt.encode(Common::kUtf8).c_str(), GUI::kFBModeSave, nullptr, defaultFileName.encode(Common::kUtf8).c_str());

		if (browser.runModal() > 0) {
			Common::String path = browser.getResult();
			writeStream.reset(saveFileManager->openForSaving(path, false));

			if (writeStream) {
				assert(writeStream);

				byte copyBuffer[2048];
				uint32 bytesRead = readStream->read(copyBuffer, sizeof(copyBuffer));
				while (bytesRead) {
					writeStream->write(copyBuffer, bytesRead);
					bytesRead = readStream->read(copyBuffer, sizeof(copyBuffer));
				}
			} else {
				warning("Failed to open image output stream");
			}
		}
	}
}

void ImageAlbumDialog::close() {
	Dialog::close();
}

void ImageAlbumDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
	switch (cmd) {
	case kPrevCmd:
		if (_currentSlot > 0)
			changeToSlot(_currentSlot - 1);
		break;
	case kNextCmd:
		if (_currentSlot < _numSlots - 1)
			changeToSlot(_currentSlot + 1);
		break;
	case kSaveCmd:
		saveImageInSlot(_currentSlot);
		break;
	default:
		Dialog::handleCommand(sender, cmd, data);
	}
}

GUI::Dialog *createImageAlbumDialog(const Common::U32String &title, ImageAlbumImageSupplier *imageSupplier, uint initialSlot) {
	return new ImageAlbumDialog(title, imageSupplier, initialSlot);
}

} // End of namespace GUI
back to top