#include "Document/CameraRawGeometryCalibration.h" #include "Document/BrushStroke.h" #include "Document/CameraRaw.h" #include "Document/ImageAdjustments.h" #include "IO/ImageExporter.h" #include #include #include #include #include extern "C" { #include "AdjustPixels.h" #include "BrushPixels.h" } using ImageAdjustmentPixels::clamp; namespace { bool readable(const CameraRawGeometryGuide &guide) { return std::hypot(guide.endX - guide.startX, guide.endY - guide.startY) > 0.01; } // Swift's guidedCorrections: the first line levels, the second tilts. std::array guided(const std::vector &guides) { if (guides.empty()) return {0, 0, 0}; const CameraRawGeometryGuide &first = guides.front(); const double dx = first.endX - first.startX, dy = first.endY - first.startY; if (std::hypot(dx, dy) <= 1e-4) return {0, 0, 0}; double rotate = -std::atan2(dy, dx) * 180 / std::numbers::pi; if (rotate > 45) rotate -= 90; else if (rotate < -45) rotate += 90; double vertical = 0, horizontal = 0; if (guides.size() > 1) { const CameraRawGeometryGuide &second = guides[1]; const double sx = second.endX - second.startX, sy = second.endY - second.startY; if (std::hypot(sx, sy) > 1e-4) { const double angle = std::atan2(sy, sx) * 180 / std::numbers::pi; vertical = std::abs(angle) > 45 ? (angle > 0 ? 25 : -25) : 0; horizontal = std::abs(angle) <= 45 ? (angle > 0 ? 25 : -25) : 0; } } return {vertical, horizontal, rotate}; } } bool CameraRawGeometrySettings::usesGuides() const { return upright == CameraRawUprightMode::guided && std::any_of(guides.begin(), guides.end(), readable); } bool CameraRawGeometrySettings::adjusts() const { return usesGuides() || vertical != 0 || horizontal != 0 || rotate != 0 || aspect != 0 || scale != 0 || offsetX != 0 || offsetY != 0; } CameraRawGeometrySettings CameraRawGeometrySettings::normalized() const { CameraRawGeometrySettings result = *this; result.vertical = clamp(vertical, -100, 100, 0); result.horizontal = clamp(horizontal, -100, 100, 0); result.rotate = clamp(rotate, -45, 45, 0); result.aspect = clamp(aspect, -100, 100, 0); result.scale = clamp(scale, -100, 100, 0); result.offsetX = clamp(offsetX, -100, 100, 0); result.offsetY = clamp(offsetY, -100, 100, 0); result.guides.clear(); std::copy_if(guides.begin(), guides.end(), std::back_inserter(result.guides), readable); return result; } std::array CameraRawGeometrySettings::effectiveCorrections() const { if (upright == CameraRawUprightMode::off) return {vertical, horizontal, rotate}; const std::array extra = guided(guides); return {vertical + extra[0], horizontal + extra[1], rotate + extra[2]}; } // Core Image's corners, y measured up from the bottom. std::array CameraRawGeometrySettings::outputCorners(int width, int height, double vertical, double horizontal, double rotation) const { const double w = width, h = height; const double strength = projection == CameraRawProjection::perspective ? 1.0 : 0.55; const double v = vertical / 100 * w * 0.18 * strength, hz = horizontal / 100 * h * 0.18 * strength; const double aspectScale = 1 + aspect / 200, zoom = 1 + scale / 100; const double shiftX = offsetX / 100 * w * 0.15, shiftY = offsetY / 100 * h * 0.15; std::array corners{QPointF(-v + shiftX, h + shiftY), QPointF(w + v + shiftX, h + shiftY), QPointF(w + hz + shiftX, -shiftY), QPointF(-hz + shiftX, -shiftY)}; const QPointF center(w / 2 + shiftX, h / 2 + shiftY); const double radians = rotation * std::numbers::pi / 180, cosine = std::cos(radians), sine = std::sin(radians); for (QPointF &corner : corners) { const double dx = corner.x() - center.x(), dy = corner.y() - center.y(); corner = QPointF(center.x() + dx * cosine - dy * sine, center.y() + dx * sine + dy * cosine); } if (aspectScale != 1) for (QPointF &corner : corners) corner = QPointF(center.x() + (corner.x() - center.x()) * aspectScale, center.y() + (corner.y() - center.y()) / aspectScale); if (zoom != 1) for (QPointF &corner : corners) corner = center + (corner - center) * zoom; return corners; } QImage CameraRawGeometrySettings::apply(const QImage &image) const { const CameraRawGeometrySettings settings = normalized(); const int width = image.width(), height = image.height(); if (!settings.adjusts() || width <= 0 || height <= 0) return image; const auto [vertical, horizontal, rotate] = settings.effectiveCorrections(); const std::array corners = settings.outputCorners(width, height, vertical, horizontal, rotate); // Core Image counts y upward: top-down, a corner's y flips. QPolygonF target; for (const QPointF &corner : corners) target << QPointF(corner.x(), height - corner.y()); QTransform warp; // Normalized settings keep the quad whole; else a bug. if (!QTransform::quadToQuad(QPolygonF(QRectF(0, 0, width, height)).mid(0, 4), target, warp)) throw std::logic_error("the geometry's corners fold"); QImage result = BrushRaster::context(width, height, false); { QPainter painter(&result); painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); painter.setTransform(warp); painter.drawImage(QRectF(0, 0, width, height), image); } if (!settings.constrainCrop) return result; std::array edges{}; brush_alpha_bounds(result.constBits(), size_t(width), size_t(height), size_t(result.bytesPerLine()), edges.data()); const QRect crop(int(edges[0]), int(edges[1]), int(edges[2] - edges[0]), int(edges[3] - edges[1])); if (crop.width() < 1 || crop.height() < 1 || (crop.width() >= width && crop.height() >= height)) return result; const QImage cropped = result.copy(crop); if (cropped.isNull()) throw ExportError(ExportError::Kind::render); QImage fitted = BrushRaster::context(width, height, false); const double fit = std::min(double(width) / crop.width(), double(height) / crop.height()); QPainter painter(&fitted); BrushRaster::draw(cropped, QRectF((width - crop.width() * fit) / 2, (height - crop.height() * fit) / 2, crop.width() * fit, crop.height() * fit), painter); return fitted; } QString rawValue(CameraRawProcessVersion version) { return QStringLiteral("Version %1").arg(int(version) + 1); } QString summary(CameraRawProcessVersion version) { switch (version) { case CameraRawProcessVersion::version1: return QStringLiteral("Earliest response. Hue, saturation, and shadow tint move about half as far as Version 6."); case CameraRawProcessVersion::version2: return QStringLiteral("A little stronger than Version 1. The sliders below still fall well short of the current look."); case CameraRawProcessVersion::version3: return QStringLiteral("Firmer color than Version 2. Primary shifts stay gentler than the current process."); case CameraRawProcessVersion::version4: return QStringLiteral("The 2012 response. Calibration reaches most of the strength used by Version 6."); case CameraRawProcessVersion::version5: return QStringLiteral("Close to the current process, with slightly softer primary and shadow shifts."); case CameraRawProcessVersion::version6: return QStringLiteral("Current default. The calibration sliders below apply at full strength."); } throw std::logic_error("unknown process version"); } bool CameraRawCalibrationSettings::adjusts() const { return shadowTint != 0 || redHue != 0 || redSaturation != 0 || greenHue != 0 || greenSaturation != 0 || blueHue != 0 || blueSaturation != 0; } CameraRawCalibrationSettings CameraRawCalibrationSettings::normalized() const { return {process, clamp(shadowTint, -100, 100, 0), clamp(redHue, -100, 100, 0), clamp(redSaturation, -100, 100, 0), clamp(greenHue, -100, 100, 0), clamp(greenSaturation, -100, 100, 0), clamp(blueHue, -100, 100, 0), clamp(blueSaturation, -100, 100, 0)}; } void CameraRawSettings::applyCalibration(uchar *pixels, int width, int height, qsizetype stride) const { const CameraRawCalibrationSettings primaries = calibration.normalized(); if (!primaries.adjusts()) return; adjust_camera_raw_calibration(pixels, size_t(width), size_t(height), size_t(stride), primaries.shadowTint, primaries.redHue, primaries.redSaturation, primaries.greenHue, primaries.greenSaturation, primaries.blueHue, primaries.blueSaturation, int(primaries.process) + 1); }