Files
OmaPhoto/src/Document/BrushStroke.cpp
T

164 lines
7.4 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Document/BrushStroke.h"
#include "Rendering/PoolMap.h"
#include "IO/ImageExporter.h"
#include <QtConcurrent>
#include <cmath>
#include <cstring>
#include <numeric>
#include <stdexcept>
QString rawValue(SpotHealingMode mode)
{
switch (mode) {
case SpotHealingMode::contentAware:
return QStringLiteral("Content-Aware");
case SpotHealingMode::createTexture:
return QStringLiteral("Create Texture");
case SpotHealingMode::proximityMatch:
return QStringLiteral("Proximity Match");
}
throw std::logic_error("unknown spot healing mode");
}
QImage BrushRaster::context(int width, int height, bool mask)
{
QImage surface(width, height, mask ? QImage::Format_Grayscale8 : QImage::Format_RGBA8888_Premultiplied);
if (surface.isNull())
throw ExportError(ExportError::Kind::render);
surface.fill(0);
return surface;
}
QImage BrushRaster::copy(const QImage &image)
{
QImage result = context(image.width(), image.height(), false);
if (image.format() != QImage::Format_RGBA8888_Premultiplied) {
QPainter painter(&result);
draw(image, QRectF(0, 0, image.width(), image.height()), painter);
return result;
}
for (int y = 0; y < image.height(); ++y)
std::memcpy(result.scanLine(y), image.constScanLine(y), size_t(image.width()) * 4);
return result;
}
void BrushRaster::inBands(qsizetype count, const std::function<void(qsizetype, qsizetype)> &body)
{
const int bands = count < 250'000 ? 1 : QThread::idealThreadCount() * 2;
const qsizetype size = (count + bands - 1) / bands;
try {
std::vector<int> indices(static_cast<size_t>(bands));
std::iota(indices.begin(), indices.end(), 0);
PoolMap::blocking(indices, [&](int band) {
const qsizetype start = band * size;
if (start < count)
body(start, std::min(size, count - start));
});
} catch (const std::bad_alloc &) {
// Out of memory fails as a context would.
throw ExportError(ExportError::Kind::render);
}
}
void BrushRaster::draw(const QImage &image, const QRectF &rect, QPainter &context, const QRectF &source)
{
context.save();
context.setRenderHint(QPainter::SmoothPixmapTransform, false);
context.setCompositionMode(QPainter::CompositionMode_Source);
context.drawImage(rect, image, source.isNull() ? QRectF(image.rect()) : source);
context.restore();
}
QImage BrushRaster::alphaView(const QImage &mask)
{
if (mask.format() != QImage::Format_Grayscale8)
throw std::logic_error("a layer mask must be 8-bit gray");
return QImage(mask.constBits(), mask.width(), mask.height(), mask.bytesPerLine(), QImage::Format_Alpha8);
}
QRectF BrushRaster::visibleRect(const QPainter &context)
{
const QPaintDevice &device = *context.device();
// A widget's size is logical; an image's is device pixels.
QTransform toDevice = context.combinedTransform();
if (device.devType() == QInternal::Widget)
toDevice *= QTransform::fromScale(1 / device.devicePixelRatio(), 1 / device.devicePixelRatio());
const QRectF bounds = toDevice.inverted().mapRect(QRectF(0, 0, device.width(), device.height()));
return context.hasClipping() ? bounds.intersected(context.clipBoundingRect()) : bounds;
}
QTransform BrushRaster::pixelToDocument(const LayerTransform &transform, int width, int height)
{
const QPointF center = transform.center();
QTransform map;
map.translate(center.x(), center.y());
map.rotate(transform.degrees());
map.scale(transform.size.width() / width * (transform.flipX ? -1 : 1),
transform.size.height() / height * (transform.flipY ? -1 : 1));
map.translate(-width / 2.0, -height / 2.0);
return map;
}
// Source-over of the colour at coverage × alpha, bytewise.
void BrushRaster::fill(const QColor &color, const QImage &coverage, const QRectF &rect, double alpha, QImage &context,
QPainter::CompositionMode mode, const QImage &clip)
{
if (coverage.format() != QImage::Format_Grayscale8 || coverage.size() != rect.size().toSize() || rect.topLeft() != rect.topLeft().toPoint())
throw std::logic_error("coverage must be gray and 1:1 with its rect");
if (!clip.isNull() && (clip.format() != QImage::Format_Grayscale8 || clip.size() != coverage.size()))
throw std::logic_error("a clip must be gray and sized as the coverage");
// Erasing takes coverage out; a mask never erases.
const bool erasing = mode == QPainter::CompositionMode_DestinationOut;
if ((!erasing && mode != QPainter::CompositionMode_SourceOver) || (erasing && context.format() == QImage::Format_Grayscale8))
throw std::logic_error("a brush fills source-over, or erases pixels");
const int left = int(rect.left()), top = int(rect.top());
const bool gray = context.format() == QImage::Format_Grayscale8;
if (!gray && context.format() != QImage::Format_RGBA8888_Premultiplied)
throw std::logic_error("a brush context is premultiplied RGBA or gray");
// What each channel moves towards; erasing moves towards nothing.
const double target[4] = {erasing ? 0 : color.redF() * 255, erasing ? 0 : color.greenF() * 255, erasing ? 0 : color.blueF() * 255,
erasing ? 0 : 255.0};
const int channels = gray ? 1 : 4;
for (int y = 0; y < coverage.height(); ++y) {
const uchar *mask = coverage.constScanLine(y);
// A clip scales the coverage, rounded once, as CoreGraphics composites.
const uchar *within = clip.isNull() ? nullptr : clip.constScanLine(y);
uchar *row = context.scanLine(top + y) + left * channels;
for (int x = 0; x < coverage.width(); ++x) {
const double k = mask[x] / 255.0 * alpha * (within ? within[x] / 255.0 : 1.0);
if (k <= 0)
continue;
// Values are never negative: adding a half rounds them.
uchar *pixel = row + x * channels;
for (int c = 0; c < channels; ++c)
pixel[c] = uchar(pixel[c] * (1 - k) + target[c] * k + 0.5);
}
}
}
QImage BrushRaster::shiftedByFraction(const QImage &image, double fx, double fy)
{
QImage shifted = BrushRaster::context(image.width() + 1, image.height() + 1, false);
// Weights in 1/65536: the pixel, left, above, above left.
const qint64 here = std::lround((1 - fx) * (1 - fy) * 65536), left = std::lround(fx * (1 - fy) * 65536);
const qint64 above = std::lround((1 - fx) * fy * 65536), corner = 65536 - here - left - above;
uchar *const out = shifted.bits();
const uchar *const in = image.constBits();
const qsizetype outStride = shifted.bytesPerLine(), inStride = image.bytesPerLine();
const int width = image.width(), height = image.height();
std::vector<int> rows(size_t(shifted.height()));
std::iota(rows.begin(), rows.end(), 0);
PoolMap::blocking(rows, [&](int y) {
const uchar *row = y < height ? in + y * inStride : nullptr, *up = y > 0 ? in + (y - 1) * inStride : nullptr;
uchar *line = out + y * outStride;
for (int x = 0; x <= width; ++x) {
for (int c = 0; c < 4; ++c) {
const qint64 sum = (row && x < width ? here * row[x * 4 + c] : 0) + (row && x > 0 ? left * row[(x - 1) * 4 + c] : 0)
+ (up && x < width ? above * up[x * 4 + c] : 0) + (up && x > 0 ? corner * up[(x - 1) * 4 + c] : 0);
line[x * 4 + c] = uchar((sum + 32768) >> 16);
}
}
});
return shifted;
}