Merge pull request #766 from GriffinRichards/color-fix

Fix image export colors, add color space settings
This commit is contained in:
GriffinR 2025-08-27 22:44:34 -04:00 committed by GitHub
commit fb45714dda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 208 additions and 112 deletions

View File

@ -10,6 +10,7 @@ and this project somewhat adheres to [Semantic Versioning](https://semver.org/sp
### Fixed ### Fixed
- Fix rare crash while quitting Porymap. - Fix rare crash while quitting Porymap.
- Fix exported images on macOS using a different color space than in Porymap.
## [6.2.0] - 2025-08-08 ## [6.2.0] - 2025-08-08
### Added ### Added

View File

@ -48,52 +48,74 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>493</width> <width>493</width>
<height>374</height> <height>408</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_6"> <layout class="QVBoxLayout" name="verticalLayout_6">
<item> <item>
<widget class="QCheckBox" name="checkBox_MonitorProjectFiles"> <layout class="QFormLayout" name="formLayout">
<property name="toolTip"> <item row="2" column="0" colspan="2">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, a prompt to reload your project will appear if relevant project files are edited&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <widget class="QCheckBox" name="checkBox_CheckForUpdates">
</property> <property name="toolTip">
<property name="text"> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, Porymap will automatically alert you on startup if a new release is available&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>Monitor project files</string> </property>
</property> <property name="text">
</widget> <string>Automatically check for updates</string>
</item> </property>
<item> </widget>
<widget class="QCheckBox" name="checkBox_OpenRecentProject"> </item>
<property name="toolTip"> <item row="1" column="0" colspan="2">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, Porymap will automatically open your most recently opened project on startup&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <widget class="QCheckBox" name="checkBox_OpenRecentProject">
</property> <property name="toolTip">
<property name="text"> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, Porymap will automatically open your most recently opened project on startup&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>Open recent project on launch</string> </property>
</property> <property name="text">
</widget> <string>Open recent project on launch</string>
</item> </property>
<item> </widget>
<widget class="QCheckBox" name="checkBox_CheckForUpdates"> </item>
<property name="toolTip"> <item row="0" column="0" colspan="2">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, Porymap will automatically alert you on startup if a new release is available&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <widget class="QCheckBox" name="checkBox_MonitorProjectFiles">
</property> <property name="toolTip">
<property name="text"> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, a prompt to reload your project will appear if relevant project files are edited&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>Automatically check for updates</string> </property>
</property> <property name="text">
</widget> <string>Monitor project files</string>
</item> </property>
<item> </widget>
<widget class="QGroupBox" name="groupBox_Themes"> </item>
<property name="sizePolicy"> <item row="3" column="0">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <widget class="QLabel" name="label_ApplicationTheme">
<horstretch>0</horstretch> <property name="text">
<verstretch>0</verstretch> <string>Application Theme</string>
</sizepolicy> </property>
</property> </widget>
<property name="title"> </item>
<string>Application Theme</string> <item row="3" column="1">
</property> <widget class="NoScrollComboBox" name="comboBox_ApplicationTheme">
</widget> <property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_ColorSpace">
<property name="text">
<string>Image Export Color Space</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="NoScrollComboBox" name="comboBox_ColorSpace">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The color space to use for exported images. If &amp;quot;---&amp;quot; is set, no color space will be used for the exported image. For details on each color space, see Qt's manual page for QColorSpace.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="groupBox_Fonts"> <widget class="QGroupBox" name="groupBox_Fonts">
@ -483,6 +505,13 @@
</layout> </layout>
</widget> </widget>
</widget> </widget>
<customwidgets>
<customwidget>
<class>NoScrollComboBox</class>
<extends>QComboBox</extends>
<header>noscrollcombobox.h</header>
</customwidget>
</customwidgets>
<resources/> <resources/>
<connections/> <connections/>
</ui> </ui>

View File

@ -14,6 +14,7 @@
#include <QGraphicsPixmapItem> #include <QGraphicsPixmapItem>
#include <QFontDatabase> #include <QFontDatabase>
#include <QStandardPaths> #include <QStandardPaths>
#include <QColorSpace>
#include <set> #include <set>
#include "events.h" #include "events.h"
@ -145,6 +146,7 @@ public:
std::set<LogType> statusBarLogTypes; std::set<LogType> statusBarLogTypes;
QFont applicationFont; QFont applicationFont;
QFont mapListFont; QFont mapListFont;
int imageExportColorSpaceId;
protected: protected:
virtual void parseConfigKeyValue(QString key, QString value) override; virtual void parseConfigKeyValue(QString key, QString value) override;

View File

@ -42,6 +42,7 @@ public:
MapConnection* createMirror(); MapConnection* createMirror();
QPixmap render() const; QPixmap render() const;
QImage renderImage() const;
QPoint relativePixelPos(bool clipped = false) const; QPoint relativePixelPos(bool clipped = false) const;
static QPointer<Project> project; static QPointer<Project> project;

View File

@ -4,6 +4,7 @@
#include <QString> #include <QString>
#include <QLineEdit> #include <QLineEdit>
#include <QColorSpace>
namespace Util { namespace Util {
void numericalModeSort(QStringList &list); void numericalModeSort(QStringList &list);
@ -17,6 +18,7 @@ namespace Util {
void setErrorStylesheet(QLineEdit *lineEdit, bool isError); void setErrorStylesheet(QLineEdit *lineEdit, bool isError);
QString toStylesheetString(const QFont &font); QString toStylesheetString(const QFont &font);
void show(QWidget *w); void show(QWidget *w);
QColorSpace toColorSpace(int colorSpaceInt);
} }
#endif // UTILITY_H #endif // UTILITY_H

View File

@ -58,6 +58,7 @@ private:
QBuffer *m_timelapseBuffer = nullptr; QBuffer *m_timelapseBuffer = nullptr;
QMovie *m_timelapseMovie = nullptr; QMovie *m_timelapseMovie = nullptr;
QGraphicsPixmapItem *m_preview = nullptr; QGraphicsPixmapItem *m_preview = nullptr;
QImage m_previewImage;
ImageExporterSettings m_settings; ImageExporterSettings m_settings;
ImageExporterMode m_mode = ImageExporterMode::Normal; ImageExporterMode m_mode = ImageExporterMode::Normal;
@ -77,15 +78,15 @@ private:
void setConnectionDirectionEnabled(const QString &dir, bool enable); void setConnectionDirectionEnabled(const QString &dir, bool enable);
void saveImage(); void saveImage();
QGifImage* createTimelapseGifImage(QProgressDialog *progress); QGifImage* createTimelapseGifImage(QProgressDialog *progress);
QPixmap getStitchedImage(QProgressDialog *progress); QImage getStitchedImage(QProgressDialog *progress);
QPixmap getFormattedMapPixmap(); QImage getFormattedMapImage();
void paintBorder(QPainter *painter, Layout *layout); void paintBorder(QPainter *painter, Layout *layout);
void paintCollision(QPainter *painter, Layout *layout); void paintCollision(QPainter *painter, Layout *layout);
void paintConnections(QPainter *painter, const Map *map); void paintConnections(QPainter *painter, const Map *map);
void paintEvents(QPainter *painter, const Map *map); void paintEvents(QPainter *painter, const Map *map);
void paintGrid(QPainter *painter, const Layout *layout = nullptr); void paintGrid(QPainter *painter, const Layout *layout = nullptr);
QMargins getMargins(const Map *map); QMargins getMargins(const Map *map);
QPixmap getExpandedPixmap(const QPixmap &pixmap, const QSize &targetSize, const QColor &fillColor); QImage getExpandedImage(const QImage &image, const QSize &targetSize, const QColor &fillColor);
bool currentHistoryAppliesToFrame(QUndoStack *historyStack); bool currentHistoryAppliesToFrame(QUndoStack *historyStack);
protected: protected:

View File

@ -81,6 +81,7 @@ private:
CheckeredBgScene *m_scene = nullptr; CheckeredBgScene *m_scene = nullptr;
QGraphicsPixmapItem *m_preview = nullptr; QGraphicsPixmapItem *m_preview = nullptr;
QImage m_previewImage;
bool m_previewUpdateQueued = false; bool m_previewUpdateQueued = false;
QList<int> m_layerOrder; QList<int> m_layerOrder;
ProjectConfig m_savedConfig; ProjectConfig m_savedConfig;

View File

@ -29,7 +29,6 @@ signals:
private: private:
Ui::PreferenceEditor *ui; Ui::PreferenceEditor *ui;
NoScrollComboBox *themeSelector;
QFont applicationFont; QFont applicationFont;
QFont mapListFont; QFont mapListFont;

View File

@ -373,6 +373,16 @@ void PorymapConfig::reset() {
this->statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN }; this->statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN };
this->applicationFont = QFont(); this->applicationFont = QFont();
this->mapListFont = PorymapConfig::defaultMapListFont(); this->mapListFont = PorymapConfig::defaultMapListFont();
#ifdef Q_OS_MACOS
// Since the release of the Retina display, Apple products use the Display P3 color space by default.
// If we don't use this for exported images (which by default will either have no color space or the sRGB
// color space) then they may appear to have different colors than the same image displayed in Porymap.
this->imageExportColorSpaceId = static_cast<int>(QColorSpace::DisplayP3);
#else
// As of writing Qt has no way to get a reasonable color space from the user's environment,
// so we export images without one and let them handle it.
this->imageExportColorSpaceId = 0;
#endif
} }
void PorymapConfig::parseConfigKeyValue(QString key, QString value) { void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
@ -563,6 +573,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
} else if (key == "map_list_font") { } else if (key == "map_list_font") {
this->mapListFont = QFont(); this->mapListFont = QFont();
this->mapListFont.fromString(value); this->mapListFont.fromString(value);
} else if (key == "image_export_color_space_id") {
this->imageExportColorSpaceId = getConfigInteger(key, value, 0, 8);
} else { } else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key)); logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key));
} }
@ -656,6 +668,7 @@ QMap<QString, QString> PorymapConfig::getKeyValueMap() {
map.insert("status_bar_log_types", logTypesStrings.join(",")); map.insert("status_bar_log_types", logTypesStrings.join(","));
map.insert("application_font", this->applicationFont.toString()); map.insert("application_font", this->applicationFont.toString());
map.insert("map_list_font", this->mapListFont.toString()); map.insert("map_list_font", this->mapListFont.toString());
map.insert("image_export_color_space_id", QString::number(this->imageExportColorSpaceId));
return map; return map;
} }

