mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-05-27 08:20:21 -04:00
PSD: improved option support
Added support for the following options: - `ImageTransformation`: uses EXIF data (same behaviour of Photoshop and GIMP) - `Description`: uses EXIF data - `ImageFormat` Closes #17
This commit is contained in:
parent
873ec1bb5f
commit
e83458a5d8
BIN
autotests/read/psd/orientation1.psd
Normal file
BIN
autotests/read/psd/orientation1.psd
Normal file
Binary file not shown.
11
autotests/read/psd/orientation1.psd.json
Normal file
11
autotests/read/psd/orientation1.psd.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"fileName" : "orientation_all.png",
|
||||
"metadata" : [
|
||||
{
|
||||
"key" : "Software" ,
|
||||
"value" : "LIFE Pro 2.18.5 (Windows)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
BIN
autotests/read/psd/orientation2.psd
Normal file
BIN
autotests/read/psd/orientation2.psd
Normal file
Binary file not shown.
11
autotests/read/psd/orientation2.psd.json
Normal file
11
autotests/read/psd/orientation2.psd.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"fileName" : "orientation_all.png",
|
||||
"metadata" : [
|
||||
{
|
||||
"key" : "Software" ,
|
||||
"value" : "LIFE Pro 2.18.5 (Windows)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
BIN
autotests/read/psd/orientation3.psd
Normal file
BIN
autotests/read/psd/orientation3.psd
Normal file
Binary file not shown.
11
autotests/read/psd/orientation3.psd.json
Normal file
11
autotests/read/psd/orientation3.psd.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"fileName" : "orientation_all.png",
|
||||
"metadata" : [
|
||||
{
|
||||
"key" : "Software" ,
|
||||
"value" : "LIFE Pro 2.18.5 (Windows)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
BIN
autotests/read/psd/orientation4.psd
Normal file
BIN
autotests/read/psd/orientation4.psd
Normal file
Binary file not shown.
11
autotests/read/psd/orientation4.psd.json
Normal file
11
autotests/read/psd/orientation4.psd.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"fileName" : "orientation_all.png",
|
||||
"metadata" : [
|
||||
{
|
||||
"key" : "Software" ,
|
||||
"value" : "LIFE Pro 2.18.5 (Windows)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
BIN
autotests/read/psd/orientation5.psd
Normal file
BIN
autotests/read/psd/orientation5.psd
Normal file
Binary file not shown.
11
autotests/read/psd/orientation5.psd.json
Normal file
11
autotests/read/psd/orientation5.psd.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"fileName" : "orientation_all.png",
|
||||
"metadata" : [
|
||||
{
|
||||
"key" : "Software" ,
|
||||
"value" : "LIFE Pro 2.18.5 (Windows)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
BIN
autotests/read/psd/orientation6.psd
Normal file
BIN
autotests/read/psd/orientation6.psd
Normal file
Binary file not shown.
11
autotests/read/psd/orientation6.psd.json
Normal file
11
autotests/read/psd/orientation6.psd.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"fileName" : "orientation_all.png",
|
||||
"metadata" : [
|
||||
{
|
||||
"key" : "Software" ,
|
||||
"value" : "LIFE Pro 2.18.5 (Windows)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
BIN
autotests/read/psd/orientation7.psd
Normal file
BIN
autotests/read/psd/orientation7.psd
Normal file
Binary file not shown.
11
autotests/read/psd/orientation7.psd.json
Normal file
11
autotests/read/psd/orientation7.psd.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"fileName" : "orientation_all.png",
|
||||
"metadata" : [
|
||||
{
|
||||
"key" : "Software" ,
|
||||
"value" : "LIFE Pro 2.18.5 (Windows)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
BIN
autotests/read/psd/orientation8.psd
Normal file
BIN
autotests/read/psd/orientation8.psd
Normal file
Binary file not shown.
11
autotests/read/psd/orientation8.psd.json
Normal file
11
autotests/read/psd/orientation8.psd.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"fileName" : "orientation_all.png",
|
||||
"metadata" : [
|
||||
{
|
||||
"key" : "Software" ,
|
||||
"value" : "LIFE Pro 2.18.5 (Windows)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
BIN
autotests/read/psd/orientation_all.png
Normal file
BIN
autotests/read/psd/orientation_all.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
@ -473,9 +473,9 @@ PSDColorModeDataSection readColorModeDataSection(QDataStream &s, bool *ok = null
|
||||
if (cms.duotone.data.size() != size)
|
||||
*ok = false;
|
||||
} else { // read the palette (768 bytes)
|
||||
auto&& palette = cms.palette;
|
||||
auto &&palette = cms.palette;
|
||||
QList<quint8> vect(size);
|
||||
for (auto&& v : vect)
|
||||
for (auto &&v : vect)
|
||||
s >> v;
|
||||
for (qsizetype i = 0, n = vect.size()/3; i < n; ++i)
|
||||
palette.append(qRgb(vect.at(i), vect.at(n+i), vect.at(n+n+i)));
|
||||
@ -491,7 +491,7 @@ PSDColorModeDataSection readColorModeDataSection(QDataStream &s, bool *ok = null
|
||||
* \param irs The image resource section.
|
||||
* \return True on success, otherwise false.
|
||||
*/
|
||||
static bool setColorSpace(QImage& img, const PSDImageResourceSection& irs)
|
||||
static bool setColorSpace(QImage &img, const PSDImageResourceSection &irs)
|
||||
{
|
||||
if (!irs.contains(IRI_ICCPROFILE) || img.isNull())
|
||||
return false;
|
||||
@ -510,7 +510,7 @@ static bool setColorSpace(QImage& img, const PSDImageResourceSection& irs)
|
||||
* \param irs The image resource section.
|
||||
* \return True on success, otherwise false.
|
||||
*/
|
||||
static bool setXmpData(QImage& img, const PSDImageResourceSection& irs)
|
||||
static bool setXmpData(QImage &img, const PSDImageResourceSection &irs)
|
||||
{
|
||||
if (!irs.contains(IRI_XMPMETADATA))
|
||||
return false;
|
||||
@ -529,15 +529,11 @@ static bool setXmpData(QImage& img, const PSDImageResourceSection& irs)
|
||||
* \brief setExifData
|
||||
* Adds EXIF metadata to QImage.
|
||||
* \param img The image.
|
||||
* \param irs The image resource section.
|
||||
* \param exif The decoded EXIF data.
|
||||
* \return True on success, otherwise false.
|
||||
*/
|
||||
static bool setExifData(QImage& img, const PSDImageResourceSection& irs)
|
||||
static bool setExifData(QImage &img, const MicroExif &exif)
|
||||
{
|
||||
if (!irs.contains(IRI_EXIFDATA1))
|
||||
return false;
|
||||
auto irb = irs.value(IRI_EXIFDATA1);
|
||||
auto exif = MicroExif::fromByteArray(irb.data);
|
||||
if (exif.isEmpty())
|
||||
return false;
|
||||
exif.toImageMetadata(img);
|
||||
@ -545,12 +541,12 @@ static bool setExifData(QImage& img, const PSDImageResourceSection& irs)
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief hasMergedData
|
||||
* \brief HasMergedData
|
||||
* Checks if merged image data are available.
|
||||
* \param irs The image resource section.
|
||||
* \return True on success or if the block does not exist, otherwise false.
|
||||
*/
|
||||
static bool hasMergedData(const PSDImageResourceSection& irs)
|
||||
static bool HasMergedData(const PSDImageResourceSection &irs)
|
||||
{
|
||||
if (!irs.contains(IRI_VERSIONINFO))
|
||||
return true;
|
||||
@ -567,7 +563,7 @@ static bool hasMergedData(const PSDImageResourceSection& irs)
|
||||
* \param irs The image resource section.
|
||||
* \return True on success, otherwise false.
|
||||
*/
|
||||
static bool setResolution(QImage& img, const PSDImageResourceSection& irs)
|
||||
static bool setResolution(QImage &img, const PSDImageResourceSection &irs)
|
||||
{
|
||||
if (!irs.contains(IRI_RESOLUTIONINFO))
|
||||
return false;
|
||||
@ -601,7 +597,7 @@ static bool setResolution(QImage& img, const PSDImageResourceSection& irs)
|
||||
* \param irs The image resource section.
|
||||
* \return True on success, otherwise false.
|
||||
*/
|
||||
static bool setTransparencyIndex(QImage& img, const PSDImageResourceSection& irs)
|
||||
static bool setTransparencyIndex(QImage &img, const PSDImageResourceSection &irs)
|
||||
{
|
||||
if (!irs.contains(IRI_TRANSPARENCYINDEX))
|
||||
return false;
|
||||
@ -613,7 +609,7 @@ static bool setTransparencyIndex(QImage& img, const PSDImageResourceSection& irs
|
||||
|
||||
auto palette = img.colorTable();
|
||||
if (idx < palette.size()) {
|
||||
auto&& v = palette[idx];
|
||||
auto &&v = palette[idx];
|
||||
v = QRgb(v & ~0xFF000000);
|
||||
img.setColorTable(palette);
|
||||
return true;
|
||||
@ -813,7 +809,7 @@ static QImage::Format imageFormat(const PSDHeader &header, bool alpha)
|
||||
* \param format The Qt image format.
|
||||
* \return The number of channels of the image format.
|
||||
*/
|
||||
static qint32 imageChannels(const QImage::Format& format)
|
||||
static qint32 imageChannels(const QImage::Format &format)
|
||||
{
|
||||
qint32 c = 4;
|
||||
switch(format) {
|
||||
@ -1086,7 +1082,7 @@ inline void labToRgb(uchar *target, qint32 targetChannels, const char *source, q
|
||||
}
|
||||
}
|
||||
|
||||
bool readChannel(QByteArray& target, QDataStream &stream, quint32 compressedSize, quint16 compression)
|
||||
bool readChannel(QByteArray &target, QDataStream &stream, quint32 compressedSize, quint16 compression)
|
||||
{
|
||||
if (compression) {
|
||||
if (compressedSize > kMaxQVectorSize) {
|
||||
@ -1107,39 +1103,161 @@ bool readChannel(QByteArray& target, QDataStream &stream, quint32 compressedSize
|
||||
return stream.status() == QDataStream::Ok;
|
||||
}
|
||||
|
||||
// Load the PSD image.
|
||||
static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
} // Private
|
||||
|
||||
class PSDHandlerPrivate
|
||||
{
|
||||
// Checking for PSB
|
||||
auto isPsb = header.version == 2;
|
||||
bool ok = false;
|
||||
|
||||
// Color Mode Data section
|
||||
auto cmds = readColorModeDataSection(stream, &ok);
|
||||
if (!ok) {
|
||||
qDebug() << "Error while skipping Color Mode Data section";
|
||||
return false;
|
||||
public:
|
||||
PSDHandlerPrivate()
|
||||
{
|
||||
}
|
||||
~PSDHandlerPrivate()
|
||||
{
|
||||
}
|
||||
|
||||
// Image Resources Section
|
||||
auto irs = readImageResourceSection(stream, &ok);
|
||||
if (!ok) {
|
||||
qDebug() << "Error while reading Image Resources Section";
|
||||
return false;
|
||||
}
|
||||
// Checking for merged image (Photoshop compatibility data)
|
||||
if (!hasMergedData(irs)) {
|
||||
qDebug() << "No merged data found";
|
||||
return false;
|
||||
bool isPsb() const
|
||||
{
|
||||
return m_header.version == 2;
|
||||
}
|
||||
|
||||
// Layer and Mask section
|
||||
auto lms = readLayerAndMaskSection(stream, isPsb, &ok);
|
||||
if (!ok) {
|
||||
qDebug() << "Error while skipping Layer and Mask section";
|
||||
return false;
|
||||
bool isValid() const
|
||||
{
|
||||
return IsValid(m_header);
|
||||
}
|
||||
|
||||
bool isSupported() const
|
||||
{
|
||||
return IsSupported(m_header);
|
||||
}
|
||||
|
||||
bool hasAlpha() const
|
||||
{
|
||||
// Try to identify the nature of spots: note that this is just one of many ways to identify the presence
|
||||
// of alpha channels: should work in most cases where colorspaces != RGB/Gray
|
||||
auto alpha = m_header.color_mode == CM_RGB;
|
||||
if (!m_lms.isNull())
|
||||
alpha = m_lms.hasAlpha();
|
||||
return alpha;
|
||||
}
|
||||
|
||||
bool hasMergedData() const
|
||||
{
|
||||
return HasMergedData(m_irs);
|
||||
}
|
||||
|
||||
QSize size() const
|
||||
{
|
||||
if (isValid())
|
||||
return QSize(m_header.width, m_header.height);
|
||||
return {};
|
||||
}
|
||||
|
||||
QImage::Format format() const
|
||||
{
|
||||
return imageFormat(m_header, hasAlpha());
|
||||
}
|
||||
|
||||
QImageIOHandler::Transformations transformation() const
|
||||
{
|
||||
return m_exif.transformation();
|
||||
}
|
||||
|
||||
bool readInfo(QDataStream &stream)
|
||||
{
|
||||
// Checking for PSB
|
||||
auto ok = false;
|
||||
|
||||
// Header
|
||||
stream >> m_header;
|
||||
|
||||
// Check image file format.
|
||||
if (stream.atEnd() || !IsValid(m_header)) {
|
||||
// qDebug() << "This PSD file is not valid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's a supported format.
|
||||
if (!IsSupported(m_header)) {
|
||||
// qDebug() << "This PSD file is not supported.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Color Mode Data section
|
||||
m_cmds = readColorModeDataSection(stream, &ok);
|
||||
if (!ok) {
|
||||
qDebug() << "Error while skipping Color Mode Data section";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Image Resources Section
|
||||
m_irs = readImageResourceSection(stream, &ok);
|
||||
if (!ok) {
|
||||
qDebug() << "Error while reading Image Resources Section";
|
||||
return false;
|
||||
}
|
||||
// Checking for merged image (Photoshop compatibility data)
|
||||
if (!hasMergedData()) {
|
||||
qDebug() << "No merged data found";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Layer and Mask section
|
||||
m_lms = readLayerAndMaskSection(stream, isPsb(), &ok);
|
||||
if (!ok) {
|
||||
qDebug() << "Error while skipping Layer and Mask section";
|
||||
return false;
|
||||
}
|
||||
|
||||
// storing decoded EXIF
|
||||
if (m_irs.contains(IRI_EXIFDATA1)) {
|
||||
m_exif = MicroExif::fromByteArray(m_irs.value(IRI_EXIFDATA1).data);
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
PSDHeader m_header;
|
||||
PSDColorModeDataSection m_cmds;
|
||||
PSDImageResourceSection m_irs;
|
||||
PSDLayerAndMaskSection m_lms;
|
||||
|
||||
// cache to avoid decoding exif multiple times
|
||||
MicroExif m_exif;
|
||||
};
|
||||
|
||||
PSDHandler::PSDHandler()
|
||||
: QImageIOHandler()
|
||||
, d(new PSDHandlerPrivate)
|
||||
{
|
||||
}
|
||||
|
||||
bool PSDHandler::canRead() const
|
||||
{
|
||||
if (canRead(device())) {
|
||||
setFormat("psd");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PSDHandler::read(QImage *image)
|
||||
{
|
||||
QDataStream stream(device());
|
||||
stream.setByteOrder(QDataStream::BigEndian);
|
||||
|
||||
if (!d->isValid()) {
|
||||
if (!d->readInfo(stream))
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &&header = d->m_header;
|
||||
auto &&cmds = d->m_cmds;
|
||||
auto &&irs = d->m_irs;
|
||||
// auto &&lms = d->m_lms;
|
||||
auto isPsb = d->isPsb();
|
||||
auto alpha = d->hasAlpha();
|
||||
|
||||
QImage img;
|
||||
// Find out if the data is compressed.
|
||||
// Known values:
|
||||
// 0: no compression
|
||||
@ -1151,19 +1269,13 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to identify the nature of spots: note that this is just one of many ways to identify the presence
|
||||
// of alpha channels: should work in most cases where colorspaces != RGB/Gray
|
||||
auto alpha = header.color_mode == CM_RGB;
|
||||
if (!lms.isNull())
|
||||
alpha = lms.hasAlpha();
|
||||
|
||||
const QImage::Format format = imageFormat(header, alpha);
|
||||
const QImage::Format format = d->format();
|
||||
if (format == QImage::Format_Invalid) {
|
||||
qWarning() << "Unsupported image format. color_mode:" << header.color_mode << "depth:" << header.depth << "channel_count:" << header.channel_count;
|
||||
return false;
|
||||
}
|
||||
|
||||
img = imageAlloc(header.width, header.height, format);
|
||||
img = imageAlloc(d->size(), format);
|
||||
if (img.isNull()) {
|
||||
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width, header.height);
|
||||
return false;
|
||||
@ -1187,7 +1299,7 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
QList<quint32> strides(header.height * header.channel_count, raw_count);
|
||||
// Read the compressed stride sizes
|
||||
if (compression) {
|
||||
for (auto&& v : strides) {
|
||||
for (auto &&v : strides) {
|
||||
if (isPsb) {
|
||||
stream >> v;
|
||||
continue;
|
||||
@ -1242,7 +1354,7 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
qDebug() << "Error while seeking the stream of channel" << c << "line" << y;
|
||||
return false;
|
||||
}
|
||||
auto&& strideSize = strides.at(strideNumber);
|
||||
auto &&strideSize = strides.at(strideNumber);
|
||||
if (!readChannel(rawStride, stream, strideSize, compression)) {
|
||||
qDebug() << "Error while reading the stream of channel" << c << "line" << y;
|
||||
return false;
|
||||
@ -1379,7 +1491,7 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
}
|
||||
|
||||
// EXIF data
|
||||
if (!setExifData(img, irs)) {
|
||||
if (!setExifData(img, d->m_exif)) {
|
||||
// qDebug() << "No EXIF data found!";
|
||||
}
|
||||
|
||||
@ -1390,60 +1502,6 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
||||
img.setText(QStringLiteral("PSDDuotoneOptions"), QString::fromUtf8(cmds.duotone.data.toHex()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // Private
|
||||
|
||||
class PSDHandlerPrivate
|
||||
{
|
||||
public:
|
||||
PSDHandlerPrivate() {}
|
||||
~PSDHandlerPrivate() {}
|
||||
PSDHeader m_header;
|
||||
};
|
||||
|
||||
PSDHandler::PSDHandler()
|
||||
: QImageIOHandler()
|
||||
, d(new PSDHandlerPrivate)
|
||||
{
|
||||
}
|
||||
|
||||
bool PSDHandler::canRead() const
|
||||
{
|
||||
if (canRead(device())) {
|
||||
setFormat("psd");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PSDHandler::read(QImage *image)
|
||||
{
|
||||
QDataStream s(device());
|
||||
s.setByteOrder(QDataStream::BigEndian);
|
||||
|
||||
auto&& header = d->m_header;
|
||||
s >> header;
|
||||
|
||||
// Check image file format.
|
||||
if (s.atEnd() || !IsValid(header)) {
|
||||
// qDebug() << "This PSD file is not valid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's a supported format.
|
||||
if (!IsSupported(header)) {
|
||||
// qDebug() << "This PSD file is not supported.";
|
||||
return false;
|
||||
}
|
||||
|
||||
QImage img;
|
||||
if (!LoadPSD(s, header, img)) {
|
||||
// qDebug() << "Error loading PSD file.";
|
||||
return false;
|
||||
}
|
||||
|
||||
*image = img;
|
||||
return true;
|
||||
}
|
||||
@ -1452,6 +1510,12 @@ bool PSDHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
if (option == QImageIOHandler::Size)
|
||||
return true;
|
||||
if (option == QImageIOHandler::ImageFormat)
|
||||
return true;
|
||||
if (option == QImageIOHandler::ImageTransformation)
|
||||
return true;
|
||||
if (option == QImageIOHandler::Description)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1459,18 +1523,37 @@ QVariant PSDHandler::option(ImageOption option) const
|
||||
{
|
||||
QVariant v;
|
||||
|
||||
if (option == QImageIOHandler::Size) {
|
||||
auto&& header = d->m_header;
|
||||
if (IsValid(header)) {
|
||||
v = QVariant::fromValue(QSize(header.width, header.height));
|
||||
} else if (auto dev = device()) {
|
||||
auto ba = dev->peek(sizeof(PSDHeader));
|
||||
QDataStream s(ba);
|
||||
if (auto dev = device()) {
|
||||
if (!d->isValid()) {
|
||||
QDataStream s(dev);
|
||||
s.setByteOrder(QDataStream::BigEndian);
|
||||
d->readInfo(s);
|
||||
}
|
||||
}
|
||||
|
||||
s >> header;
|
||||
if (s.status() == QDataStream::Ok && IsValid(header))
|
||||
v = QVariant::fromValue(QSize(header.width, header.height));
|
||||
if (option == QImageIOHandler::Size) {
|
||||
if (d->isValid()) {
|
||||
v = QVariant::fromValue(d->size());
|
||||
}
|
||||
}
|
||||
|
||||
if (option == QImageIOHandler::ImageFormat) {
|
||||
if (d->isValid()) {
|
||||
v = QVariant::fromValue(d->format());
|
||||
}
|
||||
}
|
||||
|
||||
if (option == QImageIOHandler::ImageTransformation) {
|
||||
if (d->isValid()) {
|
||||
v = QVariant::fromValue(int(d->transformation()));
|
||||
}
|
||||
}
|
||||
|
||||
if (option == QImageIOHandler::Description) {
|
||||
if (d->isValid()) {
|
||||
auto descr = d->m_exif.description();
|
||||
if (!descr.isEmpty())
|
||||
v = QVariant::fromValue(descr);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user