OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor (github.com/robbietilton/Compositor), whose Swift source is its specification. It reads and writes Compositor's .comp projects. Packages for Ubuntu, Fedora and Arch come from the release workflow; scripts/install/ holds one install script per distribution family, and flake.nix serves NixOS. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
c864304695
564 files changed
+100804
No files matched your search
@@ -0,0 +1,243 @@
|
||||
#include "IO/ImageImporter.h"
|
||||
#include "Logging.h"
|
||||
#include "Rendering/RasterSnapshot.h"
|
||||
#include <QColorSpace>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QImageReader>
|
||||
#include <QTransform>
|
||||
#include <cmath>
|
||||
#include <libheif/heif.h>
|
||||
|
||||
namespace {
|
||||
QString description(ImageImportError::Kind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case ImageImportError::Kind::unreadable:
|
||||
return QStringLiteral("The image could not be read. It may be damaged or unavailable.");
|
||||
case ImageImportError::Kind::unsupported:
|
||||
return QStringLiteral("Choose a JPEG, PNG, HEIC, or TIFF image.");
|
||||
case ImageImportError::Kind::tooLarge:
|
||||
return QStringLiteral("This import exceeds the current 100-megapixel document budget or 30,000-pixel side limit.");
|
||||
}
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
||||
[[noreturn]] void refuse(ImageImportError::Kind kind, const QString &path, const QString &reason)
|
||||
{
|
||||
qCWarning(lcIO).noquote() << "cannot import" << path + ":" << reason;
|
||||
throw ImageImportError(kind);
|
||||
}
|
||||
|
||||
void checkSize(const QSize &size, qint64 remainingPixels, const QString &path)
|
||||
{
|
||||
if (size.width() <= 0 || size.height() <= 0)
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("it states no size"));
|
||||
if (size.width() > 30'000 || size.height() > 30'000 || qint64(size.width()) * size.height() > remainingPixels)
|
||||
refuse(ImageImportError::Kind::tooLarge, path, QStringLiteral("%1 x %2 pixels").arg(size.width()).arg(size.height()));
|
||||
}
|
||||
|
||||
// HEVC in HEIF, by libheif's own reading of the brand.
|
||||
bool isHEIC(const QByteArray &head)
|
||||
{
|
||||
const char *type = heif_get_file_mime_type(reinterpret_cast<const uint8_t *>(head.constData()), int(head.size()));
|
||||
return qstrcmp(type, "image/heic") == 0;
|
||||
}
|
||||
|
||||
// BT.709's curve, shared by BT.601 and BT.2020, as a table.
|
||||
QList<uint16_t> videoCurve()
|
||||
{
|
||||
QList<uint16_t> table(4096);
|
||||
for (int index = 0; index < table.size(); ++index) {
|
||||
const double value = index / double(table.size() - 1);
|
||||
table[index] = uint16_t(std::lround(65535 * (value < 0.081 ? value / 4.5 : std::pow((value + 0.099) / 1.099, 1 / 0.45))));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
// A HEIC's colours: its ICC profile, else its nclx box.
|
||||
QColorSpace statedColorSpace(const heif_image_handle *handle, const QString &path)
|
||||
{
|
||||
QByteArray profile(qsizetype(heif_image_handle_get_raw_color_profile_size(handle)), Qt::Uninitialized);
|
||||
if (heif_image_handle_get_raw_color_profile(handle, profile.data()).code == heif_error_Ok)
|
||||
return QColorSpace::fromIccProfile(profile);
|
||||
heif_color_profile_nclx *box = nullptr;
|
||||
const heif_error error = heif_image_handle_get_nclx_color_profile(handle, &box);
|
||||
// No profile at all: untagged pixels count as sRGB.
|
||||
if (error.code == heif_error_Color_profile_does_not_exist)
|
||||
return QColorSpace();
|
||||
if (error.code != heif_error_Ok)
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("its nclx colour box: ") + QString::fromUtf8(error.message));
|
||||
const std::unique_ptr<heif_color_profile_nclx, void (*)(heif_color_profile_nclx *)> nclx(box, heif_nclx_color_profile_free);
|
||||
// Unspecified primaries or curve count as sRGB's.
|
||||
QColorSpace stated(QColorSpace::SRgb);
|
||||
if (nclx->color_primaries != heif_color_primaries_unspecified)
|
||||
stated = QColorSpace(QPointF(nclx->color_primary_white_x, nclx->color_primary_white_y),
|
||||
QPointF(nclx->color_primary_red_x, nclx->color_primary_red_y),
|
||||
QPointF(nclx->color_primary_green_x, nclx->color_primary_green_y),
|
||||
QPointF(nclx->color_primary_blue_x, nclx->color_primary_blue_y), QColorSpace::TransferFunction::SRgb);
|
||||
if (!stated.isValid())
|
||||
refuse(ImageImportError::Kind::unreadable, path,
|
||||
QStringLiteral("colour primaries %1 have no conversion to sRGB here").arg(int(nclx->color_primaries)));
|
||||
switch (nclx->transfer_characteristics) {
|
||||
case heif_transfer_characteristic_IEC_61966_2_1:
|
||||
case heif_transfer_characteristic_unspecified:
|
||||
return stated;
|
||||
case heif_transfer_characteristic_linear:
|
||||
stated.setTransferFunction(QColorSpace::TransferFunction::Linear);
|
||||
return stated;
|
||||
case heif_transfer_characteristic_ITU_R_BT_470_6_System_M:
|
||||
stated.setTransferFunction(QColorSpace::TransferFunction::Gamma, 2.2f);
|
||||
return stated;
|
||||
case heif_transfer_characteristic_ITU_R_BT_470_6_System_B_G:
|
||||
stated.setTransferFunction(QColorSpace::TransferFunction::Gamma, 2.8f);
|
||||
return stated;
|
||||
// xvYCC and BT.1361 are BT.709's curve between zero and one.
|
||||
case heif_transfer_characteristic_IEC_61966_2_4:
|
||||
case heif_transfer_characteristic_ITU_R_BT_1361:
|
||||
case heif_transfer_characteristic_ITU_R_BT_709_5:
|
||||
case heif_transfer_characteristic_ITU_R_BT_601_6:
|
||||
case heif_transfer_characteristic_ITU_R_BT_2020_2_10bit:
|
||||
case heif_transfer_characteristic_ITU_R_BT_2020_2_12bit:
|
||||
// Qt 6.4's constructor from points and a table is broken.
|
||||
stated.setTransferFunction(videoCurve());
|
||||
return stated;
|
||||
default:
|
||||
refuse(ImageImportError::Kind::unreadable, path,
|
||||
QStringLiteral("transfer characteristics %1 have no conversion to sRGB here").arg(int(nclx->transfer_characteristics)));
|
||||
}
|
||||
}
|
||||
|
||||
// libheif turns and crops the picture as the file says.
|
||||
QImage decodeHEIC(const QString &path, qint64 remainingPixels)
|
||||
{
|
||||
// libheif's documented frame: deinit frees what init loaded.
|
||||
struct Library {
|
||||
Library() { heif_init(nullptr); }
|
||||
~Library() { heif_deinit(); }
|
||||
} library;
|
||||
const std::unique_ptr<heif_context, void (*)(heif_context *)> context(heif_context_alloc(), heif_context_free);
|
||||
heif_error error = heif_context_read_from_file(context.get(), QFile::encodeName(path).constData(), nullptr);
|
||||
if (error.code != heif_error_Ok)
|
||||
refuse(ImageImportError::Kind::unreadable, path, QString::fromUtf8(error.message));
|
||||
heif_image_handle *primary = nullptr;
|
||||
error = heif_context_get_primary_image_handle(context.get(), &primary);
|
||||
if (error.code != heif_error_Ok)
|
||||
refuse(ImageImportError::Kind::unreadable, path, QString::fromUtf8(error.message));
|
||||
const std::unique_ptr<heif_image_handle, void (*)(const heif_image_handle *)> handle(primary, heif_image_handle_release);
|
||||
checkSize(QSize(heif_image_handle_get_width(primary), heif_image_handle_get_height(primary)), remainingPixels, path);
|
||||
const QColorSpace stated = statedColorSpace(primary, path);
|
||||
heif_image *decoded = nullptr;
|
||||
error = heif_decode_image(primary, &decoded, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, nullptr);
|
||||
if (error.code != heif_error_Ok)
|
||||
refuse(ImageImportError::Kind::unreadable, path, QString::fromUtf8(error.message));
|
||||
const std::unique_ptr<heif_image, void (*)(const heif_image *)> picture(decoded, heif_image_release);
|
||||
int stride = 0;
|
||||
const uint8_t *pixels = heif_image_get_plane_readonly(decoded, heif_channel_interleaved, &stride);
|
||||
if (!pixels)
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("libheif gave no pixels"));
|
||||
const bool premultiplied = heif_image_handle_is_premultiplied_alpha(primary);
|
||||
// The copy outlives libheif's buffer.
|
||||
QImage image = QImage(pixels, heif_image_get_width(decoded, heif_channel_interleaved), heif_image_get_height(decoded, heif_channel_interleaved),
|
||||
stride, premultiplied ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGBA8888).copy();
|
||||
image.setColorSpace(stated);
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
ImportedImage::ImportedImage(QImage image, QImage thumbnail, QString name)
|
||||
: thumbnail(std::move(thumbnail)), name(std::move(name)), m_image(std::move(image))
|
||||
{
|
||||
}
|
||||
|
||||
ImportedImage::ImportedImage(std::shared_ptr<const RasterSnapshot> raster, QImage thumbnail, QString name)
|
||||
: thumbnail(std::move(thumbnail)), name(std::move(name)), raster(std::move(raster))
|
||||
{
|
||||
}
|
||||
|
||||
QSize ImportedImage::size() const
|
||||
{
|
||||
return raster ? QSize(raster->width, raster->height) : m_image.size();
|
||||
}
|
||||
|
||||
QImage ImportedImage::image() const
|
||||
{
|
||||
return raster ? raster->makeImage() : m_image;
|
||||
}
|
||||
|
||||
ImageIdentity ImportedImage::identity() const
|
||||
{
|
||||
return raster ? ImageIdentity{raster.get(), 0} : ImageIdentity{nullptr, m_image.cacheKey()};
|
||||
}
|
||||
|
||||
qint64 ImportedImage::byteCount() const
|
||||
{
|
||||
if (!raster)
|
||||
return m_image.sizeInBytes();
|
||||
return qint64(raster->width) * raster->height * (raster->isMask ? 1 : 4);
|
||||
}
|
||||
|
||||
ImageImportError::ImageImportError(Kind kind) : std::runtime_error(description(kind).toStdString()), kind(kind) {}
|
||||
|
||||
void ImageImporter::liftAllocationLimit()
|
||||
{
|
||||
static const bool lifted = (QImageReader::setAllocationLimit(0), true);
|
||||
Q_UNUSED(lifted)
|
||||
}
|
||||
|
||||
ImportedImage ImageImporter::decode(const QString &path, qint64 remainingPixels)
|
||||
{
|
||||
liftAllocationLimit();
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
refuse(ImageImportError::Kind::unreadable, path, file.errorString());
|
||||
// The type comes from the bytes: Qt checks them too.
|
||||
const QByteArray head = file.peek(64);
|
||||
QImageReader reader(&file);
|
||||
const QByteArray format = reader.format();
|
||||
const bool heic = isHEIC(head);
|
||||
// Other ISO media is unwanted: AVIF, plain HEIF, video.
|
||||
if (!heic && head.mid(4, 4) == "ftyp")
|
||||
refuse(ImageImportError::Kind::unsupported, path, QStringLiteral("ISO media that is not HEIC"));
|
||||
if (!heic && format.isEmpty())
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("no image format is recognised"));
|
||||
if (!heic && format != "jpeg" && format != "png" && format != "tiff")
|
||||
refuse(ImageImportError::Kind::unsupported, path, QString::fromLatin1(format));
|
||||
// Qt's own turning drops the colour space: turn afterwards.
|
||||
QImageIOHandler::Transformations orientation = QImageIOHandler::TransformationNone;
|
||||
QImage image;
|
||||
if (heic) {
|
||||
image = decodeHEIC(path, remainingPixels);
|
||||
} else {
|
||||
checkSize(reader.size(), remainingPixels, path);
|
||||
orientation = reader.transformation();
|
||||
if (!reader.read(&image))
|
||||
refuse(ImageImportError::Kind::unreadable, path, reader.errorString());
|
||||
}
|
||||
image.convertToColorSpace(QColorSpace::SRgb);
|
||||
image = std::move(image).convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
// Qt 6.9 renamed mirrored as flipped; the floor stays 6.4.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
|
||||
Qt::Orientations flips;
|
||||
flips.setFlag(Qt::Horizontal, orientation.testFlag(QImageIOHandler::TransformationMirror));
|
||||
flips.setFlag(Qt::Vertical, orientation.testFlag(QImageIOHandler::TransformationFlip));
|
||||
image = std::move(image).flipped(flips);
|
||||
#else
|
||||
image = std::move(image).mirrored(orientation.testFlag(QImageIOHandler::TransformationMirror),
|
||||
orientation.testFlag(QImageIOHandler::TransformationFlip));
|
||||
#endif
|
||||
if (orientation.testFlag(QImageIOHandler::TransformationRotate90))
|
||||
image = image.transformed(QTransform().rotate(90));
|
||||
if (image.isNull())
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("out of memory"));
|
||||
image.setColorSpace(QColorSpace::SRgb);
|
||||
// Swift: scale = min(1, 96 / longest), extent rounded outward.
|
||||
const qint64 longest = std::max(image.width(), image.height());
|
||||
const QSize small(int((image.width() * 96 + longest - 1) / longest), int((image.height() * 96 + longest - 1) / longest));
|
||||
const QImage thumbnail = longest <= 96 ? image : image.scaled(small, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
if (thumbnail.isNull())
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("out of memory"));
|
||||
const QString name = QFileInfo(path).completeBaseName();
|
||||
qCInfo(lcIO).noquote() << "imported" << path << image.width() << "x" << image.height();
|
||||
return ImportedImage(image, thumbnail, name.isEmpty() ? QFileInfo(path).fileName() : name);
|
||||
}
|
||||
Reference in new issue
Block a user