mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-21 07:54:13 +02:00
Compare commits
5 Commits
70c665199f
...
76ff5c4dd9
| Author | SHA1 | Date | |
|---|---|---|---|
| 76ff5c4dd9 | |||
| b395527915 | |||
| 7741bb33b2 | |||
| f02b576cb8 | |||
| e0ef24fe68 |
+1223
-566
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1209
-553
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1224
-567
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1222
-565
File diff suppressed because it is too large
Load Diff
+1220
-563
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1226
-569
File diff suppressed because it is too large
Load Diff
+1222
-565
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1224
-567
File diff suppressed because it is too large
Load Diff
+1222
-565
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1218
-561
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1220
-563
File diff suppressed because it is too large
Load Diff
+1226
-569
File diff suppressed because it is too large
Load Diff
+1222
-565
File diff suppressed because it is too large
Load Diff
+1218
-561
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1218
-561
File diff suppressed because it is too large
Load Diff
+1218
-561
File diff suppressed because it is too large
Load Diff
+1218
-561
File diff suppressed because it is too large
Load Diff
+1222
-565
File diff suppressed because it is too large
Load Diff
+1223
-566
File diff suppressed because it is too large
Load Diff
+1222
-565
File diff suppressed because it is too large
Load Diff
+1224
-567
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user