#include "ImageData.hpp" #include #include #include #include #include #include ImageData::ImageData(ImageFormat format, uint width, uint height) : format(format), width(width), height(height) { size_t pixsize; switch (format) { case ImageFormat::rgb888: pixsize = 3; break; case ImageFormat::rgba8888: pixsize = 4; break; default: throw std::runtime_error("format is not supported"); } data = std::make_unique(width * height * pixsize); } ImageData::ImageData(ImageFormat format, uint width, uint height, std::unique_ptr data) : format(format), width(width), height(height), data(std::move(data)) { } ImageData::ImageData(ImageFormat format, uint width, uint height, const ubyte* data) : format(format), width(width), height(height) { size_t pixsize; switch (format) { case ImageFormat::rgb888: pixsize = 3; break; case ImageFormat::rgba8888: pixsize = 4; break; default: throw std::runtime_error("format is not supported"); } this->data = std::make_unique(width * height * pixsize); std::memcpy(this->data.get(), data, width * height * pixsize); } ImageData::~ImageData() = default; void ImageData::flipX() { switch (format) { case ImageFormat::rgb888: case ImageFormat::rgba8888: { uint size = (format == ImageFormat::rgba8888) ? 4 : 3; for (uint y = 0; y < height; y++) { for (uint x = 0; x < width / 2; x++) { for (uint c = 0; c < size; c++) { ubyte temp = data[(y * width + x) * size + c]; data[(y * width + x) * size + c] = data[(y * width + (width - x - 1)) * size + c]; data[(y * width + (width - x - 1)) * size + c] = temp; } } } break; } default: throw std::runtime_error("format is not supported"); } } void ImageData::flipY() { switch (format) { case ImageFormat::rgb888: case ImageFormat::rgba8888: { uint size = (format == ImageFormat::rgba8888) ? 4 : 3; for (uint y = 0; y < height/2; y++) { for (uint x = 0; x < width; x++) { for (uint c = 0; c < size; c++) { ubyte temp = data[(y * width + x) * size + c]; data[(y * width + x) * size + c] = data[((height-y-1) * width + x) * size + c]; data[((height-y-1) * width + x) * size + c] = temp; } } } break; } default: throw std::runtime_error("format is not supported"); } } void ImageData::blit(const ImageData& image, int x, int y) { if (format == image.format) { blitMatchingFormat(image, x, y); return; } if (format == ImageFormat::rgba8888 && image.format == ImageFormat::rgb888) { blitRGB_on_RGBA(image, x, y); return; } throw std::runtime_error("mismatching format"); } std::unique_ptr ImageData::cropped(int x, int y, int width, int height) const { width = std::min(width, this->width - x); height = std::min(height, this->height - y); if (width <= 0 || height <= 0) { throw std::runtime_error("invalid crop dimensions"); } auto subImage = std::make_unique(format, width, height); subImage->blitMatchingFormat(*this, -x, -y); return subImage; } static bool clip_line(int& x1, int& y1, int& x2, int& y2, int width, int height) { const int left = 0; const int right = width; const int bottom = 0; const int top = height; int dx = x2 - x1; int dy = y2 - y1; float t0 = 0.0f; float t1 = 1.0f; auto clip = [](int p, int q, float& t0, float& t1) { if (p == 0) { return q >= 0; } float t = static_cast(q) / p; if (p < 0) { if (t > t1) return false; if (t > t0) t0 = t; } else { if (t < t0) return false; if (t < t1) t1 = t; } return true; }; if (!clip(-dx, x1 - left, t0, t1)) return false; if (!clip( dx, right - x1, t0, t1)) return false; if (!clip(-dy, y1 - bottom, t0, t1)) return false; if (!clip( dy, top - y1, t0, t1)) return false; if (t1 < 1.0f) { x2 = x1 + static_cast(std::round(t1 * dx)); y2 = y1 + static_cast(std::round(t1 * dy)); } if (t0 > 0.0f) { x1 = x1 + static_cast(std::round(t0 * dx)); y1 = y1 + static_cast(std::round(t0 * dy)); } return true; } template static void draw_line(ImageData& image, int x1, int y1, int x2, int y2, const glm::ivec4& color) { ubyte* data = image.getData(); uint width = image.getWidth(); uint height = image.getHeight(); if ((x1 < 0 || x1 >= width || x2 < 0 || x2 >= width || y1 < 0 || y1 >= height || y2 < 0 || y2 >= height) && !clip_line(x1, y1, x2, y2, width, height)) { return; } int dx = std::abs(x2 - x1); int dy = -std::abs(y2 - y1); int sx = x1 < x2 ? 1 : -1; int sy = y1 < y2 ? 1 : -1; int err = dx + dy; while (true) { size_t pos = (y1 * width + x1) * channels; for (int i = 0; i < channels; i++) { data[pos + i] = color[i]; } if (x1 == x2 && y1 == y2) break; int e2 = 2 * err; if (e2 >= dy) { err += dy; x1 += sx; } if (e2 <= dx) { err += dx; y1 += sy; } } } void ImageData::drawLine(int x1, int y1, int x2, int y2, const glm::ivec4& color) { switch (format) { case ImageFormat::rgb888: draw_line<3>(*this, x1, y1, x2, y2, color); break; case ImageFormat::rgba8888: draw_line<4>(*this, x1, y1, x2, y2, color); break; default: break; } } template static void draw_rect(ImageData& image, int dstX, int dstY, int width, int height, const glm::ivec4& color) { ubyte* data = image.getData(); int imageWidth = image.getWidth(); int imageHeight = image.getHeight(); int x1 = glm::min(glm::max(dstX, 0), imageWidth - 1); int y1 = glm::min(glm::max(dstY, 0), imageHeight - 1); int x2 = glm::min(glm::max(dstX + width, 0), imageWidth - 1); int y2 = glm::min(glm::max(dstY + height, 0), imageHeight - 1); for (int y = y1; y <= y2; y++) { for (int x = x1; x <= x2; x++) { int index = (y * imageWidth + x) * channels; for (int i = 0; i < channels; i++) { data[index + i] = color[i]; } } } } void ImageData::drawRect(int x, int y, int width, int height, const glm::ivec4& color) { switch (format) { case ImageFormat::rgb888: draw_rect<3>(*this, x, y, width, height, color); break; case ImageFormat::rgba8888: draw_rect<4>(*this, x, y, width, height, color); break; default: break; } } void ImageData::blitRGB_on_RGBA(const ImageData& image, int x, int y) { ubyte* source = image.getData(); uint srcwidth = image.getWidth(); uint srcheight = image.getHeight(); for (uint srcy = std::max(0, -y); srcy < std::min(srcheight, height - y); srcy++) { for (uint srcx = std::max(0, -x); srcx < std::min(srcwidth, width - x); srcx++) { uint dstx = srcx + x; uint dsty = srcy + y; uint dstidx = (dsty * width + dstx) * 4; uint srcidx = (srcy * srcwidth + srcx) * 3; for (uint c = 0; c < 3; c++) { data[dstidx + c] = source[srcidx + c]; } data[dstidx + 3] = 255; } } } void ImageData::blitMatchingFormat(const ImageData& image, int x, int y) { uint comps; switch (format) { case ImageFormat::rgb888: comps = 3; break; case ImageFormat::rgba8888: comps = 4; break; default: throw std::runtime_error("only unsigned byte formats supported"); } ubyte* source = image.getData(); const uint width = this->width; const uint height = this->height; const uint src_width = image.getWidth(); const uint src_height = image.getHeight(); ubyte* data = this->data.get(); for (uint srcy = std::max(0, -y); srcy < std::min(src_height, height - y); srcy++) { for (uint srcx = std::max(0, -x); srcx < std::min(src_width, width - x); srcx++) { uint dstx = srcx + x; uint dsty = srcy + y; uint dstidx = (dsty * width + dstx) * comps; uint srcidx = (srcy * src_width + srcx) * comps; for (uint c = 0; c < comps; c++) { data[dstidx + c] = source[srcidx + c]; } } } } /* Extrude rectangle zone border pixels out by 1 pixel. Used to remove atlas texture border artifacts */ void ImageData::extrude(int x, int y, int w, int h) { uint comps; switch (format) { case ImageFormat::rgb888: comps = 3; break; case ImageFormat::rgba8888: comps = 4; break; default: throw std::runtime_error("only unsigned byte formats supported"); } int rx = x + w - 1; int ry = y + h - 1; // top-left pixel if (x > 0 && static_cast(x) < width && y > 0 && static_cast(y) < height) { uint srcidx = (y * width + x) * comps; uint dstidx = ((y - 1) * width + x - 1) * comps; for (uint c = 0; c < comps; c++) { data[dstidx + c] = data[srcidx + c]; } } // top-right pixel if (rx >= 0 && static_cast(rx) < width-1 && y > 0 && static_cast(y) < height) { uint srcidx = (y * width + rx) * comps; uint dstidx = ((y - 1) * width + rx + 1) * comps; for (uint c = 0; c < comps; c++) { data[dstidx + c] = data[srcidx + c]; } } // bottom-left pixel if (x > 0 && static_cast(x) < width && ry >= 0 && static_cast(ry) < height-1) { uint srcidx = (ry * width + x) * comps; uint dstidx = ((ry + 1) * width + x - 1) * comps; for (uint c = 0; c < comps; c++) { data[dstidx + c] = data[srcidx + c]; } } // bottom-right pixel if (rx >= 0 && static_cast(rx) < width-1 && ry >= 0 && static_cast(ry) < height-1) { uint srcidx = (ry * width + rx) * comps; uint dstidx = ((ry + 1) * width + rx + 1) * comps; for (uint c = 0; c < comps; c++) { data[dstidx + c] = data[srcidx + c]; } } // left border if (x > 0 && static_cast(x) < width) { for (uint ey = std::max(y, 0); static_cast(ey) < y + h; ey++) { uint srcidx = (ey * width + x) * comps; uint dstidx = (ey * width + x - 1) * comps; for (uint c = 0; c < comps; c++) { data[dstidx + c] = data[srcidx + c]; } } } // top border if (y > 0 && static_cast(y) < height) { for (uint ex = std::max(x, 0); static_cast(ex) < x + w; ex++) { uint srcidx = (y * width + ex) * comps; uint dstidx = ((y-1) * width + ex) * comps; for (uint c = 0; c < comps; c++) { data[dstidx + c] = data[srcidx + c]; } } } // right border if (rx >= 0 && static_cast(rx) < width-1) { for (uint ey = std::max(y, 0); static_cast(ey) < y + h; ey++) { uint srcidx = (ey * width + rx) * comps; uint dstidx = (ey * width + rx + 1) * comps; for (uint c = 0; c < comps; c++) { data[dstidx + c] = data[srcidx + c]; } } } // bottom border if (ry >= 0 && static_cast(ry) < height-1) { for (uint ex = std::max(x, 0); static_cast(ex) < x + w; ex++) { uint srcidx = (ry * width + ex) * comps; uint dstidx = ((ry+1) * width + ex) * comps; for (uint c = 0; c < comps; c++) { data[dstidx + c] = data[srcidx + c]; } } } } // Fixing black transparent pixels for Mip-Mapping void ImageData::fixAlphaColor() { int samples = 0; int sums[3] {}; for (uint ly = 0; ly < height; ly++) { for (uint lx = 0; lx < width; lx++) { if (data[(ly * width + lx) * 4 + 3] == 0) { continue; } samples++; for (int c = 0; c < 3; c++) { sums[c] += data[(ly * width + lx) * 4 + c]; } } } if (samples == 0) { return; } for (int i = 0; i < 3; i++) { sums[i] /= samples; } for (uint ly = 0; ly < height; ly++) { for (uint lx = 0; lx < width; lx++) { if (data[(ly * width + lx) * 4 + 3] != 0) { continue; } for (int i = 0; i < 3; i++) { data[(ly * width + lx) * 4 + i] = sums[i]; } } } } static void check_matching(const ImageData& a, const ImageData& b) { if (b.getWidth() != a.getWidth() || b.getHeight() != a.getHeight() || b.getFormat() != a.getFormat()) { throw std::runtime_error("image sizes or formats do not match"); } } void ImageData::mulColor(const glm::ivec4& color) { uint comps; switch (format) { case ImageFormat::rgb888: comps = 3; break; case ImageFormat::rgba8888: comps = 4; break; default: throw std::runtime_error("only unsigned byte formats supported"); } for (uint y = 0; y < height; y++) { for (uint x = 0; x < width; x++) { uint idx = (y * width + x) * comps; for (uint c = 0; c < comps; c++) { float val = static_cast(data[idx + c]) * color[c] / 255.0f; data[idx + c] = static_cast(std::min(std::max(val, 0.0f), 255.0f)); } } } } void ImageData::addColor(const ImageData& other, int multiplier) { check_matching(*this, other); uint comps; switch (format) { case ImageFormat::rgb888: comps = 3; break; case ImageFormat::rgba8888: comps = 4; break; default: throw std::runtime_error("only unsigned byte formats supported"); } for (uint y = 0; y < height; y++) { for (uint x = 0; x < width; x++) { uint idx = (y * width + x) * comps; for (uint c = 0; c < comps; c++) { int val = data[idx + c] + other.data[idx + c] * multiplier; data[idx + c] = static_cast(std::min(std::max(val, 0), 255)); } } } } void ImageData::extend(int newWidth, int newHeight) { size_t comps; switch (format) { case ImageFormat::rgb888: comps = 3; break; case ImageFormat::rgba8888: comps = 4; break; default: throw std::runtime_error("only unsigned byte formats supported"); } auto newData = std::make_unique(newWidth * newHeight * comps); for (uint y = 0; y < newHeight; y++) { for (uint x = 0; x < newWidth; x++) { for (size_t c = 0; c < comps; c++) { if (x < width && y < height) { newData[(y * newWidth + x) * comps + c] = data[(y * width + x) * comps + c]; } else { newData[(y * newWidth + x) * comps + c] = 0; } } } } data = std::move(newData); width = newWidth; height = newHeight; } void ImageData::addColor(const glm::ivec4& color, int multiplier) { uint comps; switch (format) { case ImageFormat::rgb888: comps = 3; break; case ImageFormat::rgba8888: comps = 4; break; default: throw std::runtime_error("only unsigned byte formats supported"); } for (uint y = 0; y < height; y++) { for (uint x = 0; x < width; x++) { uint idx = (y * width + x) * comps; for (uint c = 0; c < comps; c++) { int val = data[idx + c] + color[c] * multiplier; data[idx + c] = static_cast(std::min(std::max(val, 0), 255)); } } } } void ImageData::mulColor(const ImageData& other) { check_matching(*this, other); uint comps; switch (format) { case ImageFormat::rgb888: comps = 3; break; case ImageFormat::rgba8888: comps = 4; break; default: throw std::runtime_error("only unsigned byte formats supported"); } for (uint y = 0; y < height; y++) { for (uint x = 0; x < width; x++) { uint idx = (y * width + x) * comps; for (uint c = 0; c < comps; c++) { float val = static_cast(data[idx + c]) * static_cast(other.data[idx + c]) / 255.0f; data[idx + c] = static_cast(std::min(std::max(val, 0.0f), 255.0f)); } } } } std::unique_ptr add_atlas_margins(ImageData* image, int grid_size) { // RGBA is only supported assert(image->getFormat() == ImageFormat::rgba8888); assert(image->getWidth() == image->getHeight()); int srcwidth = image->getWidth(); int srcheight = image->getHeight(); int dstwidth = srcwidth + grid_size * 2; int dstheight = srcheight + grid_size * 2; const ubyte* srcdata = (const ubyte*)image->getData(); auto dstdata = std::make_unique(dstwidth*dstheight * 4); int imgres = image->getWidth() / grid_size; for (int row = 0; row < grid_size; row++) { for (int col = 0; col < grid_size; col++) { int sox = col * imgres; int soy = row * imgres; int dox = 1 + col * (imgres + 2); int doy = 1 + row * (imgres + 2); for (int ly = -1; ly <= imgres; ly++) { for (int lx = -1; lx <= imgres; lx++) { int sy = std::max(std::min(ly, imgres-1), 0); int sx = std::max(std::min(lx, imgres-1), 0); for (int c = 0; c < 4; c++) dstdata[((doy+ly) * dstwidth + dox + lx) * 4 + c] = srcdata[((soy+sy) * srcwidth + sox + sx) * 4 + c]; } } // Fixing black transparent pixels for Mip-Mapping for (int ly = 0; ly < imgres; ly++) { for (int lx = 0; lx < imgres; lx++) { if (srcdata[((soy+ly) * srcwidth + sox + lx) * 4 + 3]) { for (int c = 0; c < 3; c++) { int val = srcdata[((soy+ly) * srcwidth + sox + lx) * 4 + c]; if (dstdata[((doy+ly) * dstwidth + dox + lx + 1) * 4 + 3] == 0) dstdata[((doy+ly) * dstwidth + dox + lx + 1) * 4 + c] = val; if (dstdata[((doy+ly + 1) * dstwidth + dox + lx) * 4 + 3] == 0) dstdata[((doy+ly + 1) * dstwidth + dox + lx) * 4 + c] = val; } } } } } } return std::make_unique( image->getFormat(), dstwidth, dstheight, std::move(dstdata) ); }