Fix transparency export (not possible for jpeg).

This commit is contained in:
Andre Rummler
2026-09-06 14:52:56 +02:00
parent 6f80da66d5
commit ef8e41a5c3
2 changed files with 185 additions and 0 deletions
@@ -31,10 +31,14 @@
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include <QAction>
#include <QBuffer>
#include <QFileDialog>
#include <QFileInfo>
#include <QGraphicsSceneContextMenuEvent>
#include <QImageWriter>
#include <QMenu>
#include <QMessageBox>
#include <QTextStream>
/**
@brief DiagramImageItem::DiagramImageItem
@@ -1029,6 +1033,177 @@ void DiagramImageItem::restoreAspectRatio()
diagram()->undoStack().push(undo);
}
/**
@brief DiagramImageItem::saveImageAs
Saves the currently displayed pixmap (crop and colour-keyed
transparency already applied -- what the item actually looks like
on the diagram, not the pristine original) to an arbitrary file on
disk.
*/
void DiagramImageItem::saveImageAs()
{
saveImagePixmapAs(pixmap_, tr("Enregistrer l'image sous..."), !m_transparent_colors.isEmpty());
}
/**
@brief DiagramImageItem::saveOriginalImageAs
Saves m_base_pixmap -- the true, pristine original, before any crop
or colour-keyed transparency -- rather than the item's current,
possibly-cropped-and-keyed display pixmap. The only way to recover
the un-cropped, un-keyed source once either of those has actually
been applied, short of undoing every edit back to the point it was
first inserted or replaced.
*/
void DiagramImageItem::saveOriginalImageAs()
{
saveImagePixmapAs(m_base_pixmap, tr("Enregistrer l'image d'origine sous..."), false);
}
/**
@brief DiagramImageItem::saveImagePixmapAs
Shared by saveImageAs() and saveOriginalImageAs(): prompts for a
destination, resolves whichever format was actually intended, warns
before silently dropping transparency, and writes the file. A
read-only export, not an edit: doesn't touch diagram()'s undo stack,
and works even on a read-only diagram, unlike every other action in
this item's context menu.
@param pixmap the pixmap to save -- pixmap_ or m_base_pixmap
@param dialogTitle distinguishes the two callers in the save dialog's own title bar
@param hasTransparency whether `pixmap` has colour-keyed transparency worth warning about losing (never true for the pristine original, which predates any such keying)
*/
void DiagramImageItem::saveImagePixmapAs(const QPixmap &pixmap, const QString &dialogTitle, bool hasTransparency)
{
QWidget *parentWidget = (diagram() && !diagram()->views().isEmpty()) ? diagram()->views().first() : nullptr;
// Filter text -> extension. Built once and used both to construct
// the dialog's filter list and, below, to resolve whichever one the
// person actually had selected -- rather than only ever falling
// back to a single hardcoded format regardless of their choice,
// which is what this used to do (getSaveFileName() doesn't reliably
// auto-append the selected filter's extension on every platform,
// and the previous version's fallback ignored the selected filter
// entirely, defaulting to PNG even when JPEG or BMP had been
// explicitly chosen).
const QList<QPair<QString, QString>> filters = {
{tr("Image PNG (*.png)"), QStringLiteral("png")},
{tr("Image JPEG (*.jpg *.jpeg)"), QStringLiteral("jpg")},
{tr("Image BMP (*.bmp)"), QStringLiteral("bmp")},
// SVG here always means a raster image wrapped in an SVG
// container (an <image> element embedding this same pixmap as
// base64 PNG), never a true vector export -- this item only
// ever holds raster data, even when originally inserted from an
// SVG file, since that file was rasterized once at import time
// and its vector information is already gone by the time this
// runs.
{tr("Image SVG (*.svg)"), QStringLiteral("svg")},
};
QStringList filterStrings;
for (const auto &f : filters)
filterStrings << f.first;
filterStrings << tr("Tous les fichiers (*)");
QString selectedFilter;
QString path = QFileDialog::getSaveFileName(
parentWidget, dialogTitle, QString(), filterStrings.join(QLatin1String(";;")), &selectedFilter);
if (path.isEmpty())
return;
// Resolve the target format: prefer an extension already present
// and actually writable, otherwise fall back to whichever filter
// was selected in the dialog (not a fixed default), so the format
// picked in the dropdown is the one that's actually honoured even
// when the typed name carries no extension of its own.
QString suffix = QFileInfo(path).suffix().toLower();
const bool suffixIsSvg = (suffix == QLatin1String("svg"));
const bool suffixIsWritableRaster = QImageWriter::supportedImageFormats().contains(suffix.toUtf8());
if (!suffixIsSvg && !suffixIsWritableRaster)
{
QString fallbackExt = QStringLiteral("png");
for (const auto &f : filters)
if (f.first == selectedFilter) { fallbackExt = f.second; break; }
path += QLatin1Char('.') + fallbackExt;
suffix = fallbackExt;
}
// JPEG and BMP have no usable alpha channel here -- Qt's writers for
// both silently drop it. That alone wouldn't be too bad (the
// warning already tells the person transparency won't survive),
// except pixmap_ itself has already lost the true colour
// information wherever it's transparent by this point: QPixmap
// stores its data premultiplied internally, which zeroes out RGB
// at alpha=0 the moment applyColorKey()'s QImage result gets
// wrapped into a QPixmap -- confirmed directly, not assumed
// (QPixmap::fromImage() on a white, alpha=0 pixel reliably comes
// back black on toImage()). Saving pixmap_ as-is to a format with
// no alpha channel doesn't fall back to the original picture, it
// reveals that already-lost black. The fix below substitutes the
// cropped base pixmap for the save in this specific case -- it has
// the same, correct RGB values everywhere applyColorKey() left a
// pixel opaque (colour-keying only ever changes alpha, never RGB),
// so it's an exact reproduction of what the image looked like
// before any colour was ever keyed out, not an approximation.
const bool formatPreservesAlpha = (suffix == QLatin1String("png") || suffix == QLatin1String("svg"));
QPixmap toSave = pixmap;
if (hasTransparency && !formatPreservesAlpha)
{
if (QMessageBox::warning(parentWidget,
tr("Transparence non conservée"),
tr("Ce format ne prend pas en charge la transparence : l'image sera enregistrée "
"telle qu'elle était avant l'application de la couleur transparente. Continuer ?"),
QMessageBox::Yes | QMessageBox::Cancel) != QMessageBox::Yes)
return;
toSave = m_base_pixmap.copy(m_crop_rect);
}
bool ok;
if (suffix == QLatin1String("svg"))
ok = writeRasterAsSvg(toSave, path);
else
ok = toSave.save(path);
if (!ok)
{
QMessageBox::warning(parentWidget,
tr("Échec de l'enregistrement"),
tr("Impossible d'enregistrer l'image à cet emplacement."));
}
}
/**
@brief DiagramImageItem::writeRasterAsSvg
Wraps `pixmap` as a base64-embedded PNG inside a minimal, valid SVG
document -- the closest this can honestly offer to "save as SVG"
given this item only ever holds raster data (see the note in
saveImagePixmapAs()). Opens in any SVG viewer at the pixmap's own
pixel size, but is not, and cannot be, a vector re-export.
@param pixmap the raster image to embed
@param path destination file path
@return whether the file was written successfully
*/
bool DiagramImageItem::writeRasterAsSvg(const QPixmap &pixmap, const QString &path)
{
QByteArray pngData;
QBuffer buffer(&pngData);
buffer.open(QIODevice::WriteOnly);
if (!pixmap.save(&buffer, "PNG"))
return false;
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
return false;
QTextStream out(&file);
out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
<< "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" "
<< "width=\"" << pixmap.width() << "\" height=\"" << pixmap.height() << "\" "
<< "viewBox=\"0 0 " << pixmap.width() << ' ' << pixmap.height() << "\">\n"
<< " <image width=\"" << pixmap.width() << "\" height=\"" << pixmap.height() << "\" "
<< "xlink:href=\"data:image/png;base64," << QString::fromLatin1(pngData.toBase64()) << "\"/>\n"
<< "</svg>\n";
return true;
}
/**
@brief DiagramImageItem::itemChange
*/
@@ -1382,6 +1557,12 @@ void DiagramImageItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
QAction *replace = menu.data()->addAction(tr("Remplacer l'image..."));
connect(replace, &QAction::triggered, this, &DiagramImageItem::replaceImage);
QAction *saveAs = menu.data()->addAction(tr("Enregistrer l'image sous..."));
connect(saveAs, &QAction::triggered, this, &DiagramImageItem::saveImageAs);
QAction *saveOriginalAs = menu.data()->addAction(tr("Enregistrer l'image d'origine sous..."));
connect(saveOriginalAs, &QAction::triggered, this, &DiagramImageItem::saveOriginalImageAs);
QAction *transparentColor = menu.data()->addAction(tr("Couleur transparente..."));
connect(transparentColor, &QAction::triggered, this, &DiagramImageItem::setTransparentColor);
@@ -143,6 +143,10 @@ class DiagramImageItem : public QetGraphicsItem {
void setTransparentColor();
void crop();
void restoreAspectRatio();
void saveImageAs();
void saveOriginalImageAs();
void saveImagePixmapAs(const QPixmap &pixmap, const QString &dialogTitle, bool hasTransparency);
static bool writeRasterAsSvg(const QPixmap &pixmap, const QString &path);
static QPixmap computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<ImageTransparentColorDialog::PickedColor> &colors);
void toggleHandleMode();