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
Re-derives what pixmap_ should be from first principles: crop the
true original down to the chosen region, then colour-key whichever
colours have been picked out of it. Used whenever crop() or
setTransparentColor() changes one of those two independently, so
the other's effect is correctly re-applied on top rather than lost
or compounded -- cropping after colours were already picked has to
still show them keyed out; picking colours after a crop has to only
ever consider what's still actually part of the image.
colours have been picked out of it, each at its own tolerance. Used
whenever crop() or setTransparentColor() changes one of those two
independently, so the other's effect is correctly re-applied on top
rather than lost or compounded -- cropping after colours were
already picked has to still show them keyed out; picking colours
after a crop has to only ever consider what's still actually part
of the image.
@param base the true, uncropped original
@param cropRect the region of base to keep, in base's own coordinates
@param colors colours to key transparent within the cropped region
@param tolerance how loosely to match those colours, 0-100
@param colors colours (each with its own tolerance) to key transparent within the cropped region
*/
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);
if (colors.isEmpty())
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_crop_rect = pixmap.rect();
m_transparent_colors.clear();
m_transparent_tolerance = 10;
const QDomElement colorsElement = e.firstChildElement("transparent_colors");
bool hasColors = !colorsElement.isNull();
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"))
{
m_transparent_colors.append(QColor(
colorElement.attribute("r").toInt(),
colorElement.attribute("g").toInt(),
colorElement.attribute("b").toInt()));
m_transparent_colors.append({
QColor(colorElement.attribute("r").toInt(),
colorElement.attribute("g").toInt(),
colorElement.attribute("b").toInt()),
colorElement.attribute("tolerance", QString::number(wrapperTolerance)).toInt()});
}
}
@@ -1296,13 +1303,21 @@ QDomElement DiagramImageItem::toXml(QDomDocument &document) const
if (hasColors)
{
QDomElement colorsElement = document.createElement("transparent_colors");
colorsElement.setAttribute("tolerance", m_transparent_tolerance);
for (const QColor &color : m_transparent_colors)
// Best-effort fallback for an OLDER version of this same code
// (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");
colorElement.setAttribute("r", color.red());
colorElement.setAttribute("g", color.green());
colorElement.setAttribute("b", color.blue());
colorElement.setAttribute("r", pc.color.red());
colorElement.setAttribute("g", pc.color.green());
colorElement.setAttribute("b", pc.color.blue());
colorElement.setAttribute("tolerance", pc.tolerance);
colorsElement.appendChild(colorElement);
}
result.appendChild(colorsElement);
@@ -1520,12 +1535,11 @@ void DiagramImageItem::setTransparentColor()
QWidget *parentWidget = diagram()->views().isEmpty() ? nullptr : diagram()->views().first();
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)
return;
m_transparent_colors = dialog.pickedColors();
m_transparent_tolerance = dialog.tolerance();
const QPixmap oldPixmap = pixmap_;
const QPixmap newPixmap = dialog.resultPixmap();
@@ -1609,7 +1623,7 @@ void DiagramImageItem::crop()
const QPointF oldPos = pos();
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;
// boundingRect() is exactly QRectF(pixmap_.rect()) (confirmed by
+14 -13
View File
@@ -20,6 +20,7 @@
#include "qetgraphicsitem.h"
#include "shapetransform.h"
#include "../ui/imagetransparentcolordialog.h"
#include <QColor>
#include <QList>
@@ -128,7 +129,7 @@ class DiagramImageItem : public QetGraphicsItem {
void setTransparentColor();
void crop();
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();
HandleMode nextHandleMode() const;
@@ -161,22 +162,22 @@ class DiagramImageItem : public QetGraphicsItem {
QPixmap pixmap_;
// The true, pristine original -- never itself cropped or colour-
// keyed. pixmap_ (the displayed result) is always re-derived from
// this plus m_crop_rect and m_transparent_colors/tolerance, via
// computeDisplayPixmap(). Without keeping this separate, re-opening
// either the crop or transparency dialog after using the other
// would show an already-modified image as if it were the source --
// areas already cropped away or coloured out would be gone for
// good, with no way to recover or adjust them, only start over.
// Updated by whatever genuinely replaces or reorients the image's
// actual content (construction, replaceImage(), and mirror(), which
// also mirrors m_crop_rect to keep referring to the same region of
// the now-flipped base) -- never by crop() or setTransparentColor()
// this plus m_crop_rect and m_transparent_colors (each colour
// carrying its own tolerance), via computeDisplayPixmap(). Without
// keeping this separate, re-opening either the crop or
// transparency dialog after using the other would show an
// already-modified image as if it were the source -- areas already
// cropped away or coloured out would be gone for good, with no way
// to recover or adjust them, only start over. Updated by whatever
// genuinely replaces or reorients the image's actual content
// (construction, replaceImage(), and mirror(), which also mirrors
// 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
// shown.
QPixmap m_base_pixmap;
QRect m_crop_rect; // relative to m_base_pixmap; equals m_base_pixmap.rect() when nothing has been cropped
QList<QColor> m_transparent_colors;
int m_transparent_tolerance = 10;
QList<ImageTransparentColorDialog::PickedColor> m_transparent_colors;
// Independent scaleX/scaleY here is the actual point of this whole
// member: QGraphicsItem::scale() is a single, uniform float, which
+126 -84
View File
@@ -25,6 +25,7 @@
#include <QPainter>
#include <QPushButton>
#include <QSlider>
#include <QToolButton>
#include <QVBoxLayout>
#include <QVector>
@@ -48,10 +49,10 @@ ClickableImageLabel::ClickableImageLabel(const QImage &sourceImage, QWidget *par
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
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;
setPixmap(QPixmap::fromImage(displayImage));
setPixmap(QPixmap::fromImage(m_displayImage));
setCursor(Qt::CrossCursor);
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 parent
*/
ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixmap, const QList<QColor> &existingColors,
int existingTolerance, QWidget *parent) :
ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixmap,
const QList<PickedColor> &existingColors, QWidget *parent) :
QDialog(parent),
m_sourceImage(basePixmap.toImage()),
m_pickedColors(existingColors),
m_tolerance(existingTolerance)
m_pickedColors(existingColors)
{
setWindowTitle(tr("Couleur transparente"));
if (!m_pickedColors.isEmpty())
m_lastToleranceUsed = m_pickedColors.last().tolerance;
m_sourceLabel = new ClickableImageLabel(m_sourceImage, this);
m_previewSourceImage = m_sourceLabel->displayImage();
m_previewLabel = new QLabel(this);
m_hintLabel = new QLabel(this);
m_toleranceSlider = new QSlider(Qt::Horizontal, this);
m_toleranceSlider->setRange(0, 100);
m_toleranceSlider->setValue(m_tolerance);
m_hintLabel->setWordWrap(true);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
m_okButton = buttons->button(QDialogButtonBox::Ok);
@@ -119,29 +120,19 @@ ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixm
grid->addWidget(m_sourceLabel, 1, 0);
grid->addWidget(m_previewLabel, 1, 1);
// An empty row to start with if existingColors is empty --
// rebuildSwatches() below populates it either way (including from
// existingColors on the first call), and again as colours get
// added or removed.
m_swatchesLayout = new QHBoxLayout;
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);
// One row per picked colour, appended/removed as rebuildSwatches()
// runs -- starts empty either way, populated by the very first
// rebuildSwatches() call below (including from existingColors, if
// any were passed in).
m_swatchesLayout = new QVBoxLayout;
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(grid);
mainLayout->addLayout(colorRow);
mainLayout->addLayout(toleranceRow);
mainLayout->addWidget(m_hintLabel);
mainLayout->addLayout(m_swatchesLayout);
mainLayout->addWidget(buttons);
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::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
white background *and* a grey border, say) in the same pass.
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
*/
void ImageTransparentColorDialog::onColorPicked(const QColor &color)
{
if (m_pickedColors.contains(color))
return;
for (const PickedColor &existing : std::as_const(m_pickedColors))
if (existing.color == color)
return;
m_pickedColors.append(color);
m_pickedColors.append({color, m_lastToleranceUsed});
m_okButton->setEnabled(true);
rebuildSwatches();
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
Removes one colour from the set -- the counterpart onColorPicked()
was missing entirely before: picking the wrong pixel by mistake had
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());
rebuildSwatches();
updatePreview();
@@ -187,11 +203,16 @@ void ImageTransparentColorDialog::removeColor(const QColor &color)
/**
@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
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
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()
{
@@ -202,88 +223,109 @@ void ImageTransparentColorDialog::rebuildSwatches()
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->setStyleSheet(QStringLiteral("background-color: rgb(%1,%2,%3); border: 1px solid palette(mid);")
.arg(color.red()).arg(color.green()).arg(color.blue()));
swatch->setToolTip(tr("rgb(%1, %2, %3) -- cliquer pour retirer").arg(color.red()).arg(color.green()).arg(color.blue()));
connect(swatch, &QPushButton::clicked, this, [this, color]() { removeColor(color); });
m_swatchesLayout->addWidget(swatch);
swatch->setStyleSheet(QStringLiteral(
"background-color: rgb(%1,%2,%3); border: 1px solid palette(mid); border-radius: 12px;")
.arg(pc.color.red()).arg(pc.color.green()).arg(pc.color.blue()));
swatch->setToolTip(tr("rgb(%1, %2, %3)").arg(pc.color.red()).arg(pc.color.green()).arg(pc.color.blue()));
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()
? 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"));
}
/**
@brief ImageTransparentColorDialog::onToleranceChanged
@param value the new tolerance, 0-100
*/
void ImageTransparentColorDialog::onToleranceChanged(int value)
{
m_tolerance = value;
if (!m_pickedColors.isEmpty())
updatePreview();
: 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::updatePreview
Recomputes the checkerboard-backed preview against the current set
of picked colours and the shared tolerance. Always runs against
m_sourceImage (the original, full-resolution image), not any
already-keyed result -- so adjusting the tolerance, or adding or
removing a colour, re-evaluates every picked colour from scratch
each time rather than compounding successive passes.
of picked colours, each at its own tolerance. Runs against
m_previewSourceImage (the same downsampled copy ClickableImageLabel
already computed for its own display, not the full-resolution
m_sourceImage) -- this fires on every tolerance slider tick, not
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()
{
const QImage keyed = applyColorKey(m_sourceImage, m_pickedColors, m_tolerance);
const QImage keyed = applyColorKey(m_previewSourceImage, m_pickedColors);
m_previewLabel->setPixmap(onCheckerboard(keyed));
}
/**
@brief ImageTransparentColorDialog::resultPixmap
@return the colour-keyed pixmap against every picked colour, or the
original pixmap unchanged if none were ever picked (the Ok button
stays disabled until at least one is, so this is mostly a defensive
fallback).
@return the colour-keyed pixmap against every picked colour, each at
its own tolerance, or the original pixmap unchanged if none were
ever picked (the Ok button stays disabled until at least one is, so
this is mostly a defensive fallback).
*/
QPixmap ImageTransparentColorDialog::resultPixmap() const
{
if (m_pickedColors.isEmpty())
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
Binary transparency within tolerance, not a smooth falloff: every
pixel within `tolerance` (0-100, mapped onto the maximum possible
RGB distance) of *any* of keyColors becomes fully transparent,
everything else keeps its existing alpha untouched. 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.
pixel within a colour's own `tolerance` (0-100, mapped onto the
maximum possible RGB distance) of that colour becomes fully
transparent, everything else keeps its existing alpha untouched --
each of keyColors is checked against its own threshold, not one
shared across all of them, since a background colour and a border
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 keyColors the colours to make transparent
@param tolerance 0 (exact match only) to 100 (everything)
@param keyColors the colours (each with its own tolerance) to make transparent
@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);
if (keyColors.isEmpty())
return result;
QVector<QRgb> keys;
struct KeyEntry { QRgb rgb; qint64 threshold; };
QVector<KeyEntry> keys;
keys.reserve(keyColors.size());
for (const QColor &c : keyColors)
keys.append(c.rgb());
const qint64 threshold = qint64(tolerance) * tolerance * 3 * 255 * 255 / (100 * 100);
for (const PickedColor &pc : keyColors)
{
const qint64 threshold = qint64(pc.tolerance) * pc.tolerance * 3 * 255 * 255 / (100 * 100);
keys.append({pc.color.rgb(), threshold});
}
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)
{
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;
if (distSq <= threshold)
if (distSq <= key.threshold)
{
line[x] = qRgba(qRed(px), qGreen(px), qBlue(px), 0);
break;
+63 -37
View File
@@ -24,9 +24,8 @@
#include <QLabel>
#include <QList>
class QSlider;
class QVBoxLayout;
class QPushButton;
class QHBoxLayout;
class QMouseEvent;
/**
@@ -45,6 +44,17 @@ class ClickableImageLabel : public QLabel
public:
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:
void colorPicked(const QColor &color);
@@ -53,6 +63,7 @@ class ClickableImageLabel : public QLabel
private:
QImage m_source;
QImage m_displayImage;
qreal m_displayScale = 1.0;
};
@@ -60,73 +71,88 @@ class ClickableImageLabel : public QLabel
@brief The ImageTransparentColorDialog class
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
the previous pick, shown as a row of removable swatches -- adjust a
shared tolerance, and see a live checkerboard-backed preview of the
result before committing. 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.
the previous pick -- each shown as its own row: a colour swatch, a
slider for that colour's own tolerance right next to it, and a
remove button. Each colour keeps its own tolerance rather than
sharing one: a white background and a grey border rarely need the
same looseness of match, and forcing one tolerance onto both meant
either the background left ragged edges or the border ate into
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
{
Q_OBJECT
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 --
/// the caller's responsibility to pass the true original, not
/// an already colour-keyed result, or previously-transparent
/// areas would show as plain background rather than a pickable
/// surface, and re-picking the same colour would be a no-op.
/// @param existingColors colours already keyed out of basePixmap
/// in a previous session, shown as swatches from the start
/// rather than forcing them to be re-picked from scratch.
/// @param existingTolerance the tolerance from that previous
/// session, if any.
explicit ImageTransparentColorDialog(const QPixmap &basePixmap, const QList<QColor> &existingColors = {},
int existingTolerance = 10, QWidget *parent = nullptr);
/// @param existingColors colours (with their individual
/// tolerances) already keyed out of basePixmap in a previous
/// session, shown as rows from the start rather than forcing
/// them to be re-picked from scratch.
explicit ImageTransparentColorDialog(const QPixmap &basePixmap,
const QList<PickedColor> &existingColors = {}, QWidget *parent = nullptr);
/// 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;
/// The final set of picked colours, for the caller to remember
/// across dialog sessions -- may differ from existingColors if
/// any were added or removed.
QList<QColor> pickedColors() const { return m_pickedColors; }
/// The final tolerance, for the same reason.
int tolerance() const { return m_tolerance; }
/// The final set of picked colours and their individual
/// tolerances, for the caller to remember across dialog
/// sessions -- may differ from existingColors if any were
/// added, removed, or had their tolerance adjusted.
QList<PickedColor> pickedColors() const { return m_pickedColors; }
/// Public so DiagramImageItem can re-derive its display pixmap
/// directly (base + crop + these colours) without needing to
/// re-open this dialog every time the crop region changes --
/// binary transparency within tolerance, not a smooth falloff:
/// every pixel within `tolerance` (0-100, mapped onto the
/// maximum possible RGB distance) of *any* of keyColors becomes
/// fully transparent, everything else keeps its existing alpha
/// untouched. 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.
static QImage applyColorKey(const QImage &source, const QList<QColor> &keyColors, int tolerance);
/// every pixel within a colour's own `tolerance` (0-100, mapped
/// onto the maximum possible RGB distance) of that colour
/// becomes fully transparent, everything else keeps its
/// existing alpha untouched. 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.
static QImage applyColorKey(const QImage &source, const QList<PickedColor> &keyColors);
private slots:
void onColorPicked(const QColor &color);
void onToleranceChanged(int value);
private:
void removeColor(const QColor &color);
void setToleranceForIndex(int index, int value);
void removeColor(int index);
void rebuildSwatches();
void updatePreview();
static QPixmap onCheckerboard(const QImage &image);
QImage m_sourceImage;
QList<QColor> m_pickedColors;
int m_tolerance = 10;
QImage m_sourceImage;
QImage m_previewSourceImage; // downsampled -- see ClickableImageLabel::displayImage()'s comment for why
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;
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;
QSlider *m_toleranceSlider;
QPushButton *m_okButton;
};