mirror of
https://github.com/zebrajr/opencv.git
synced 2025-12-06 12:19:50 +01:00
Merge pull request #27127 from sturkmen72:apng_has_hidden_frame
Changes about when APNG has a hidden frame #27127 closes : #27074 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
parent
3259863924
commit
850b686f8a
|
|
@ -277,6 +277,8 @@ struct CV_EXPORTS_W_SIMPLE Animation
|
|||
CV_PROP_RW std::vector<int> durations;
|
||||
//! Vector of frames, where each Mat represents a single frame.
|
||||
CV_PROP_RW std::vector<Mat> frames;
|
||||
//! image that can be used for the format in addition to the animation or if animation is not supported in the reader (like in PNG).
|
||||
CV_PROP_RW Mat still_image;
|
||||
|
||||
/** @brief Constructs an Animation object with optional loop count and background color.
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ public:
|
|||
*/
|
||||
size_t getFrameCount() const { return m_frame_count; }
|
||||
|
||||
/**
|
||||
* @brief Set the internal m_frame_count variable to 1.
|
||||
*/
|
||||
void resetFrameCount() { m_frame_count = 1; }
|
||||
|
||||
/**
|
||||
* @brief Get the type of the image (e.g., color format, depth).
|
||||
* @return The type of the image.
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ bool APNGFrame::setMat(const cv::Mat& src, unsigned delayNum, unsigned delayDen)
|
|||
|
||||
if (!src.empty())
|
||||
{
|
||||
png_uint_32 rowbytes = src.depth() == CV_16U ? src.cols * src.channels() * 2 : src.cols * src.channels();
|
||||
png_uint_32 rowbytes = src.cols * (uint32_t)src.elemSize();
|
||||
_width = src.cols;
|
||||
_height = src.rows;
|
||||
_colorType = src.channels() == 1 ? PNG_COLOR_TYPE_GRAY : src.channels() == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA;
|
||||
|
|
@ -416,14 +416,17 @@ bool PngDecoder::readData( Mat& img )
|
|||
|
||||
if (m_frame_no == 0)
|
||||
{
|
||||
if (m_mat_raw.empty())
|
||||
{
|
||||
if (m_f)
|
||||
fseek(m_f, -8, SEEK_CUR);
|
||||
else
|
||||
m_buf_pos -= 8;
|
||||
}
|
||||
m_mat_raw = Mat(img.rows, img.cols, m_type);
|
||||
m_mat_next = Mat(img.rows, img.cols, m_type);
|
||||
frameRaw.setMat(m_mat_raw);
|
||||
frameNext.setMat(m_mat_next);
|
||||
if (m_f)
|
||||
fseek(m_f, -8, SEEK_CUR);
|
||||
else
|
||||
m_buf_pos -= 8;
|
||||
}
|
||||
else
|
||||
m_mat_next.copyTo(mat_cur);
|
||||
|
|
@ -433,9 +436,6 @@ bool PngDecoder::readData( Mat& img )
|
|||
if (!processing_start((void*)&frameRaw, mat_cur))
|
||||
return false;
|
||||
|
||||
if(setjmp(png_jmpbuf(m_png_ptr)))
|
||||
return false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
id = read_chunk(chunk);
|
||||
|
|
@ -446,53 +446,53 @@ bool PngDecoder::readData( Mat& img )
|
|||
{
|
||||
if (!m_is_fcTL_loaded)
|
||||
{
|
||||
m_is_fcTL_loaded = true;
|
||||
w0 = m_width;
|
||||
h0 = m_height;
|
||||
}
|
||||
|
||||
if (processing_finish())
|
||||
{
|
||||
if (dop == 2)
|
||||
memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize);
|
||||
|
||||
compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur);
|
||||
if (!delay_den)
|
||||
delay_den = 100;
|
||||
m_animation.durations.push_back(cvRound(1000.*delay_num/delay_den));
|
||||
|
||||
if (mat_cur.channels() == img.channels())
|
||||
{
|
||||
if (mat_cur.depth() == CV_16U && img.depth() == CV_8U)
|
||||
mat_cur.convertTo(img, CV_8U, 1. / 255);
|
||||
else
|
||||
mat_cur.copyTo(img);
|
||||
}
|
||||
else
|
||||
{
|
||||
Mat mat_cur_scaled;
|
||||
if (mat_cur.depth() == CV_16U && img.depth() == CV_8U)
|
||||
mat_cur.convertTo(mat_cur_scaled, CV_8U, 1. / 255);
|
||||
else
|
||||
mat_cur_scaled = mat_cur;
|
||||
|
||||
if (img.channels() == 1)
|
||||
cvtColor(mat_cur_scaled, img, COLOR_BGRA2GRAY);
|
||||
else if (img.channels() == 3)
|
||||
cvtColor(mat_cur_scaled, img, COLOR_BGRA2BGR);
|
||||
}
|
||||
|
||||
if (dop != 2)
|
||||
{
|
||||
memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize);
|
||||
if (dop == 1)
|
||||
for (j = 0; j < h0; j++)
|
||||
memset(frameNext.getRows()[y0 + j] + x0 * img.channels(), 0, w0 * img.channels());
|
||||
}
|
||||
m_mat_raw.copyTo(m_animation.still_image);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
if (processing_finish())
|
||||
{
|
||||
if (dop == 2)
|
||||
memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize);
|
||||
|
||||
compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur);
|
||||
if (!delay_den)
|
||||
delay_den = 100;
|
||||
m_animation.durations.push_back(cvRound(1000. * delay_num / delay_den));
|
||||
|
||||
if (mat_cur.channels() == img.channels())
|
||||
{
|
||||
if (mat_cur.depth() == CV_16U && img.depth() == CV_8U)
|
||||
mat_cur.convertTo(img, CV_8U, 1. / 255);
|
||||
else
|
||||
mat_cur.copyTo(img);
|
||||
}
|
||||
else
|
||||
{
|
||||
Mat mat_cur_scaled;
|
||||
if (mat_cur.depth() == CV_16U && img.depth() == CV_8U)
|
||||
mat_cur.convertTo(mat_cur_scaled, CV_8U, 1. / 255);
|
||||
else
|
||||
mat_cur_scaled = mat_cur;
|
||||
|
||||
if (img.channels() == 1)
|
||||
cvtColor(mat_cur_scaled, img, COLOR_BGRA2GRAY);
|
||||
else if (img.channels() == 3)
|
||||
cvtColor(mat_cur_scaled, img, COLOR_BGRA2BGR);
|
||||
}
|
||||
|
||||
if (dop != 2)
|
||||
{
|
||||
memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize);
|
||||
if (dop == 1)
|
||||
for (j = 0; j < h0; j++)
|
||||
memset(frameNext.getRows()[y0 + j] + x0 * img.channels(), 0, w0 * img.channels());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
w0 = png_get_uint_32(&chunk.p[12]);
|
||||
|
|
@ -515,7 +515,16 @@ bool PngDecoder::readData( Mat& img )
|
|||
}
|
||||
|
||||
memcpy(&m_chunkIHDR.p[8], &chunk.p[12], 8);
|
||||
return true;
|
||||
|
||||
if (m_is_fcTL_loaded)
|
||||
return true;
|
||||
else
|
||||
{
|
||||
m_is_fcTL_loaded = true;
|
||||
ClearPngPtr();
|
||||
if (!processing_start((void*)&frameRaw, mat_cur))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (id == id_IDAT)
|
||||
{
|
||||
|
|
@ -650,8 +659,8 @@ void PngDecoder::compose_frame(std::vector<png_bytep>& rows_dst, const std::vect
|
|||
const size_t elem_size = img.elemSize();
|
||||
if (_bop == 0) {
|
||||
// Overwrite mode: copy source row directly to destination
|
||||
for(uint32_t j = 0; j < h; ++j) {
|
||||
std::memcpy(rows_dst[j + y] + x * elem_size,rows_src[j], w * elem_size);
|
||||
for (uint32_t j = 0; j < h; ++j) {
|
||||
std::memcpy(rows_dst[j + y] + x * elem_size, rows_src[j], w * elem_size);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -665,23 +674,24 @@ void PngDecoder::compose_frame(std::vector<png_bytep>& rows_dst, const std::vect
|
|||
|
||||
// Blending mode
|
||||
for (unsigned int i = 0; i < w; i++, sp += channels, dp += channels) {
|
||||
if (channels < 4 || sp[3] == 65535) { // Fully opaque in 16-bit (max value)
|
||||
uint16_t alpha = sp[3];
|
||||
|
||||
if (channels < 4 || alpha == 65535 || dp[3] == 0) {
|
||||
// Fully opaque OR destination fully transparent: direct copy
|
||||
memcpy(dp, sp, elem_size);
|
||||
continue;
|
||||
}
|
||||
else if (sp[3] != 0) { // Partially transparent
|
||||
if (dp[3] != 0) { // Both source and destination have alpha
|
||||
uint32_t u = sp[3] * 65535; // 16-bit max
|
||||
uint32_t v = (65535 - sp[3]) * dp[3];
|
||||
uint32_t al = u + v;
|
||||
dp[0] = static_cast<uint16_t>((sp[0] * u + dp[0] * v) / al); // Red
|
||||
dp[1] = static_cast<uint16_t>((sp[1] * u + dp[1] * v) / al); // Green
|
||||
dp[2] = static_cast<uint16_t>((sp[2] * u + dp[2] * v) / al); // Blue
|
||||
dp[3] = static_cast<uint16_t>(al / 65535); // Alpha
|
||||
}
|
||||
else {
|
||||
// If destination alpha is 0, copy source pixel
|
||||
memcpy(dp, sp, elem_size);
|
||||
}
|
||||
|
||||
if (alpha != 0) {
|
||||
// Alpha blending
|
||||
uint64_t u = static_cast<uint64_t>(alpha) * 65535;
|
||||
uint64_t v = static_cast<uint64_t>(65535 - alpha) * dp[3];
|
||||
uint64_t al = u + v;
|
||||
|
||||
dp[0] = static_cast<uint16_t>((sp[0] * u + dp[0] * v) / al); // Red
|
||||
dp[1] = static_cast<uint16_t>((sp[1] * u + dp[1] * v) / al); // Green
|
||||
dp[2] = static_cast<uint16_t>((sp[2] * u + dp[2] * v) / al); // Blue
|
||||
dp[3] = static_cast<uint16_t>(al / 65535); // Alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -694,25 +704,24 @@ void PngDecoder::compose_frame(std::vector<png_bytep>& rows_dst, const std::vect
|
|||
|
||||
// Blending mode
|
||||
for (unsigned int i = 0; i < w; i++, sp += channels, dp += channels) {
|
||||
if (channels < 4 || sp[3] == 255) {
|
||||
// Fully opaque: copy source pixel directly
|
||||
uint8_t alpha = sp[3];
|
||||
|
||||
if (channels < 4 || alpha == 255 || dp[3] == 0) {
|
||||
// Fully opaque OR destination fully transparent: direct copy
|
||||
memcpy(dp, sp, elem_size);
|
||||
continue;
|
||||
}
|
||||
else if (sp[3] != 0) {
|
||||
|
||||
if (alpha != 0) {
|
||||
// Alpha blending
|
||||
if (dp[3] != 0) {
|
||||
int u = sp[3] * 255;
|
||||
int v = (255 - sp[3]) * dp[3];
|
||||
int al = u + v;
|
||||
dp[0] = (sp[0] * u + dp[0] * v) / al; // Red
|
||||
dp[1] = (sp[1] * u + dp[1] * v) / al; // Green
|
||||
dp[2] = (sp[2] * u + dp[2] * v) / al; // Blue
|
||||
dp[3] = al / 255; // Alpha
|
||||
}
|
||||
else {
|
||||
// If destination alpha is 0, copy source pixel
|
||||
memcpy(dp, sp, elem_size);
|
||||
}
|
||||
uint32_t u = alpha * 255;
|
||||
uint32_t v = (255 - alpha) * dp[3];
|
||||
uint32_t al = u + v;
|
||||
|
||||
dp[0] = static_cast<uint8_t>((sp[0] * u + dp[0] * v) / al); // Red
|
||||
dp[1] = static_cast<uint8_t>((sp[1] * u + dp[1] * v) / al); // Green
|
||||
dp[2] = static_cast<uint8_t>((sp[2] * u + dp[2] * v) / al); // Blue
|
||||
dp[3] = static_cast<uint8_t>(al / 255); // Alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1483,7 +1492,7 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
|
|||
|
||||
if (m_isBilevel)
|
||||
CV_LOG_WARNING(NULL, "IMWRITE_PNG_BILEVEL parameter is not supported yet.");
|
||||
uint32_t first =0;
|
||||
|
||||
uint32_t loops= animation.loop_count;
|
||||
uint32_t coltype= animation.frames[0].channels() == 1 ? PNG_COLOR_TYPE_GRAY : animation.frames[0].channels() == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA;
|
||||
|
||||
|
|
@ -1568,7 +1577,7 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
|
|||
buf_IHDR[11] = 0;
|
||||
buf_IHDR[12] = 0;
|
||||
|
||||
png_save_uint_32(buf_acTL, num_frames - first);
|
||||
png_save_uint_32(buf_acTL, num_frames);
|
||||
png_save_uint_32(buf_acTL + 4, loops);
|
||||
|
||||
writeToStreamOrBuffer(header, 8, m_f);
|
||||
|
|
@ -1577,8 +1586,6 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
|
|||
|
||||
if (num_frames > 1)
|
||||
writeChunk(m_f, "acTL", buf_acTL, 8);
|
||||
else
|
||||
first = 0;
|
||||
|
||||
if (palsize > 0)
|
||||
writeChunk(m_f, "PLTE", (unsigned char*)(&palette), palsize * 3);
|
||||
|
|
@ -1634,19 +1641,31 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
|
|||
|
||||
for (j = 0; j < 6; j++)
|
||||
op[j].valid = 0;
|
||||
|
||||
if (!animation.still_image.empty() && num_frames > 1)
|
||||
{
|
||||
CV_Assert(animation.still_image.type() == animation.frames[0].type() && animation.still_image.size() == animation.frames[0].size());
|
||||
APNGFrame apngFrame;
|
||||
Mat tmp;
|
||||
if (animation.still_image.depth() == CV_16U)
|
||||
{
|
||||
animation.still_image.convertTo(tmp, CV_8U, 1.0 / 255);
|
||||
}
|
||||
else
|
||||
tmp = animation.still_image;
|
||||
|
||||
cvtColor(tmp, tmp, COLOR_BGRA2RGBA);
|
||||
apngFrame.setMat(tmp);
|
||||
|
||||
deflateRectOp(apngFrame.getPixels(), x0, y0, w0, h0, bpp, rowbytes, zbuf_size, 0);
|
||||
deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, 0);
|
||||
writeIDATs(m_f, 0, zbuf.data(), zsize, idat_size);
|
||||
}
|
||||
|
||||
deflateRectOp(frames[0].getPixels(), x0, y0, w0, h0, bpp, rowbytes, zbuf_size, 0);
|
||||
deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, 0);
|
||||
|
||||
if (first)
|
||||
{
|
||||
writeIDATs(m_f, 0, zbuf.data(), zsize, idat_size);
|
||||
for (j = 0; j < 6; j++)
|
||||
op[j].valid = 0;
|
||||
deflateRectOp(frames[1].getPixels(), x0, y0, w0, h0, bpp, rowbytes, zbuf_size, 0);
|
||||
deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, 0);
|
||||
}
|
||||
|
||||
for (i = first; i < num_frames - 1; i++)
|
||||
for (i = 0; i < num_frames - 1; i++)
|
||||
{
|
||||
uint32_t op_min;
|
||||
int op_best;
|
||||
|
|
@ -1673,7 +1692,7 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
|
|||
}
|
||||
|
||||
/* dispose = previous */
|
||||
if (i > first)
|
||||
if (i > 0)
|
||||
getRect(width, height, rest.data(), frames[i + 1].getPixels(), over3.data(), bpp, rowbytes, zbuf_size, has_tcolor, tcolor, 2);
|
||||
|
||||
op_min = op[0].size;
|
||||
|
|
@ -1699,9 +1718,9 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
|
|||
png_save_uint_16(buf_fcTL + 22, frames[i].getDelayDen());
|
||||
buf_fcTL[24] = dop;
|
||||
buf_fcTL[25] = bop;
|
||||
writeChunk(m_f, "fcTL", buf_fcTL, 26);
|
||||
|
||||
writeIDATs(m_f, i, zbuf.data(), zsize, idat_size);
|
||||
writeChunk(m_f, "fcTL", buf_fcTL, 26);
|
||||
writeIDATs(m_f, animation.still_image.empty() ? i : 1, zbuf.data(), zsize, idat_size);
|
||||
|
||||
/* process apng dispose - begin */
|
||||
if (dop != 2)
|
||||
|
|
@ -1728,7 +1747,7 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
|
|||
deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, op_best);
|
||||
}
|
||||
|
||||
if (num_frames > 1)
|
||||
if (num_frames > 1 /* don't write fcTL chunk if animation has only one frame */)
|
||||
{
|
||||
png_save_uint_32(buf_fcTL, next_seq_num++);
|
||||
png_save_uint_32(buf_fcTL + 4, w0);
|
||||
|
|
|
|||
|
|
@ -501,11 +501,12 @@ imread_( const String& filename, int flags, OutputArray mat )
|
|||
Mat real_mat = mat.getMat();
|
||||
const void * original_ptr = real_mat.data;
|
||||
bool success = false;
|
||||
decoder->resetFrameCount(); // this is needed for PngDecoder. it should be called before decoder->readData()
|
||||
try
|
||||
{
|
||||
if (decoder->readData(real_mat))
|
||||
{
|
||||
CV_CheckTrue((decoder->getFrameCount() > 1) || original_ptr == real_mat.data, "Internal imread issue");
|
||||
CV_CheckTrue(original_ptr == real_mat.data, "Internal imread issue");
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -800,6 +801,7 @@ imreadanimation_(const String& filename, int flags, int start, int count, Animat
|
|||
}
|
||||
animation.bgcolor = decoder->animation().bgcolor;
|
||||
animation.loop_count = decoder->animation().loop_count;
|
||||
animation.still_image = decoder->animation().still_image;
|
||||
|
||||
return success;
|
||||
}
|
||||
|
|
@ -910,6 +912,7 @@ static bool imdecodeanimation_(InputArray buf, int flags, int start, int count,
|
|||
}
|
||||
animation.bgcolor = decoder->animation().bgcolor;
|
||||
animation.loop_count = decoder->animation().loop_count;
|
||||
animation.still_image = decoder->animation().still_image;
|
||||
|
||||
return success;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -636,6 +636,51 @@ TEST(Imgcodecs_APNG, imencode_animation)
|
|||
}
|
||||
}
|
||||
|
||||
TEST(Imgcodecs_APNG, animation_has_hidden_frame)
|
||||
{
|
||||
// Set the path to the test image directory and filename for loading.
|
||||
const string root = cvtest::TS::ptr()->get_data_path();
|
||||
const string filename = root + "readwrite/033.png";
|
||||
Animation animation1, animation2, animation3;
|
||||
|
||||
imreadanimation(filename, animation1);
|
||||
|
||||
EXPECT_FALSE(animation1.still_image.empty());
|
||||
EXPECT_EQ((size_t)2, animation1.frames.size());
|
||||
|
||||
std::vector<unsigned char> buf;
|
||||
EXPECT_TRUE(imencodeanimation(".png", animation1, buf));
|
||||
EXPECT_TRUE(imdecodeanimation(buf, animation2));
|
||||
|
||||
EXPECT_FALSE(animation2.still_image.empty());
|
||||
EXPECT_EQ(animation1.frames.size(), animation2.frames.size());
|
||||
|
||||
animation1.frames.erase(animation1.frames.begin());
|
||||
animation1.durations.erase(animation1.durations.begin());
|
||||
EXPECT_TRUE(imencodeanimation(".png", animation1, buf));
|
||||
EXPECT_TRUE(imdecodeanimation(buf, animation3));
|
||||
|
||||
EXPECT_FALSE(animation1.still_image.empty());
|
||||
EXPECT_TRUE(animation3.still_image.empty());
|
||||
EXPECT_EQ((size_t)1, animation3.frames.size());
|
||||
}
|
||||
|
||||
TEST(Imgcodecs_APNG, animation_imread_preview)
|
||||
{
|
||||
// Set the path to the test image directory and filename for loading.
|
||||
const string root = cvtest::TS::ptr()->get_data_path();
|
||||
const string filename = root + "readwrite/033.png";
|
||||
cv::Mat imread_result;
|
||||
cv::imread(filename, imread_result, cv::IMREAD_UNCHANGED);
|
||||
EXPECT_FALSE(imread_result.empty());
|
||||
|
||||
Animation animation;
|
||||
imreadanimation(filename, animation);
|
||||
EXPECT_FALSE(animation.still_image.empty());
|
||||
|
||||
EXPECT_EQ(0, cv::norm(animation.still_image, imread_result, cv::NORM_INF));
|
||||
}
|
||||
|
||||
#endif // HAVE_PNG
|
||||
|
||||
#if defined(HAVE_PNG) || defined(HAVE_SPNG)
|
||||
|
|
@ -676,7 +721,7 @@ TEST(Imgcodecs_APNG, imread_animation_16u)
|
|||
img = imread(filename, IMREAD_ANYDEPTH);
|
||||
ASSERT_FALSE(img.empty());
|
||||
EXPECT_TRUE(img.type() == CV_16UC1);
|
||||
EXPECT_EQ(19519, img.at<ushort>(0, 0));
|
||||
EXPECT_EQ(19517, img.at<ushort>(0, 0));
|
||||
|
||||
img = imread(filename, IMREAD_COLOR | IMREAD_ANYDEPTH);
|
||||
ASSERT_FALSE(img.empty());
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user