#include "IO/PSD/PSDVector.h" #include "Document/DocumentLimits.h" #include "Document/BrushStroke.h" #include "Document/EditorSession.h" #include #include #include #include namespace { std::optional entry(const PSDVector::Extra &extra, const char *key) { const QString name = QString::fromLatin1(key); if (!extra.contains(name)) return std::nullopt; return extra.at(name); } qint32 i32(const QByteArray &bytes, qsizetype at) { return qFromBigEndian(bytes.constData() + at); } qint16 i16(const QByteArray &bytes, qsizetype at) { return qFromBigEndian(bytes.constData() + at); } std::optional offset(const QByteArray &key, const QByteArray &data, qsizetype start = 0) { const qsizetype found = start < data.size() ? data.indexOf(key, start) : -1; if (found < 0) return std::nullopt; return found; } std::optional doubleAt(qsizetype at, const QByteArray &data) { if (at + 8 > data.size()) return std::nullopt; return std::bit_cast(qFromBigEndian(data.constData() + at)); } std::optional number(const QByteArray &data, const QByteArray &key) { const std::optional start = offset(key, data); if (!start) return std::nullopt; const qsizetype type = *start + key.size(); if (type + 12 > data.size() || data.mid(type, 4) != "doub") return std::nullopt; return doubleAt(type + 4, data); } std::optional int32(const QByteArray &data, const QByteArray &key) { const std::optional start = offset(key, data); if (!start) return std::nullopt; const qsizetype type = *start + key.size(); if (type + 8 > data.size() || data.mid(type, 4) != "long") return std::nullopt; return i32(data, type + 4); } QPointF point(const QByteArray &bytes, qsizetype at, QSizeF canvas) { const double y = double(i32(bytes, at)) / 0x1000000; const double x = double(i32(bytes, at + 4)) / 0x1000000; return QPointF(x * canvas.width(), y * canvas.height()); } // CGRect.integral, in doubles, so a wild box never overflows. QRectF integral(const QRectF &box) { const double left = std::floor(box.left()), top = std::floor(box.top()); return QRectF(left, top, std::ceil(box.right()) - left, std::ceil(box.bottom()) - top); } // Within the side limit and the pixel budget, else tooLarge. QSize pixelSize(QSizeF size, qint64 remainingPixels) { if (std::abs(size.width()) > DocumentLimits::maxSide || std::abs(size.height()) > DocumentLimits::maxSide) throw ImageImportError(ImageImportError::Kind::tooLarge); const qint64 budget = std::min(EditorSession::maxShapePixels, std::max(0, remainingPixels)); // Boxes are whole here, so the whole pixels decide alone. const int width = std::max(1, int(size.width())), height = std::max(1, int(size.height())); if (qint64(width) * height > budget) throw ImageImportError(ImageImportError::Kind::tooLarge); return QSize(width, height); } struct Origination { ShapeKind kind; QRectF bounds; double cornerRadius = 0; }; // Photoshop's `vogk`: 1 and 2 rectangles, 5 an ellipse. std::optional origination(const std::optional &data) { const std::optional type = data ? int32(*data, "keyOriginType") : std::nullopt; if (!type) return std::nullopt; ShapeKind kind; switch (*type) { case 1: case 2: kind = ShapeKind::rectangle; break; case 5: kind = ShapeKind::ellipse; break; default: return std::nullopt; } const qsizetype from = offset("keyOriginShapeBBox", *data).value_or(0); const std::optional left = PSDVector::unit(*data, "Left", from), top = PSDVector::unit(*data, "Top ", from), right = PSDVector::unit(*data, "Rght", from), bottom = PSDVector::unit(*data, "Btom", from); if (!left || !top || !right || !bottom) return std::nullopt; const QRectF bounds(*left, *top, *right - *left, *bottom - *top); if (!(bounds.width() >= 1) || !(bounds.height() >= 1) || !std::isfinite(bounds.x()) || !std::isfinite(bounds.y()) || !std::isfinite(bounds.width()) || !std::isfinite(bounds.height())) return std::nullopt; Origination origin{kind, bounds}; const std::optional radiiAt = kind == ShapeKind::rectangle ? offset("keyOriginRRectRadii", *data) : std::nullopt; if (radiiAt) { std::vector radii; for (const char *key : {"topLeft", "topRight", "bottomRight", "bottomLeft"}) if (const std::optional radius = PSDVector::unit(*data, key, *radiiAt)) radii.push_back(*radius); if (radii.size() == 4) { const auto [lo, hi] = std::minmax_element(radii.begin(), radii.end()); if (*hi - *lo > 0.5) return std::nullopt; origin.cornerRadius = *hi; } } return origin; } // Four sharp anchors in one subpath read as a rectangle. std::optional sharpRect(const std::optional &data, QSizeF canvas) { const std::optional outline = data ? PSDVector::path(*data, canvas) : std::nullopt; if (!outline) return std::nullopt; qsizetype at = 8; int remaining = 0; std::vector anchors; bool sharp = true; while (at + 26 <= data->size()) { const int type = i16(*data, at); const QByteArray body = data->mid(at + 2, 24); at += 26; if (type == 0 || type == 3) { if (!anchors.empty()) return std::nullopt; remaining = i16(body, 0); } else if ((type == 1 || type == 2 || type == 4 || type == 5) && remaining > 0) { remaining -= 1; const QPointF incoming = point(body, 0, canvas), anchor = point(body, 8, canvas), outgoing = point(body, 16, canvas); if (std::hypot(incoming.x() - anchor.x(), incoming.y() - anchor.y()) > 0.5 || std::hypot(outgoing.x() - anchor.x(), outgoing.y() - anchor.y()) > 0.5) sharp = false; anchors.push_back(anchor); } } if (!sharp || anchors.size() != 4) return std::nullopt; const QRectF box = outline->boundingRect(); if (!(box.width() >= 1) || !(box.height() >= 1)) return std::nullopt; return Origination{ShapeKind::rectangle, box}; } } std::optional PSDVector::live(const Extra &extra, QSizeF canvas, qint64 remainingPixels) { const std::optional stroke = entry(extra, "vstk"), solid = entry(extra, "SoCo"); const bool fillEnabled = (stroke ? boolean(*stroke, "fillEnabled") : std::nullopt).value_or(solid.has_value()); const bool strokeEnabled = (stroke ? boolean(*stroke, "strokeEnabled") : std::nullopt).value_or(false); const std::optional fill = solid ? rgb(*solid) : std::nullopt; if (!fillEnabled || !fill) return std::nullopt; std::optional origin = origination(entry(extra, "vogk")); if (!origin) origin = sharpRect(extra.contains(QStringLiteral("vmsk")) ? entry(extra, "vmsk") : entry(extra, "vsms"), canvas); if (!origin) return std::nullopt; QRectF box = integral(origin->bounds); box.setSize(QSizeF(pixelSize(box.size(), remainingPixels))); const LayerShapeStyle style{origin->kind, fill->r, fill->g, fill->b, origin->cornerRadius}; const QImage image = EditorSession::shapeImage(style.kind, box.size(), style.color(), style.cornerRadius); std::vector notes; if (strokeEnabled) notes.push_back(QStringLiteral("The Photoshop stroke isn’t supported on shape layers and was omitted.")); return Live{style, box, image, notes}; } std::optional PSDVector::raster(const Extra &extra, QSizeF canvas, qint64 remainingPixels) { const std::optional mask = extra.contains(QStringLiteral("vmsk")) ? entry(extra, "vmsk") : entry(extra, "vsms"); std::optional outline = mask ? path(*mask, canvas) : std::nullopt; if (!outline) return std::nullopt; const std::optional solid = entry(extra, "SoCo"), stroke = entry(extra, "vstk"); const std::optional fill = solid ? rgb(*solid) : std::nullopt; const bool fillEnabled = (stroke ? boolean(*stroke, "fillEnabled") : std::nullopt).value_or(fill.has_value()); const bool strokeEnabled = (stroke ? boolean(*stroke, "strokeEnabled") : std::nullopt).value_or(false); const std::optional strokeColor = stroke ? rgb(*stroke) : std::nullopt; const double strokeWidth = (stroke ? unit(*stroke, "strokeStyleLineWidth") : std::nullopt).value_or(1); if (!((fillEnabled && fill) || (strokeEnabled && strokeColor))) return std::nullopt; if (!std::isfinite(strokeWidth)) return std::nullopt; if (strokeEnabled && !(strokeWidth >= 0 && strokeWidth <= DocumentLimits::maxSide)) throw ImageImportError(ImageImportError::Kind::tooLarge); QRectF box = outline->boundingRect(); if (strokeEnabled) { const double grow = std::ceil(strokeWidth / 2 + 1); box.adjust(-grow, -grow, grow, grow); } box = integral(box); const QSize size = pixelSize(box.size(), remainingPixels); QImage image = BrushRaster::context(size.width(), size.height(), false); QPainter painter(&image); painter.translate(-box.x(), -box.y()); painter.setRenderHint(QPainter::Antialiasing); // Both CoreGraphics fills here are nonzero winding. outline->setFillRule(Qt::WindingFill); if (fillEnabled && fill) painter.fillPath(*outline, QColor::fromRgbF(float(fill->r), float(fill->g), float(fill->b))); if (strokeEnabled && strokeColor) { // SVG's miter rule is CoreGraphics': a bevel past the limit. QPen pen(QColor::fromRgbF(float(strokeColor->r), float(strokeColor->g), float(strokeColor->b)), strokeWidth, Qt::SolidLine, Qt::FlatCap, Qt::SvgMiterJoin); pen.setMiterLimit(10); painter.strokePath(*outline, pen); } painter.end(); return Raster{image, QRectF(box.topLeft(), QSizeF(size))}; } std::optional PSDVector::path(const QByteArray &data, QSizeF canvas) { if (data.size() < 8 || !(canvas.width() > 0) || !(canvas.height() > 0)) return std::nullopt; QPainterPath result; qsizetype at = 8; int remaining = 0; bool closed = true, first = true; QPointF previousOut; while (at + 26 <= data.size()) { const int type = i16(data, at); const QByteArray body = data.mid(at + 2, 24); at += 26; if (type == 0 || type == 3) { if (!first && closed) result.closeSubpath(); remaining = i16(body, 0); closed = type == 0; first = true; } else if ((type == 1 || type == 2 || type == 4 || type == 5) && remaining > 0) { remaining -= 1; const QPointF incoming = point(body, 0, canvas), anchor = point(body, 8, canvas), outgoing = point(body, 16, canvas); if (first) { result.moveTo(anchor); first = false; } else { result.cubicTo(previousOut, incoming, anchor); } previousOut = outgoing; } } if (!first && closed) result.closeSubpath(); // A CGPath is empty only without elements. if (result.elementCount() == 0) return std::nullopt; return result; } std::optional PSDVector::rgb(const QByteArray &data) { const std::optional r = number(data, "Rd "), g = number(data, "Grn "), b = number(data, "Bl "); if (!r || !g || !b) return std::nullopt; const auto channel = [](double value) { return value > 1 ? std::min(255.0, std::max(0.0, value)) / 255 : std::min(1.0, std::max(0.0, value)); }; return RGB{channel(*r), channel(*g), channel(*b)}; } std::optional PSDVector::boolean(const QByteArray &data, const QByteArray &key) { const std::optional start = offset(key, data); if (!start) return std::nullopt; const qsizetype type = *start + key.size(); if (type + 5 > data.size() || data.mid(type, 4) != "bool") return std::nullopt; return data[type + 4] != 0; } std::optional PSDVector::unit(const QByteArray &data, const QByteArray &key, qsizetype from) { const std::optional keyAt = offset(key, data, from); const std::optional units = keyAt ? offset("UntF", data, *keyAt) : std::nullopt; if (!units) return std::nullopt; return doubleAt(*units + 8, data); }