View File

@ -68,6 +68,12 @@ QPixmap MapConnection::render() const {
return map->renderConnection(m_direction, m_parentMap ? m_parentMap->layout() : nullptr); return map->renderConnection(m_direction, m_parentMap ? m_parentMap->layout() : nullptr);
} }
QImage MapConnection::renderImage() const {
render();
auto map = targetMap();
return (map && map->layout()) ? map->layout()->image : QImage();
}
// Get the position of the target map relative to its parent map. // Get the position of the target map relative to its parent map.
// For right/down connections this is offset by the dimensions of the parent map. // For right/down connections this is offset by the dimensions of the parent map.
// For left/up connections this is offset by the dimensions of the target map. // For left/up connections this is offset by the dimensions of the target map.

View File

@ -125,3 +125,23 @@ void Util::show(QWidget *w) {
w->activateWindow(); w->activateWindow();
} }
} }
// Safe conversion from an int representing a QColorSpace::NamedColorSpace to a QColorSpace.
// This lets us use 0 to mean "no color space".
QColorSpace Util::toColorSpace(int colorSpaceInt) {
QColorSpace colorSpace;
int min = static_cast<int>(QColorSpace::SRgb);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0))
// Qt 6.8.0 introduced additional color spaces
int max = static_cast<int>(QColorSpace::Bt2100Hlg);
#else
int max = static_cast<int>(QColorSpace::ProPhotoRgb);
#endif
if (colorSpaceInt >= min && colorSpaceInt <= max) {
return QColorSpace(static_cast<QColorSpace::NamedColorSpace>(colorSpaceInt));
} else {
return QColorSpace();
}
}

