184 lines
7.9 KiB
C++
184 lines
7.9 KiB
C++
#include "Document/BrushStroke.h"
|
|
#include "Document/LayerMask.h"
|
|
#include "Document/PixelAdjust.h"
|
|
#include "IO/ImageExporter.h"
|
|
#include "IO/ProjectStore.h"
|
|
#include "Rendering/RasterSnapshot.h"
|
|
#include <QRandomGenerator>
|
|
#include <cstring>
|
|
#include <new>
|
|
|
|
extern "C" {
|
|
#include "BrushPixels.h"
|
|
#include "HealPixels.h"
|
|
}
|
|
|
|
// The stroke as a layer: bounds, placement, the commit's image.
|
|
namespace {
|
|
QRectF integral(const QRectF &rect)
|
|
{
|
|
return QRectF(QPointF(std::floor(rect.left()), std::floor(rect.top())), QPointF(std::ceil(rect.right()), std::ceil(rect.bottom())));
|
|
}
|
|
|
|
// Half-open bounds of the pixels that are there, or empty.
|
|
QRectF alphaBounds(const QImage &pixels)
|
|
{
|
|
size_t edges[4] = {0, 0, 0, 0};
|
|
brush_alpha_bounds(pixels.constBits(), size_t(pixels.width()), size_t(pixels.height()), size_t(pixels.bytesPerLine()), edges);
|
|
if (edges[2] <= edges[0] || edges[3] <= edges[1])
|
|
return QRectF();
|
|
return QRectF(edges[0], edges[1], edges[2] - edges[0], edges[3] - edges[1]);
|
|
}
|
|
|
|
void drawSource(const ImportedImage &source, const QRectF &rect, QPainter &painter)
|
|
{
|
|
if (source.raster)
|
|
source.raster->draw(rect, painter);
|
|
else
|
|
BrushRaster::draw(source.image(), rect, painter);
|
|
}
|
|
}
|
|
|
|
QRectF BrushStroke::committedBounds() const
|
|
{
|
|
return integral(m_allocatedBounds.value_or(sourceRect));
|
|
}
|
|
|
|
LayerTransform BrushStroke::committedTransform() const
|
|
{
|
|
return transform(committedBounds());
|
|
}
|
|
|
|
LayerTransform BrushStroke::transform(const QRectF &bounds) const
|
|
{
|
|
const QPointF center = pixelToDocument.map(bounds.center());
|
|
LayerTransform result = paintTransform;
|
|
result.size = {bounds.width() * paintTransform.size.width() / width, bounds.height() * paintTransform.size.height() / height};
|
|
result.origin = {center.x() - result.size.width() / 2, center.y() - result.size.height() / 2};
|
|
return result;
|
|
}
|
|
|
|
// Swift's heal: the painted spot rebuilt from nearby texture.
|
|
void BrushStroke::heal()
|
|
{
|
|
if (!settings.healing || isMask)
|
|
return;
|
|
std::optional<QRectF> painted;
|
|
for (const auto &[key, coverage] : m_coverage) {
|
|
long edges[4];
|
|
heal_coverage_bounds(coverage.constBits(), size_t(coverage.width()), size_t(coverage.height()), size_t(coverage.bytesPerLine()), edges);
|
|
if (edges[2] <= edges[0] || edges[3] <= edges[1])
|
|
continue;
|
|
const QRectF rect = QRectF(edges[0], edges[1], edges[2] - edges[0], edges[3] - edges[1]).translated(m_tiles.at(key).rect.topLeft());
|
|
painted = painted ? painted->united(rect) : rect;
|
|
}
|
|
if (!painted)
|
|
return;
|
|
// Room for the kernel's search, about three spots away.
|
|
const double reach = (std::max(painted->width(), painted->height()) + 32) * 3.2;
|
|
const QRect region = painted->adjusted(-reach, -reach, reach, reach).intersected(QRectF(0, 0, width, height)).toAlignedRect();
|
|
QImage pixels = BrushRaster::context(region.width(), region.height(), false);
|
|
{
|
|
QPainter painter(&pixels);
|
|
const QRectF placed = sourceRect.translated(-region.topLeft());
|
|
if (m_source && m_source->raster)
|
|
m_source->raster->draw(placed, painter);
|
|
else if (m_source)
|
|
BrushRaster::draw(m_source->image(), placed, painter);
|
|
}
|
|
// The kernel reads coverage rows packed, as Swift's contexts are.
|
|
const std::unique_ptr<uint8_t[]> painting(new (std::nothrow) uint8_t[size_t(region.width()) * size_t(region.height())]());
|
|
if (!painting)
|
|
throw ExportError(ExportError::Kind::render);
|
|
for (const auto &[key, coverage] : m_coverage) {
|
|
const QRect tile = m_tiles.at(key).rect.toRect().translated(-region.topLeft());
|
|
const QRect within = tile.intersected(QRect(QPoint(0, 0), region.size()));
|
|
for (int y = within.top(); y <= within.bottom(); ++y)
|
|
std::memcpy(painting.get() + size_t(y) * size_t(region.width()) + size_t(within.left()),
|
|
coverage.constScanLine(y - tile.top()) + (within.left() - tile.left()), size_t(within.width()));
|
|
}
|
|
if (spot_heal(pixels.bits(), painting.get(), size_t(region.width()), size_t(region.height()), size_t(pixels.bytesPerLine()),
|
|
float(settings.opacity), int(settings.healingMode), QRandomGenerator::global()->generate()) != 0)
|
|
throw ProjectError(ProjectError::Kind::tooLarge);
|
|
for (const auto &[key, coverage] : m_coverage) {
|
|
Tile &tile = m_tiles.at(key);
|
|
QImage healed = tile.base;
|
|
healed.detach();
|
|
if (healed.isNull())
|
|
throw ExportError(ExportError::Kind::render);
|
|
{
|
|
QPainter painter(&healed);
|
|
BrushRaster::draw(pixels, QRectF(region).translated(-tile.rect.topLeft()), painter);
|
|
}
|
|
// Swift clips the context; the selection blends here instead.
|
|
tile.context = selectionClip ? PixelAdjust::blend(healed, tile.base, *selectionClip,
|
|
QTransform::fromTranslate(tile.rect.left(), tile.rect.top()) * pixelToDocument, false)
|
|
: healed;
|
|
tile.image = tile.context;
|
|
}
|
|
}
|
|
|
|
// Painting only adds alpha: the touched tiles alone are inspected.
|
|
PaintSnapshot BrushStroke::paintSnapshot() const
|
|
{
|
|
std::optional<QRectF> bounds = m_source ? std::optional(sourceRect) : std::nullopt;
|
|
for (const auto &[key, tile] : m_tiles) {
|
|
if (isMask)
|
|
break;
|
|
const QRectF rect = alphaBounds(tile.context);
|
|
if (rect.isEmpty())
|
|
continue;
|
|
const QRectF placed = rect.translated(tile.rect.topLeft());
|
|
bounds = bounds ? std::optional(bounds->united(placed)) : std::optional(placed);
|
|
}
|
|
// A mask keeps every tile touched: past its pixels, grows.
|
|
const QRectF crop = isMask ? committedBounds() : bounds.value_or(committedBounds());
|
|
const std::shared_ptr<const RasterSnapshot> raster = RasterSnapshot::replacing(m_source, sourceRect, patches(), crop, isMask, maskBackground);
|
|
return {ImportedImage(raster, raster->thumbnail(), layer.name), transform(crop), crop};
|
|
}
|
|
|
|
BrushCommit::Input BrushStroke::commitInput() const
|
|
{
|
|
const QRectF bounds = committedBounds();
|
|
std::vector<BrushPatch> shifted;
|
|
for (const BrushPatch &patch : patches())
|
|
shifted.push_back({patch.rect.translated(-bounds.topLeft()), patch.image});
|
|
return {int(bounds.width()), int(bounds.height()), m_source, shifted, isMask, layer.name, sourceRect.translated(-bounds.topLeft()), maskBackground};
|
|
}
|
|
|
|
ImportedImage BrushCommit::expandMask(const ImportedImage &asset, const Input &input, const QRectF &crop)
|
|
{
|
|
if (input.sourceRect == crop)
|
|
return asset;
|
|
QImage context = BrushRaster::context(int(crop.width()), int(crop.height()), true);
|
|
// New canvas area has no mask; old coverage stays aligned.
|
|
context.fill(Qt::white);
|
|
QPainter painter(&context);
|
|
drawSource(asset, input.sourceRect.translated(-crop.topLeft()), painter);
|
|
painter.end();
|
|
return LayerMask::assetFrom(context);
|
|
}
|
|
|
|
BrushCommit::Output BrushCommit::render(const Input &input)
|
|
{
|
|
QImage context = BrushRaster::context(input.width, input.height, input.mask);
|
|
if (input.mask)
|
|
context.fill(qRound(input.fill * 255));
|
|
QPainter painter(&context);
|
|
if (input.source)
|
|
drawSource(*input.source, input.sourceRect, painter);
|
|
for (const BrushPatch &patch : input.patches)
|
|
BrushRaster::draw(patch.image, patch.rect, painter);
|
|
painter.end();
|
|
const QRectF fullBounds(0, 0, input.width, input.height);
|
|
if (input.mask)
|
|
return {LayerMask::assetFrom(context), fullBounds};
|
|
// Every pixel with alpha stays, the soft rim included.
|
|
const QRectF found = alphaBounds(context);
|
|
const QRectF crop = found.isEmpty() ? fullBounds : found;
|
|
const QImage image = crop == fullBounds ? context : context.copy(crop.toRect());
|
|
if (image.isNull())
|
|
throw ExportError(ExportError::Kind::render);
|
|
return {ImportedImage(image, PixelAdjust::thumbnail(image), input.name), crop};
|
|
}
|