#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 #include #include 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 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 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 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 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 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}; }