View File

@ -185,9 +185,9 @@ void MapImageExporter::updateMapSelection() {
void MapImageExporter::saveImage() { void MapImageExporter::saveImage() {
// If the preview is empty (because progress was canceled) or if updates were disabled // If the preview is empty (because progress was canceled) or if updates were disabled
// then we should ensure the image in the preview is up-to-date before exporting. // then we should ensure the image in the preview is up-to-date before exporting.
if (m_preview->pixmap().isNull() || m_settings.disablePreviewUpdates) { if (m_previewImage.isNull() || m_settings.disablePreviewUpdates) {
updatePreview(true); updatePreview(true);
if (m_preview->pixmap().isNull()) if (m_previewImage.isNull())
return; // Canceled return; // Canceled
} }
if (m_mode == ImageExporterMode::Timelapse && !m_timelapseGifImage) { if (m_mode == ImageExporterMode::Timelapse && !m_timelapseGifImage) {
@ -221,7 +221,7 @@ void MapImageExporter::saveImage() {
case ImageExporterMode::Normal: case ImageExporterMode::Normal:
case ImageExporterMode::Stitch: case ImageExporterMode::Stitch:
// Normal and Stitch modes already have the image ready to go in the preview. // Normal and Stitch modes already have the image ready to go in the preview.
m_preview->pixmap().save(filepath); m_previewImage.save(filepath);
break; break;
case ImageExporterMode::Timelapse: case ImageExporterMode::Timelapse:
m_timelapseGifImage->save(filepath); m_timelapseGifImage->save(filepath);
@ -285,19 +285,19 @@ bool MapImageExporter::currentHistoryAppliesToFrame(QUndoStack *historyStack) {
} }
} }
QPixmap MapImageExporter::getExpandedPixmap(const QPixmap &pixmap, const QSize &targetSize, const QColor &fillColor) { QImage MapImageExporter::getExpandedImage(const QImage &image, const QSize &targetSize, const QColor &fillColor) {
if (pixmap.width() >= targetSize.width() && pixmap.height() >= targetSize.height()) if (image.width() >= targetSize.width() && image.height() >= targetSize.height())
return pixmap; return image;
QPixmap resizedPixmap = QPixmap(targetSize); QImage resizedImage(targetSize, QImage::Format_RGBA8888);
QPainter painter(&resizedPixmap); QPainter painter(&resizedImage);
resizedPixmap.fill(fillColor); resizedImage.fill(fillColor);
// Center the old pixmap in the new resized one. // Center the old image in the new resized one.
int x = (targetSize.width() - pixmap.width()) / 2; int x = (targetSize.width() - image.width()) / 2;
int y = (targetSize.height() - pixmap.height()) / 2; int y = (targetSize.height() - image.height()) / 2;
painter.drawPixmap(x, y, pixmap.width(), pixmap.height(), pixmap); painter.drawImage(x, y, image);
return resizedPixmap; return resizedImage;
} }
struct TimelapseStep { struct TimelapseStep {
@ -363,8 +363,7 @@ QGifImage* MapImageExporter::createTimelapseGifImage(QProgressDialog *progress)
while (step.historyStack->canRedo() && step.historyStack->index() < step.initialStackIndex && !progress->wasCanceled()) { while (step.historyStack->canRedo() && step.historyStack->index() < step.initialStackIndex && !progress->wasCanceled()) {
if (currentHistoryAppliesToFrame(step.historyStack) && --framesToSkip <= 0) { if (currentHistoryAppliesToFrame(step.historyStack) && --framesToSkip <= 0) {
// Render frame, increasing its size if necessary to match the canvas. // Render frame, increasing its size if necessary to match the canvas.
QPixmap pixmap = getExpandedPixmap(getFormattedMapPixmap(), canvasSize, m_settings.fillColor); timelapseImg->addFrame(getExpandedImage(getFormattedMapImage(), canvasSize, m_settings.fillColor));
timelapseImg->addFrame(pixmap.toImage());
framesToSkip = m_settings.timelapseSkipAmount - 1; framesToSkip = m_settings.timelapseSkipAmount - 1;
} }
step.historyStack->redo(); step.historyStack->redo();
@ -396,8 +395,7 @@ QGifImage* MapImageExporter::createTimelapseGifImage(QProgressDialog *progress)
// Final frame should always be the current state of the map. // Final frame should always be the current state of the map.
if (timelapseImg) { if (timelapseImg) {
QPixmap finalFrame = getExpandedPixmap(getFormattedMapPixmap(), canvasSize, m_settings.fillColor); timelapseImg->addFrame(getExpandedImage(getFormattedMapImage(), canvasSize, m_settings.fillColor));
timelapseImg->addFrame(finalFrame.toImage());
} }
return timelapseImg; return timelapseImg;
} }
@ -408,7 +406,7 @@ struct StitchedMap {
Map* map; Map* map;
}; };
QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { QImage MapImageExporter::getStitchedImage(QProgressDialog *progress) {
// Do a breadth-first search to gather a collection of // Do a breadth-first search to gather a collection of
// all reachable maps with their relative offsets. // all reachable maps with their relative offsets.
QSet<QString> visited; QSet<QString> visited;
@ -419,7 +417,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) {
progress->setLabelText("Gathering stitched maps..."); progress->setLabelText("Gathering stitched maps...");
while (!unvisited.isEmpty()) { while (!unvisited.isEmpty()) {
if (progress->wasCanceled()) { if (progress->wasCanceled()) {
return QPixmap(); return QImage();
} }
progress->setMaximum(visited.size() + unvisited.size()); progress->setMaximum(visited.size() + unvisited.size());
progress->setValue(visited.size()); progress->setValue(visited.size());
@ -439,7 +437,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) {
} }
} }
if (stitchedMaps.isEmpty()) if (stitchedMaps.isEmpty())
return QPixmap(); return QImage();
progress->setMaximum(stitchedMaps.size()); progress->setMaximum(stitchedMaps.size());
int numDrawn = 0; int numDrawn = 0;
@ -450,10 +448,10 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) {
dimensions |= (QRect(map.x, map.y, map.map->pixelWidth(), map.map->pixelHeight()) + getMargins(map.map)); dimensions |= (QRect(map.x, map.y, map.map->pixelWidth(), map.map->pixelHeight()) + getMargins(map.map));
} }
QPixmap stitchedPixmap(dimensions.width(), dimensions.height()); QImage stitchedImage(dimensions.width(), dimensions.height(), QImage::Format_RGBA8888);
stitchedPixmap.fill(m_settings.fillColor); stitchedImage.fill(m_settings.fillColor);
QPainter painter(&stitchedPixmap); QPainter painter(&stitchedImage);
painter.translate(-dimensions.left(), -dimensions.top()); painter.translate(-dimensions.left(), -dimensions.top());
// Borders can occlude neighboring maps, so we draw all the borders before drawing any maps. // Borders can occlude neighboring maps, so we draw all the borders before drawing any maps.
@ -469,7 +467,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) {
numDrawn = 0; numDrawn = 0;
for (const StitchedMap &map : stitchedMaps) { for (const StitchedMap &map : stitchedMaps) {
if (progress->wasCanceled()) { if (progress->wasCanceled()) {
return QPixmap(); return QImage();
} }
painter.translate(map.x, map.y); painter.translate(map.x, map.y);
paintBorder(&painter, map.map->layout()); paintBorder(&painter, map.map->layout());
@ -485,10 +483,11 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) {
numDrawn = 0; numDrawn = 0;
for (const StitchedMap &map : stitchedMaps) { for (const StitchedMap &map : stitchedMaps) {
if (progress->wasCanceled()) { if (progress->wasCanceled()) {
return QPixmap(); return QImage();
} }
map.map->layout()->render(true);
painter.translate(map.x, map.y); painter.translate(map.x, map.y);
painter.drawPixmap(0, 0, map.map->layout()->render(true)); painter.drawImage(0, 0, map.map->layout()->image);
paintCollision(&painter, map.map->layout()); paintCollision(&painter, map.map->layout());
painter.translate(-map.x, -map.y); painter.translate(-map.x, -map.y);
@ -504,7 +503,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) {
numDrawn = 0; numDrawn = 0;
for (const StitchedMap &map : stitchedMaps) { for (const StitchedMap &map : stitchedMaps) {
if (progress->wasCanceled()) { if (progress->wasCanceled()) {
return QPixmap(); return QImage();
} }
painter.translate(map.x, map.y); painter.translate(map.x, map.y);
paintEvents(&painter, map.map); paintEvents(&painter, map.map);
@ -515,7 +514,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) {
} }
} }
return stitchedPixmap; return stitchedImage;
} }
void MapImageExporter::updatePreview(bool forceUpdate) { void MapImageExporter::updatePreview(bool forceUpdate) {
@ -528,18 +527,17 @@ void MapImageExporter::updatePreview(bool forceUpdate) {
progress.setModal(true); progress.setModal(true);
progress.setMinimumDuration(1000); progress.setMinimumDuration(1000);
QPixmap previewPixmap;
if (m_mode == ImageExporterMode::Normal) { if (m_mode == ImageExporterMode::Normal) {
previewPixmap = getFormattedMapPixmap(); m_previewImage = getFormattedMapImage();
} else if (m_mode == ImageExporterMode::Stitch) { } else if (m_mode == ImageExporterMode::Stitch) {
previewPixmap = getStitchedImage(&progress); m_previewImage = getStitchedImage(&progress);
} else if (m_mode == ImageExporterMode::Timelapse) { } else if (m_mode == ImageExporterMode::Timelapse) {
if (m_timelapseMovie) if (m_timelapseMovie)
m_timelapseMovie->stop(); m_timelapseMovie->stop();
m_timelapseGifImage = createTimelapseGifImage(&progress); m_timelapseGifImage = createTimelapseGifImage(&progress);
if (!m_timelapseGifImage) { if (!m_timelapseGifImage) {
previewPixmap = QPixmap(); m_previewImage = QImage();
} else { } else {
// We want to convert the QGifImage data into a QMovie for the preview display. // We want to convert the QGifImage data into a QMovie for the preview display.
// Both support input/output with a QIODevice, so we use a QBuffer to translate the data. // Both support input/output with a QIODevice, so we use a QBuffer to translate the data.
@ -556,12 +554,15 @@ void MapImageExporter::updatePreview(bool forceUpdate) {
m_preview->setPixmap(m_timelapseMovie->currentPixmap()); m_preview->setPixmap(m_timelapseMovie->currentPixmap());
}); });
m_timelapseMovie->start(); m_timelapseMovie->start();
previewPixmap = m_timelapseMovie->currentPixmap(); m_previewImage = m_timelapseMovie->currentImage();
} }
} else {
m_previewImage = QImage();
} }
progress.close(); progress.close();
m_preview->setPixmap(previewPixmap); m_previewImage.setColorSpace(Util::toColorSpace(porymapConfig.imageExportColorSpaceId));
m_preview->setPixmap(QPixmap::fromImage(m_previewImage));
m_scene->setSceneRect(m_scene->itemsBoundingRect()); m_scene->setSceneRect(m_scene->itemsBoundingRect());
scalePreview(); scalePreview();
} }
@ -572,23 +573,24 @@ void MapImageExporter::scalePreview() {
ui->graphicsView_Preview->fitInView(m_preview, Qt::KeepAspectRatioByExpanding); ui->graphicsView_Preview->fitInView(m_preview, Qt::KeepAspectRatioByExpanding);
} }
QPixmap MapImageExporter::getFormattedMapPixmap() { QImage MapImageExporter::getFormattedMapImage() {
if (!m_layout) if (!m_layout)
return QPixmap(); return QImage();
m_layout->render(true); m_layout->render(true);
// Create pixmap large enough to contain the map and the marginal elements (the border, grid, etc.) // Create image large enough to contain the map and the marginal elements (the border, grid, etc.)
QMargins margins = getMargins(m_map); QMargins margins = getMargins(m_map);
QPixmap pixmap = QPixmap(m_layout->pixmap.width() + margins.left() + margins.right(), QImage image(m_layout->image.width() + margins.left() + margins.right(),
m_layout->pixmap.height() + margins.top() + margins.bottom()); m_layout->image.height() + margins.top() + margins.bottom(),
pixmap.fill(m_settings.fillColor); QImage::Format_RGBA8888);
image.fill(m_settings.fillColor);
QPainter painter(&pixmap); QPainter painter(&image);
painter.translate(margins.left(), margins.top()); painter.translate(margins.left(), margins.top());
paintBorder(&painter, m_layout); paintBorder(&painter, m_layout);
painter.drawPixmap(0, 0, m_layout->pixmap); painter.drawImage(0, 0, m_layout->image);
paintCollision(&painter, m_layout); paintCollision(&painter, m_layout);
if (m_map) { if (m_map) {
paintConnections(&painter, m_map); paintConnections(&painter, m_map);
@ -596,7 +598,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap() {
} }
paintGrid(&painter, m_layout); paintGrid(&painter, m_layout);
return pixmap; return image;
} }
QMargins MapImageExporter::getMargins(const Map *map) { QMargins MapImageExporter::getMargins(const Map *map) {
@ -630,9 +632,11 @@ void MapImageExporter::paintCollision(QPainter *painter, Layout *layout) {
if (!m_settings.showCollision) if (!m_settings.showCollision)
return; return;
layout->renderCollision(true);
auto savedOpacity = painter->opacity(); auto savedOpacity = painter->opacity();
painter->setOpacity(static_cast<qreal>(porymapConfig.collisionOpacity) / 100); painter->setOpacity(static_cast<qreal>(porymapConfig.collisionOpacity) / 100);
painter->drawPixmap(0, 0, layout->renderCollision(true)); painter->drawImage(0, 0, layout->collision_image);
painter->setOpacity(savedOpacity); painter->setOpacity(savedOpacity);
} }
@ -652,7 +656,7 @@ void MapImageExporter::paintBorder(QPainter *painter, Layout *layout) {
// Skip border painting if it would be fully covered by the rest of the map // Skip border painting if it would be fully covered by the rest of the map
if (layout->isWithinBounds(QRect(x, y, layout->getBorderWidth(), layout->getBorderHeight()))) if (layout->isWithinBounds(QRect(x, y, layout->getBorderWidth(), layout->getBorderHeight())))
continue; continue;
painter->drawPixmap(x * Metatile::pixelWidth(), y * Metatile::pixelHeight(), layout->border_pixmap); painter->drawImage(x * Metatile::pixelWidth(), y * Metatile::pixelHeight(), layout->border_image);
} }
painter->restore(); painter->restore();
@ -665,7 +669,7 @@ void MapImageExporter::paintConnections(QPainter *painter, const Map *map) {
for (const auto &connection : map->getConnections()) { for (const auto &connection : map->getConnections()) {
if (!m_settings.showConnections.contains(connection->direction())) if (!m_settings.showConnections.contains(connection->direction()))
continue; continue;
painter->drawImage(connection->relativePixelPos(true), connection->render().toImage()); painter->drawImage(connection->relativePixelPos(true), connection->renderImage());
} }
} }

