Compare commits

...

5 Commits

Author SHA1 Message Date
Laurent Trinques 76ff5c4dd9 Merge pull request #810 from arummler/feature-graphics-primitives-squashed-additional-commits
Image speed improvement and additional transparency feature
2026-09-05 21:36:33 +02:00
Andre Rummler b395527915 Add transparency to each color for image transparency. 2026-09-05 20:54:19 +02:00
Andre Rummler 7741bb33b2 Use downscaled image for image transparency window to improve resposiveness. 2026-09-05 20:54:09 +02:00
Laurent Trinques f02b576cb8 Update FR translations files 2026-09-05 20:47:59 +02:00
Laurent Trinques e0ef24fe68 Update translations files 2026-09-05 20:43:54 +02:00
36 changed files with 38112 additions and 17663 deletions
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1209 -553
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1224 -567
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1222 -565
View File
File diff suppressed because it is too large Load Diff
+1220 -563
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1226 -569
View File
File diff suppressed because it is too large Load Diff
+1222 -565
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1224 -567
View File
File diff suppressed because it is too large Load Diff
+1222 -565
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1218 -561
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1220 -563
View File
File diff suppressed because it is too large Load Diff
+1226 -569
View File
File diff suppressed because it is too large Load Diff
+1222 -565
View File
File diff suppressed because it is too large Load Diff
+1218 -561
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1218 -561
View File
File diff suppressed because it is too large Load Diff
+1218 -561
View File
File diff suppressed because it is too large Load Diff
+1218 -561
View File
File diff suppressed because it is too large Load Diff
+1222 -565
View File
File diff suppressed because it is too large Load Diff
+1223 -566
View File
File diff suppressed because it is too large Load Diff
+1222 -565
View File
File diff suppressed because it is too large Load Diff
+1224 -567
View File
File diff suppressed because it is too large Load Diff
+38 -24
View File
@@ -1064,23 +1064,24 @@ QVariant DiagramImageItem::itemChange(GraphicsItemChange change, const QVariant
@brief DiagramImageItem::computeDisplayPixmap @brief DiagramImageItem::computeDisplayPixmap
Re-derives what pixmap_ should be from first principles: crop the Re-derives what pixmap_ should be from first principles: crop the
true original down to the chosen region, then colour-key whichever true original down to the chosen region, then colour-key whichever
colours have been picked out of it. Used whenever crop() or colours have been picked out of it, each at its own tolerance. Used
setTransparentColor() changes one of those two independently, so whenever crop() or setTransparentColor() changes one of those two
the other's effect is correctly re-applied on top rather than lost independently, so the other's effect is correctly re-applied on top
or compounded -- cropping after colours were already picked has to rather than lost or compounded -- cropping after colours were
still show them keyed out; picking colours after a crop has to only already picked has to still show them keyed out; picking colours
ever consider what's still actually part of the image. after a crop has to only ever consider what's still actually part
of the image.
@param base the true, uncropped original @param base the true, uncropped original
@param cropRect the region of base to keep, in base's own coordinates @param cropRect the region of base to keep, in base's own coordinates
@param colors colours to key transparent within the cropped region @param colors colours (each with its own tolerance) to key transparent within the cropped region
@param tolerance how loosely to match those colours, 0-100
*/ */
QPixmap DiagramImageItem::computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<QColor> &colors, int tolerance) QPixmap DiagramImageItem::computeDisplayPixmap(const QPixmap &base, const QRect &cropRect,
const QList<ImageTransparentColorDialog::PickedColor> &colors)
{ {
const QPixmap cropped = cropRect == base.rect() ? base : base.copy(cropRect); const QPixmap cropped = cropRect == base.rect() ? base : base.copy(cropRect);
if (colors.isEmpty()) if (colors.isEmpty())
return cropped; return cropped;
return QPixmap::fromImage(ImageTransparentColorDialog::applyColorKey(cropped.toImage(), colors, tolerance)); return QPixmap::fromImage(ImageTransparentColorDialog::applyColorKey(cropped.toImage(), colors));
} }
/** /**
@@ -1142,19 +1143,25 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
m_base_pixmap = pixmap; m_base_pixmap = pixmap;
m_crop_rect = pixmap.rect(); m_crop_rect = pixmap.rect();
m_transparent_colors.clear(); m_transparent_colors.clear();
m_transparent_tolerance = 10;
const QDomElement colorsElement = e.firstChildElement("transparent_colors"); const QDomElement colorsElement = e.firstChildElement("transparent_colors");
bool hasColors = !colorsElement.isNull(); bool hasColors = !colorsElement.isNull();
if (hasColors) if (hasColors)
{ {
m_transparent_tolerance = colorsElement.attribute("tolerance", "10").toInt(); // Files saved before per-colour tolerance existed wrote a
// single value on the wrapper element itself, shared by every
// colour -- kept here purely as the fallback default for a
// <color> that doesn't carry its own attribute, which for
// those old files is every one of them, exactly reproducing
// what they used to do (one tolerance applied to all of them).
const int wrapperTolerance = colorsElement.attribute("tolerance", "10").toInt();
for (const QDomElement &colorElement : QET::findInDomElement(colorsElement, "color")) for (const QDomElement &colorElement : QET::findInDomElement(colorsElement, "color"))
{ {
m_transparent_colors.append(QColor( m_transparent_colors.append({
colorElement.attribute("r").toInt(), QColor(colorElement.attribute("r").toInt(),
colorElement.attribute("g").toInt(), colorElement.attribute("g").toInt(),
colorElement.attribute("b").toInt())); colorElement.attribute("b").toInt()),
colorElement.attribute("tolerance", QString::number(wrapperTolerance)).toInt()});
} }
} }
@@ -1296,13 +1303,21 @@ QDomElement DiagramImageItem::toXml(QDomDocument &document) const
if (hasColors) if (hasColors)
{ {
QDomElement colorsElement = document.createElement("transparent_colors"); QDomElement colorsElement = document.createElement("transparent_colors");
colorsElement.setAttribute("tolerance", m_transparent_tolerance); // Best-effort fallback for an OLDER version of this same code
for (const QColor &color : m_transparent_colors) // (from before per-colour tolerance existed) reading a file
// saved by this one: uses the first colour's own tolerance as
// a single, plausible value rather than some fixed default,
// in case it ever needs to open a file like this. Newer code
// (including this version) always prefers each <color>'s own
// attribute below over this one.
colorsElement.setAttribute("tolerance", m_transparent_colors.first().tolerance);
for (const auto &pc : m_transparent_colors)
{ {
QDomElement colorElement = document.createElement("color"); QDomElement colorElement = document.createElement("color");
colorElement.setAttribute("r", color.red()); colorElement.setAttribute("r", pc.color.red());
colorElement.setAttribute("g", color.green()); colorElement.setAttribute("g", pc.color.green());
colorElement.setAttribute("b", color.blue()); colorElement.setAttribute("b", pc.color.blue());
colorElement.setAttribute("tolerance", pc.tolerance);
colorsElement.appendChild(colorElement); colorsElement.appendChild(colorElement);
} }
result.appendChild(colorsElement); result.appendChild(colorsElement);
@@ -1520,12 +1535,11 @@ void DiagramImageItem::setTransparentColor()
QWidget *parentWidget = diagram()->views().isEmpty() ? nullptr : diagram()->views().first(); QWidget *parentWidget = diagram()->views().isEmpty() ? nullptr : diagram()->views().first();
const QPixmap croppedBase = m_base_pixmap.copy(m_crop_rect); const QPixmap croppedBase = m_base_pixmap.copy(m_crop_rect);
ImageTransparentColorDialog dialog(croppedBase, m_transparent_colors, m_transparent_tolerance, parentWidget); ImageTransparentColorDialog dialog(croppedBase, m_transparent_colors, parentWidget);
if (dialog.exec() != QDialog::Accepted) if (dialog.exec() != QDialog::Accepted)
return; return;
m_transparent_colors = dialog.pickedColors(); m_transparent_colors = dialog.pickedColors();
m_transparent_tolerance = dialog.tolerance();
const QPixmap oldPixmap = pixmap_; const QPixmap oldPixmap = pixmap_;
const QPixmap newPixmap = dialog.resultPixmap(); const QPixmap newPixmap = dialog.resultPixmap();
@@ -1609,7 +1623,7 @@ void DiagramImageItem::crop()
const QPointF oldPos = pos(); const QPointF oldPos = pos();
const QPixmap oldPixmap = pixmap_; const QPixmap oldPixmap = pixmap_;
const QPixmap newPixmap = computeDisplayPixmap(m_base_pixmap, newCropRect, m_transparent_colors, m_transparent_tolerance); const QPixmap newPixmap = computeDisplayPixmap(m_base_pixmap, newCropRect, m_transparent_colors);
m_crop_rect = newCropRect; m_crop_rect = newCropRect;
// boundingRect() is exactly QRectF(pixmap_.rect()) (confirmed by // boundingRect() is exactly QRectF(pixmap_.rect()) (confirmed by
+14 -13
View File
@@ -20,6 +20,7 @@
#include "qetgraphicsitem.h" #include "qetgraphicsitem.h"
#include "shapetransform.h" #include "shapetransform.h"
#include "../ui/imagetransparentcolordialog.h"
#include <QColor> #include <QColor>
#include <QList> #include <QList>
@@ -128,7 +129,7 @@ class DiagramImageItem : public QetGraphicsItem {
void setTransparentColor(); void setTransparentColor();
void crop(); void crop();
void restoreAspectRatio(); void restoreAspectRatio();
static QPixmap computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<QColor> &colors, int tolerance); static QPixmap computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<ImageTransparentColorDialog::PickedColor> &colors);
void toggleHandleMode(); void toggleHandleMode();
HandleMode nextHandleMode() const; HandleMode nextHandleMode() const;
@@ -161,22 +162,22 @@ class DiagramImageItem : public QetGraphicsItem {
QPixmap pixmap_; QPixmap pixmap_;
// The true, pristine original -- never itself cropped or colour- // The true, pristine original -- never itself cropped or colour-
// keyed. pixmap_ (the displayed result) is always re-derived from // keyed. pixmap_ (the displayed result) is always re-derived from
// this plus m_crop_rect and m_transparent_colors/tolerance, via // this plus m_crop_rect and m_transparent_colors (each colour
// computeDisplayPixmap(). Without keeping this separate, re-opening // carrying its own tolerance), via computeDisplayPixmap(). Without
// either the crop or transparency dialog after using the other // keeping this separate, re-opening either the crop or
// would show an already-modified image as if it were the source -- // transparency dialog after using the other would show an
// areas already cropped away or coloured out would be gone for // already-modified image as if it were the source -- areas already
// good, with no way to recover or adjust them, only start over. // cropped away or coloured out would be gone for good, with no way
// Updated by whatever genuinely replaces or reorients the image's // to recover or adjust them, only start over. Updated by whatever
// actual content (construction, replaceImage(), and mirror(), which // genuinely replaces or reorients the image's actual content
// also mirrors m_crop_rect to keep referring to the same region of // (construction, replaceImage(), and mirror(), which also mirrors
// the now-flipped base) -- never by crop() or setTransparentColor() // m_crop_rect to keep referring to the same region of the
// now-flipped base) -- never by crop() or setTransparentColor()
// themselves, which only ever change which subset of this base is // themselves, which only ever change which subset of this base is
// shown. // shown.
QPixmap m_base_pixmap; QPixmap m_base_pixmap;
QRect m_crop_rect; // relative to m_base_pixmap; equals m_base_pixmap.rect() when nothing has been cropped QRect m_crop_rect; // relative to m_base_pixmap; equals m_base_pixmap.rect() when nothing has been cropped
QList<QColor> m_transparent_colors; QList<ImageTransparentColorDialog::PickedColor> m_transparent_colors;
int m_transparent_tolerance = 10;
// Independent scaleX/scaleY here is the actual point of this whole // Independent scaleX/scaleY here is the actual point of this whole
// member: QGraphicsItem::scale() is a single, uniform float, which // member: QGraphicsItem::scale() is a single, uniform float, which
+126 -84
View File
@@ -25,6 +25,7 @@
#include <QPainter> #include <QPainter>
#include <QPushButton> #include <QPushButton>
#include <QSlider> #include <QSlider>
#include <QToolButton>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QVector> #include <QVector>
@@ -48,10 +49,10 @@ ClickableImageLabel::ClickableImageLabel(const QImage &sourceImage, QWidget *par
const qreal scaleH = qreal(MAX_DISPLAY_SIZE) / m_source.height(); const qreal scaleH = qreal(MAX_DISPLAY_SIZE) / m_source.height();
m_displayScale = qMin(qreal(1.0), qMin(scaleW, scaleH)); // never upscale a small image, only ever shrink a large one m_displayScale = qMin(qreal(1.0), qMin(scaleW, scaleH)); // never upscale a small image, only ever shrink a large one
const QImage displayImage = (m_displayScale < 1.0) m_displayImage = (m_displayScale < 1.0)
? m_source.scaled(m_source.size() * m_displayScale, Qt::KeepAspectRatio, Qt::SmoothTransformation) ? m_source.scaled(m_source.size() * m_displayScale, Qt::KeepAspectRatio, Qt::SmoothTransformation)
: m_source; : m_source;
setPixmap(QPixmap::fromImage(displayImage)); setPixmap(QPixmap::fromImage(m_displayImage));
setCursor(Qt::CrossCursor); setCursor(Qt::CrossCursor);
setToolTip(tr("Cliquez pour choisir une couleur")); setToolTip(tr("Cliquez pour choisir une couleur"));
} }
@@ -91,23 +92,23 @@ void ClickableImageLabel::mousePressEvent(QMouseEvent *event)
@param pixmap the image to pick a transparent colour from @param pixmap the image to pick a transparent colour from
@param parent @param parent
*/ */
ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixmap, const QList<QColor> &existingColors, ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixmap,
int existingTolerance, QWidget *parent) : const QList<PickedColor> &existingColors, QWidget *parent) :
QDialog(parent), QDialog(parent),
m_sourceImage(basePixmap.toImage()), m_sourceImage(basePixmap.toImage()),
m_pickedColors(existingColors), m_pickedColors(existingColors)
m_tolerance(existingTolerance)
{ {
setWindowTitle(tr("Couleur transparente")); setWindowTitle(tr("Couleur transparente"));
if (!m_pickedColors.isEmpty())
m_lastToleranceUsed = m_pickedColors.last().tolerance;
m_sourceLabel = new ClickableImageLabel(m_sourceImage, this); m_sourceLabel = new ClickableImageLabel(m_sourceImage, this);
m_previewSourceImage = m_sourceLabel->displayImage();
m_previewLabel = new QLabel(this); m_previewLabel = new QLabel(this);
m_hintLabel = new QLabel(this); m_hintLabel = new QLabel(this);
m_hintLabel->setWordWrap(true);
m_toleranceSlider = new QSlider(Qt::Horizontal, this);
m_toleranceSlider->setRange(0, 100);
m_toleranceSlider->setValue(m_tolerance);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
m_okButton = buttons->button(QDialogButtonBox::Ok); m_okButton = buttons->button(QDialogButtonBox::Ok);
@@ -119,29 +120,19 @@ ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixm
grid->addWidget(m_sourceLabel, 1, 0); grid->addWidget(m_sourceLabel, 1, 0);
grid->addWidget(m_previewLabel, 1, 1); grid->addWidget(m_previewLabel, 1, 1);
// An empty row to start with if existingColors is empty -- // One row per picked colour, appended/removed as rebuildSwatches()
// rebuildSwatches() below populates it either way (including from // runs -- starts empty either way, populated by the very first
// existingColors on the first call), and again as colours get // rebuildSwatches() call below (including from existingColors, if
// added or removed. // any were passed in).
m_swatchesLayout = new QHBoxLayout; m_swatchesLayout = new QVBoxLayout;
auto *colorRow = new QHBoxLayout;
colorRow->addWidget(m_hintLabel);
colorRow->addStretch();
colorRow->addLayout(m_swatchesLayout);
auto *toleranceRow = new QHBoxLayout;
toleranceRow->addWidget(new QLabel(tr("Tolérance")));
toleranceRow->addWidget(m_toleranceSlider);
auto *mainLayout = new QVBoxLayout(this); auto *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(grid); mainLayout->addLayout(grid);
mainLayout->addLayout(colorRow); mainLayout->addWidget(m_hintLabel);
mainLayout->addLayout(toleranceRow); mainLayout->addLayout(m_swatchesLayout);
mainLayout->addWidget(buttons); mainLayout->addWidget(buttons);
connect(m_sourceLabel, &ClickableImageLabel::colorPicked, this, &ImageTransparentColorDialog::onColorPicked); connect(m_sourceLabel, &ClickableImageLabel::colorPicked, this, &ImageTransparentColorDialog::onColorPicked);
connect(m_toleranceSlider, &QSlider::valueChanged, this, &ImageTransparentColorDialog::onToleranceChanged);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
@@ -156,30 +147,55 @@ ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixm
the first choice, with no way to work with more than one colour (a the first choice, with no way to work with more than one colour (a
white background *and* a grey border, say) in the same pass. white background *and* a grey border, say) in the same pass.
Skips an exact duplicate rather than adding a second, indistinguishable Skips an exact duplicate rather than adding a second, indistinguishable
swatch for it. row for it. The new colour is seeded with whatever tolerance was
last used (not some fixed default), so successive picks in one
session feel consistent.
@param color the colour sampled from the source image @param color the colour sampled from the source image
*/ */
void ImageTransparentColorDialog::onColorPicked(const QColor &color) void ImageTransparentColorDialog::onColorPicked(const QColor &color)
{ {
if (m_pickedColors.contains(color)) for (const PickedColor &existing : std::as_const(m_pickedColors))
return; if (existing.color == color)
return;
m_pickedColors.append(color); m_pickedColors.append({color, m_lastToleranceUsed});
m_okButton->setEnabled(true); m_okButton->setEnabled(true);
rebuildSwatches(); rebuildSwatches();
updatePreview(); updatePreview();
} }
/**
@brief ImageTransparentColorDialog::setToleranceForIndex
Updates one colour's own tolerance in place -- deliberately doesn't
call rebuildSwatches(): only a number changed, not which rows exist
or what order they're in, so recreating every row's widgets (and,
worse, the very slider currently being dragged) on each tick would
be both wasteful and liable to interrupt the drag itself.
@param index which entry in m_pickedColors changed
@param value its new tolerance, 0-100
*/
void ImageTransparentColorDialog::setToleranceForIndex(int index, int value)
{
if (index < 0 || index >= m_pickedColors.size())
return;
m_pickedColors[index].tolerance = value;
m_lastToleranceUsed = value;
updatePreview();
}
/** /**
@brief ImageTransparentColorDialog::removeColor @brief ImageTransparentColorDialog::removeColor
Removes one colour from the set -- the counterpart onColorPicked() Removes one colour from the set -- the counterpart onColorPicked()
was missing entirely before: picking the wrong pixel by mistake had was missing entirely before: picking the wrong pixel by mistake had
no way to undo except cancelling the whole dialog and starting over. no way to undo except cancelling the whole dialog and starting over.
@param color the colour to remove @param index position in m_pickedColors to remove
*/ */
void ImageTransparentColorDialog::removeColor(const QColor &color) void ImageTransparentColorDialog::removeColor(int index)
{ {
m_pickedColors.removeAll(color); if (index < 0 || index >= m_pickedColors.size())
return;
m_pickedColors.removeAt(index);
m_okButton->setEnabled(!m_pickedColors.isEmpty()); m_okButton->setEnabled(!m_pickedColors.isEmpty());
rebuildSwatches(); rebuildSwatches();
updatePreview(); updatePreview();
@@ -187,11 +203,16 @@ void ImageTransparentColorDialog::removeColor(const QColor &color)
/** /**
@brief ImageTransparentColorDialog::rebuildSwatches @brief ImageTransparentColorDialog::rebuildSwatches
Rebuilds the row of picked-colour swatches from scratch against the Rebuilds the list of picked-colour rows from scratch against the
current m_pickedColors -- simpler and safer than trying to current m_pickedColors -- simpler and safer than trying to
incrementally add/remove individual widgets in sync with the list, incrementally add/remove individual rows in sync with the list,
given the list only ever changes one colour at a time and is never given the list only ever changes one colour at a time and is never
large enough for a full rebuild to be a real cost. large enough for a full rebuild to be a real cost. Each row is a
colour swatch (a plain, round, non-interactive indicator -- there's
nothing left to click it FOR, now that each row carries its own
slider instead of one shared slider needing a row selected first),
that row's own tolerance slider, and a small "x" button that
removes it.
*/ */
void ImageTransparentColorDialog::rebuildSwatches() void ImageTransparentColorDialog::rebuildSwatches()
{ {
@@ -202,88 +223,109 @@ void ImageTransparentColorDialog::rebuildSwatches()
delete item; delete item;
} }
for (const QColor &color : std::as_const(m_pickedColors)) for (int i = 0; i < m_pickedColors.size(); ++i)
{ {
auto *swatch = new QPushButton(this); const PickedColor &pc = m_pickedColors.at(i);
auto *row = new QWidget(this);
auto *rowLayout = new QHBoxLayout(row);
rowLayout->setContentsMargins(0, 0, 0, 0);
auto *swatch = new QLabel(row);
swatch->setFixedSize(24, 24); swatch->setFixedSize(24, 24);
swatch->setStyleSheet(QStringLiteral("background-color: rgb(%1,%2,%3); border: 1px solid palette(mid);") swatch->setStyleSheet(QStringLiteral(
.arg(color.red()).arg(color.green()).arg(color.blue())); "background-color: rgb(%1,%2,%3); border: 1px solid palette(mid); border-radius: 12px;")
swatch->setToolTip(tr("rgb(%1, %2, %3) -- cliquer pour retirer").arg(color.red()).arg(color.green()).arg(color.blue())); .arg(pc.color.red()).arg(pc.color.green()).arg(pc.color.blue()));
connect(swatch, &QPushButton::clicked, this, [this, color]() { removeColor(color); }); swatch->setToolTip(tr("rgb(%1, %2, %3)").arg(pc.color.red()).arg(pc.color.green()).arg(pc.color.blue()));
m_swatchesLayout->addWidget(swatch);
auto *slider = new QSlider(Qt::Horizontal, row);
slider->setRange(0, 100);
slider->setValue(pc.tolerance);
slider->setToolTip(tr("Tolérance pour cette couleur"));
connect(slider, &QSlider::valueChanged, this, [this, i](int value) { setToleranceForIndex(i, value); });
auto *removeButton = new QToolButton(row);
removeButton->setText(QStringLiteral("×"));
removeButton->setToolTip(tr("Retirer cette couleur"));
connect(removeButton, &QToolButton::clicked, this, [this, i]() { removeColor(i); });
rowLayout->addWidget(swatch);
rowLayout->addWidget(slider);
rowLayout->addWidget(removeButton);
m_swatchesLayout->addWidget(row);
} }
m_hintLabel->setText(m_pickedColors.isEmpty() m_hintLabel->setText(m_pickedColors.isEmpty()
? tr("Cliquez sur l'image pour choisir une couleur") ? tr("Cliquez sur l'image pour choisir une couleur")
: tr("Cliquez sur l'image pour ajouter une couleur, ou sur une pastille pour la retirer")); : tr("Cliquez sur l'image pour ajouter une couleur. Ajustez la tolérance de chaque couleur avec son curseur, ou cliquez sur × pour la retirer."));
}
/**
@brief ImageTransparentColorDialog::onToleranceChanged
@param value the new tolerance, 0-100
*/
void ImageTransparentColorDialog::onToleranceChanged(int value)
{
m_tolerance = value;
if (!m_pickedColors.isEmpty())
updatePreview();
} }
/** /**
@brief ImageTransparentColorDialog::updatePreview @brief ImageTransparentColorDialog::updatePreview
Recomputes the checkerboard-backed preview against the current set Recomputes the checkerboard-backed preview against the current set
of picked colours and the shared tolerance. Always runs against of picked colours, each at its own tolerance. Runs against
m_sourceImage (the original, full-resolution image), not any m_previewSourceImage (the same downsampled copy ClickableImageLabel
already-keyed result -- so adjusting the tolerance, or adding or already computed for its own display, not the full-resolution
removing a colour, re-evaluates every picked colour from scratch m_sourceImage) -- this fires on every tolerance slider tick, not
each time rather than compounding successive passes. just on release, and a full-resolution O(width*height*colours) pass
per tick was visibly laggy on a large source image. resultPixmap(),
below, still computes the final, committed result at full
resolution -- only this live preview is downsampled. Always
re-evaluates every picked colour from scratch against
m_previewSourceImage, not any already-keyed result, so adjusting a
tolerance or adding/removing a colour never compounds successive
passes.
*/ */
void ImageTransparentColorDialog::updatePreview() void ImageTransparentColorDialog::updatePreview()
{ {
const QImage keyed = applyColorKey(m_sourceImage, m_pickedColors, m_tolerance); const QImage keyed = applyColorKey(m_previewSourceImage, m_pickedColors);
m_previewLabel->setPixmap(onCheckerboard(keyed)); m_previewLabel->setPixmap(onCheckerboard(keyed));
} }
/** /**
@brief ImageTransparentColorDialog::resultPixmap @brief ImageTransparentColorDialog::resultPixmap
@return the colour-keyed pixmap against every picked colour, or the @return the colour-keyed pixmap against every picked colour, each at
original pixmap unchanged if none were ever picked (the Ok button its own tolerance, or the original pixmap unchanged if none were
stays disabled until at least one is, so this is mostly a defensive ever picked (the Ok button stays disabled until at least one is, so
fallback). this is mostly a defensive fallback).
*/ */
QPixmap ImageTransparentColorDialog::resultPixmap() const QPixmap ImageTransparentColorDialog::resultPixmap() const
{ {
if (m_pickedColors.isEmpty()) if (m_pickedColors.isEmpty())
return QPixmap::fromImage(m_sourceImage); return QPixmap::fromImage(m_sourceImage);
return QPixmap::fromImage(applyColorKey(m_sourceImage, m_pickedColors, m_tolerance)); return QPixmap::fromImage(applyColorKey(m_sourceImage, m_pickedColors));
} }
/** /**
@brief ImageTransparentColorDialog::applyColorKey @brief ImageTransparentColorDialog::applyColorKey
Binary transparency within tolerance, not a smooth falloff: every Binary transparency within tolerance, not a smooth falloff: every
pixel within `tolerance` (0-100, mapped onto the maximum possible pixel within a colour's own `tolerance` (0-100, mapped onto the
RGB distance) of *any* of keyColors becomes fully transparent, maximum possible RGB distance) of that colour becomes fully
everything else keeps its existing alpha untouched. Squared distance transparent, everything else keeps its existing alpha untouched --
throughout, avoiding a sqrt per pixel; breaks out of the inner loop each of keyColors is checked against its own threshold, not one
on the first matching colour, since further matches wouldn't change shared across all of them, since a background colour and a border
the outcome. colour rarely call for the same looseness of match. Squared
distance throughout, avoiding a sqrt per pixel; breaks out of the
inner loop on the first matching colour, since further matches
wouldn't change the outcome.
@param source the image to key @param source the image to key
@param keyColors the colours to make transparent @param keyColors the colours (each with its own tolerance) to make transparent
@param tolerance 0 (exact match only) to 100 (everything)
@return the resulting image, always in Format_ARGB32 @return the resulting image, always in Format_ARGB32
*/ */
QImage ImageTransparentColorDialog::applyColorKey(const QImage &source, const QList<QColor> &keyColors, int tolerance) QImage ImageTransparentColorDialog::applyColorKey(const QImage &source, const QList<PickedColor> &keyColors)
{ {
QImage result = source.convertToFormat(QImage::Format_ARGB32); QImage result = source.convertToFormat(QImage::Format_ARGB32);
if (keyColors.isEmpty()) if (keyColors.isEmpty())
return result; return result;
QVector<QRgb> keys; struct KeyEntry { QRgb rgb; qint64 threshold; };
QVector<KeyEntry> keys;
keys.reserve(keyColors.size()); keys.reserve(keyColors.size());
for (const QColor &c : keyColors) for (const PickedColor &pc : keyColors)
keys.append(c.rgb()); {
const qint64 threshold = qint64(pc.tolerance) * pc.tolerance * 3 * 255 * 255 / (100 * 100);
const qint64 threshold = qint64(tolerance) * tolerance * 3 * 255 * 255 / (100 * 100); keys.append({pc.color.rgb(), threshold});
}
for (int y = 0; y < result.height(); ++y) for (int y = 0; y < result.height(); ++y)
{ {
@@ -291,11 +333,11 @@ QImage ImageTransparentColorDialog::applyColorKey(const QImage &source, const QL
for (int x = 0; x < result.width(); ++x) for (int x = 0; x < result.width(); ++x)
{ {
const QRgb px = line[x]; const QRgb px = line[x];
for (const QRgb &key : keys) for (const KeyEntry &key : std::as_const(keys))
{ {
const int dr = qRed(px) - qRed(key), dg = qGreen(px) - qGreen(key), db = qBlue(px) - qBlue(key); const int dr = qRed(px) - qRed(key.rgb), dg = qGreen(px) - qGreen(key.rgb), db = qBlue(px) - qBlue(key.rgb);
const qint64 distSq = qint64(dr) * dr + qint64(dg) * dg + qint64(db) * db; const qint64 distSq = qint64(dr) * dr + qint64(dg) * dg + qint64(db) * db;
if (distSq <= threshold) if (distSq <= key.threshold)
{ {
line[x] = qRgba(qRed(px), qGreen(px), qBlue(px), 0); line[x] = qRgba(qRed(px), qGreen(px), qBlue(px), 0);
break; break;
+63 -37
View File
@@ -24,9 +24,8 @@
#include <QLabel> #include <QLabel>
#include <QList> #include <QList>
class QSlider; class QVBoxLayout;
class QPushButton; class QPushButton;
class QHBoxLayout;
class QMouseEvent; class QMouseEvent;
/** /**
@@ -45,6 +44,17 @@ class ClickableImageLabel : public QLabel
public: public:
explicit ClickableImageLabel(const QImage &sourceImage, QWidget *parent = nullptr); explicit ClickableImageLabel(const QImage &sourceImage, QWidget *parent = nullptr);
/// The same downsampled image already computed for display --
/// reused as the live-preview source so a tolerance drag runs
/// its per-tick colour-key pass against a small image instead
/// of the full-resolution one, which on a large source (a
/// scanned schematic background, several Mpx) made every
/// intermediate slider tick visibly lag. resultPixmap() still
/// computes the final, committed result from the true
/// full-resolution source -- only the live preview is
/// downsampled.
QImage displayImage() const { return m_displayImage; }
signals: signals:
void colorPicked(const QColor &color); void colorPicked(const QColor &color);
@@ -53,6 +63,7 @@ class ClickableImageLabel : public QLabel
private: private:
QImage m_source; QImage m_source;
QImage m_displayImage;
qreal m_displayScale = 1.0; qreal m_displayScale = 1.0;
}; };
@@ -60,73 +71,88 @@ class ClickableImageLabel : public QLabel
@brief The ImageTransparentColorDialog class @brief The ImageTransparentColorDialog class
Lets the user click directly on a preview of the image to sample one Lets the user click directly on a preview of the image to sample one
or more colors -- each click adds to the set rather than replacing or more colors -- each click adds to the set rather than replacing
the previous pick, shown as a row of removable swatches -- adjust a the previous pick -- each shown as its own row: a colour swatch, a
shared tolerance, and see a live checkerboard-backed preview of the slider for that colour's own tolerance right next to it, and a
result before committing. A self-contained modal dialog rather than remove button. Each colour keeps its own tolerance rather than
a diagram-level "click the canvas to pick" interaction mode, since sharing one: a white background and a grey border rarely need the
this needs neither undo-during-drag nor coexistence with other same looseness of match, and forcing one tolerance onto both meant
tools; it only ever needs a handful of clicks, evaluated against a either the background left ragged edges or the border ate into
pixmap the caller already has in hand. content near it. A live checkerboard-backed preview of the combined
result updates as any slider moves. A self-contained modal dialog
rather than a diagram-level "click the canvas to pick" interaction
mode, since this needs neither undo-during-drag nor coexistence
with other tools; it only ever needs a handful of clicks, evaluated
against a pixmap the caller already has in hand.
*/ */
class ImageTransparentColorDialog : public QDialog class ImageTransparentColorDialog : public QDialog
{ {
Q_OBJECT Q_OBJECT
public: public:
/// One picked colour and the tolerance it's individually keyed
/// with -- the whole point of this being a struct rather than
/// two parallel lists is that the two can never drift out of
/// index alignment with each other.
struct PickedColor
{
QColor color;
int tolerance;
};
/// @param basePixmap the pristine source to pick colours from -- /// @param basePixmap the pristine source to pick colours from --
/// the caller's responsibility to pass the true original, not /// the caller's responsibility to pass the true original, not
/// an already colour-keyed result, or previously-transparent /// an already colour-keyed result, or previously-transparent
/// areas would show as plain background rather than a pickable /// areas would show as plain background rather than a pickable
/// surface, and re-picking the same colour would be a no-op. /// surface, and re-picking the same colour would be a no-op.
/// @param existingColors colours already keyed out of basePixmap /// @param existingColors colours (with their individual
/// in a previous session, shown as swatches from the start /// tolerances) already keyed out of basePixmap in a previous
/// rather than forcing them to be re-picked from scratch. /// session, shown as rows from the start rather than forcing
/// @param existingTolerance the tolerance from that previous /// them to be re-picked from scratch.
/// session, if any. explicit ImageTransparentColorDialog(const QPixmap &basePixmap,
explicit ImageTransparentColorDialog(const QPixmap &basePixmap, const QList<QColor> &existingColors = {}, const QList<PickedColor> &existingColors = {}, QWidget *parent = nullptr);
int existingTolerance = 10, QWidget *parent = nullptr);
/// The resulting pixmap: basePixmap unchanged if no colour is /// The resulting pixmap: basePixmap unchanged if no colour is
/// picked, colour-keyed against every picked colour otherwise. /// picked, colour-keyed against every picked colour (each at
/// its own tolerance) otherwise.
QPixmap resultPixmap() const; QPixmap resultPixmap() const;
/// The final set of picked colours, for the caller to remember /// The final set of picked colours and their individual
/// across dialog sessions -- may differ from existingColors if /// tolerances, for the caller to remember across dialog
/// any were added or removed. /// sessions -- may differ from existingColors if any were
QList<QColor> pickedColors() const { return m_pickedColors; } /// added, removed, or had their tolerance adjusted.
/// The final tolerance, for the same reason. QList<PickedColor> pickedColors() const { return m_pickedColors; }
int tolerance() const { return m_tolerance; }
/// Public so DiagramImageItem can re-derive its display pixmap /// Public so DiagramImageItem can re-derive its display pixmap
/// directly (base + crop + these colours) without needing to /// directly (base + crop + these colours) without needing to
/// re-open this dialog every time the crop region changes -- /// re-open this dialog every time the crop region changes --
/// binary transparency within tolerance, not a smooth falloff: /// binary transparency within tolerance, not a smooth falloff:
/// every pixel within `tolerance` (0-100, mapped onto the /// every pixel within a colour's own `tolerance` (0-100, mapped
/// maximum possible RGB distance) of *any* of keyColors becomes /// onto the maximum possible RGB distance) of that colour
/// fully transparent, everything else keeps its existing alpha /// becomes fully transparent, everything else keeps its
/// untouched. Squared distance throughout, avoiding a sqrt per /// existing alpha untouched. Squared distance throughout,
/// pixel; breaks out of the inner loop on the first matching /// avoiding a sqrt per pixel; breaks out of the inner loop on
/// colour, since further matches wouldn't change the outcome. /// the first matching colour, since further matches wouldn't
static QImage applyColorKey(const QImage &source, const QList<QColor> &keyColors, int tolerance); /// change the outcome.
static QImage applyColorKey(const QImage &source, const QList<PickedColor> &keyColors);
private slots: private slots:
void onColorPicked(const QColor &color); void onColorPicked(const QColor &color);
void onToleranceChanged(int value);
private: private:
void removeColor(const QColor &color); void setToleranceForIndex(int index, int value);
void removeColor(int index);
void rebuildSwatches(); void rebuildSwatches();
void updatePreview(); void updatePreview();
static QPixmap onCheckerboard(const QImage &image); static QPixmap onCheckerboard(const QImage &image);
QImage m_sourceImage; QImage m_sourceImage;
QList<QColor> m_pickedColors; QImage m_previewSourceImage; // downsampled -- see ClickableImageLabel::displayImage()'s comment for why
int m_tolerance = 10; QList<PickedColor> m_pickedColors;
int m_lastToleranceUsed = 10; // seeds a newly-picked colour's own tolerance, so successive picks in one session feel consistent rather than each resetting to some fixed default
ClickableImageLabel *m_sourceLabel; ClickableImageLabel *m_sourceLabel;
QLabel *m_previewLabel; QLabel *m_previewLabel;
QHBoxLayout *m_swatchesLayout; QVBoxLayout *m_swatchesLayout; // one row per picked colour, stacked vertically -- each row now carries its own slider, too wide to lay out side by side the way plain swatches once were
QLabel *m_hintLabel; QLabel *m_hintLabel;
QSlider *m_toleranceSlider;
QPushButton *m_okButton; QPushButton *m_okButton;
}; };