View File

@ -185,7 +185,7 @@ void MetatileImageExporter::reset() {
QImage MetatileImageExporter::getImage() { QImage MetatileImageExporter::getImage() {
tryUpdatePreview(); tryUpdatePreview();
return m_preview->pixmap().toImage(); return m_previewImage;
} }
bool MetatileImageExporter::saveImage(QString filepath) { bool MetatileImageExporter::saveImage(QString filepath) {
@ -197,7 +197,7 @@ bool MetatileImageExporter::saveImage(QString filepath) {
return false; return false;
} }
} }
return m_preview->pixmap().save(filepath); return m_previewImage.save(filepath);
} }
QString MetatileImageExporter::getDefaultFileName() const { QString MetatileImageExporter::getDefaultFileName() const {
@ -244,7 +244,7 @@ void MetatileImageExporter::queuePreviewUpdate() {
// For updating only when a change has been recorded. // For updating only when a change has been recorded.
// Useful for something that might happen often, like an input widget losing focus. // Useful for something that might happen often, like an input widget losing focus.
void MetatileImageExporter::tryUpdatePreview() { void MetatileImageExporter::tryUpdatePreview() {
if (m_preview->pixmap().isNull() || m_previewUpdateQueued) { if (m_previewImage.isNull() || m_previewUpdateQueued) {
updatePreview(); updatePreview();
} }
} }
@ -261,15 +261,14 @@ void MetatileImageExporter::updatePreview() {
} }
} }
QImage previewImage;
if (ui->checkBox_PrimaryTileset->isChecked() && ui->checkBox_SecondaryTileset->isChecked()) { if (ui->checkBox_PrimaryTileset->isChecked() && ui->checkBox_SecondaryTileset->isChecked()) {
// Special behavior to combine the two tilesets while skipping the unused region between tilesets. // Special behavior to combine the two tilesets while skipping the unused region between tilesets.
previewImage = getMetatileSheetImage(m_primaryTileset, m_previewImage = getMetatileSheetImage(m_primaryTileset,
m_secondaryTileset, m_secondaryTileset,
ui->spinBox_WidthMetatiles->value(), ui->spinBox_WidthMetatiles->value(),
m_layerOrder); m_layerOrder);
} else { } else {
previewImage = getMetatileSheetImage(m_primaryTileset, m_previewImage = getMetatileSheetImage(m_primaryTileset,
m_secondaryTileset, m_secondaryTileset,
ui->spinBox_MetatileStart->value(), ui->spinBox_MetatileStart->value(),
ui->spinBox_MetatileEnd->value(), ui->spinBox_MetatileEnd->value(),
@ -277,7 +276,8 @@ void MetatileImageExporter::updatePreview() {
m_layerOrder); m_layerOrder);
} }
m_preview->setPixmap(QPixmap::fromImage(previewImage)); m_previewImage.setColorSpace(Util::toColorSpace(porymapConfig.imageExportColorSpaceId));
m_preview->setPixmap(QPixmap::fromImage(m_previewImage));
m_scene->setSceneRect(m_scene->itemsBoundingRect()); m_scene->setSceneRect(m_scene->itemsBoundingRect());
m_previewUpdateQueued = false; m_previewUpdateQueued = false;

View File

@ -6,23 +6,20 @@
#include <QAbstractButton> #include <QAbstractButton>
#include <QRegularExpression> #include <QRegularExpression>
#include <QDirIterator> #include <QDirIterator>
#include <QFormLayout>
#include <QFontDialog> #include <QFontDialog>
#include <QToolTip> #include <QToolTip>
PreferenceEditor::PreferenceEditor(QWidget *parent) : PreferenceEditor::PreferenceEditor(QWidget *parent) :
QMainWindow(parent), QMainWindow(parent),
ui(new Ui::PreferenceEditor), ui(new Ui::PreferenceEditor)
themeSelector(nullptr)
{ {
ui->setupUi(this); ui->setupUi(this);
auto *formLayout = new QFormLayout(ui->groupBox_Themes);
themeSelector = new NoScrollComboBox(ui->groupBox_Themes);
themeSelector->setEditable(false);
themeSelector->setMinimumContentsLength(0);
formLayout->addRow("Themes", themeSelector);
setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_DeleteOnClose);
ui->comboBox_ColorSpace->setMinimumContentsLength(0);
ui->comboBox_ApplicationTheme->setMinimumContentsLength(0);
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &PreferenceEditor::dialogButtonClicked); connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &PreferenceEditor::dialogButtonClicked);
connect(ui->pushButton_CustomizeApplicationFont, &QPushButton::clicked, [this] { connect(ui->pushButton_CustomizeApplicationFont, &QPushButton::clicked, [this] {
@ -59,16 +56,35 @@ void PreferenceEditor::initFields() {
themes.append(themeName); themes.append(themeName);
} }
} }
themeSelector->addItems(themes); ui->comboBox_ApplicationTheme->addItems(themes);
static const QMap<QString, int> colorSpaces = {
{"---", 0},
{"sRGB", QColorSpace::SRgb},
{"sRGB Linear", QColorSpace::SRgbLinear},
{"Adobe RGB", QColorSpace::AdobeRgb},
{"Display P3", QColorSpace::DisplayP3},
{"ProPhoto RGB", QColorSpace::ProPhotoRgb},
#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0))
// Qt 6.8.0 introduced additional color spaces
{"BT.2020", QColorSpace::Bt2020},
{"BT.2100 (PQ)", QColorSpace::Bt2100Pq},
{"BT.2100 (HLG)", QColorSpace::Bt2100Hlg},
#endif
};
for (auto it = colorSpaces.constBegin(); it != colorSpaces.constEnd(); it++) {
ui->comboBox_ColorSpace->addItem(it.key(), it.value());
}
} }
void PreferenceEditor::updateFields() { void PreferenceEditor::updateFields() {
themeSelector->setTextItem(porymapConfig.theme); ui->comboBox_ApplicationTheme->setTextItem(porymapConfig.theme);
if (porymapConfig.eventSelectionShapeMode == QGraphicsPixmapItem::MaskShape) { if (porymapConfig.eventSelectionShapeMode == QGraphicsPixmapItem::MaskShape) {
ui->radioButton_OnSprite->setChecked(true); ui->radioButton_OnSprite->setChecked(true);
} else if (porymapConfig.eventSelectionShapeMode == QGraphicsPixmapItem::BoundingRectShape) { } else if (porymapConfig.eventSelectionShapeMode == QGraphicsPixmapItem::BoundingRectShape) {
ui->radioButton_WithinRect->setChecked(true); ui->radioButton_WithinRect->setChecked(true);
} }
ui->comboBox_ColorSpace->setNumberItem(porymapConfig.imageExportColorSpaceId);
ui->lineEdit_TextEditorOpenFolder->setText(porymapConfig.textEditorOpenFolder); ui->lineEdit_TextEditorOpenFolder->setText(porymapConfig.textEditorOpenFolder);
ui->lineEdit_TextEditorGotoLine->setText(porymapConfig.textEditorGotoLine); ui->lineEdit_TextEditorGotoLine->setText(porymapConfig.textEditorGotoLine);
ui->checkBox_MonitorProjectFiles->setChecked(porymapConfig.monitorFiles); ui->checkBox_MonitorProjectFiles->setChecked(porymapConfig.monitorFiles);
@ -97,8 +113,8 @@ void PreferenceEditor::saveFields() {
bool needsProjectReload = false; bool needsProjectReload = false;
bool changedTheme = false; bool changedTheme = false;
if (themeSelector->currentText() != porymapConfig.theme) { if (ui->comboBox_ApplicationTheme->currentText() != porymapConfig.theme) {
porymapConfig.theme = themeSelector->currentText(); porymapConfig.theme = ui->comboBox_ApplicationTheme->currentText();
changedTheme = true; changedTheme = true;
} }
@ -113,6 +129,7 @@ void PreferenceEditor::saveFields() {
emit scriptSettingsChanged(scriptAutocompleteMode); emit scriptSettingsChanged(scriptAutocompleteMode);
} }
porymapConfig.imageExportColorSpaceId = ui->comboBox_ColorSpace->currentData().toInt();
porymapConfig.eventSelectionShapeMode = ui->radioButton_OnSprite->isChecked() ? QGraphicsPixmapItem::MaskShape : QGraphicsPixmapItem::BoundingRectShape; porymapConfig.eventSelectionShapeMode = ui->radioButton_OnSprite->isChecked() ? QGraphicsPixmapItem::MaskShape : QGraphicsPixmapItem::BoundingRectShape;
porymapConfig.textEditorOpenFolder = ui->lineEdit_TextEditorOpenFolder->text(); porymapConfig.textEditorOpenFolder = ui->lineEdit_TextEditorOpenFolder->text();
porymapConfig.textEditorGotoLine = ui->lineEdit_TextEditorGotoLine->text(); porymapConfig.textEditorGotoLine = ui->lineEdit_TextEditorGotoLine->text();