| 1 | // Scintilla source code edit control |
| 2 | /** @file XPM.cxx |
| 3 | ** Define a class that holds data in the X Pixmap (XPM) format. |
| 4 | **/ |
| 5 | // Copyright 1998-2003 by Neil Hodgson <neilh@scintilla.org> |
| 6 | // The License.txt file describes the conditions under which this software may be distributed. |
| 7 | |
| 8 | #include <cstdlib> |
| 9 | #include <cstring> |
| 10 | #include <climits> |
| 11 | |
| 12 | #include <stdexcept> |
| 13 | #include <string_view> |
| 14 | #include <vector> |
| 15 | #include <map> |
| 16 | #include <optional> |
| 17 | #include <algorithm> |
| 18 | #include <iterator> |
| 19 | #include <memory> |
| 20 | |
| 21 | #include "ScintillaTypes.h" |
| 22 | |
| 23 | #include "Debugging.h" |
| 24 | #include "Geometry.h" |
| 25 | #include "Platform.h" |
| 26 | |
| 27 | #include "XPM.h" |
| 28 | |
| 29 | using namespace Scintilla; |
| 30 | using namespace Scintilla::Internal; |
| 31 | |
| 32 | namespace { |
| 33 | |
| 34 | const char *NextField(const char *s) noexcept { |
| 35 | // In case there are leading spaces in the string |
| 36 | while (*s == ' ') { |
| 37 | s++; |
| 38 | } |
| 39 | while (*s && *s != ' ') { |
| 40 | s++; |
| 41 | } |
| 42 | while (*s == ' ') { |
| 43 | s++; |
| 44 | } |
| 45 | return s; |
| 46 | } |
| 47 | |
| 48 | // Data lines in XPM can be terminated either with NUL or " |
| 49 | size_t MeasureLength(const char *s) noexcept { |
| 50 | size_t i = 0; |
| 51 | while (s[i] && (s[i] != '\"')) |
| 52 | i++; |
| 53 | return i; |
| 54 | } |
| 55 | |
| 56 | unsigned int ValueOfHex(const char ch) noexcept { |
| 57 | if (ch >= '0' && ch <= '9') |
| 58 | return ch - '0'; |
| 59 | else if (ch >= 'A' && ch <= 'F') |
| 60 | return ch - 'A' + 10; |
| 61 | else if (ch >= 'a' && ch <= 'f') |
| 62 | return ch - 'a' + 10; |
| 63 | else |
| 64 | return 0; |
| 65 | } |
| 66 | |
| 67 | ColourRGBA ColourFromHex(const char *val) noexcept { |
| 68 | const unsigned int r = ValueOfHex(val[0]) * 16 + ValueOfHex(val[1]); |
| 69 | const unsigned int g = ValueOfHex(val[2]) * 16 + ValueOfHex(val[3]); |
| 70 | const unsigned int b = ValueOfHex(val[4]) * 16 + ValueOfHex(val[5]); |
| 71 | return ColourRGBA(r, g, b); |
| 72 | } |
| 73 | |
| 74 | } |
| 75 | |
| 76 | |
| 77 | ColourRGBA XPM::ColourFromCode(int ch) const noexcept { |
| 78 | return colourCodeTable[ch]; |
| 79 | } |
| 80 | |
| 81 | void XPM::FillRun(Surface *surface, int code, int startX, int y, int x) const { |
| 82 | if ((code != codeTransparent) && (startX != x)) { |
| 83 | const PRectangle rc = PRectangle::FromInts(startX, y, x, y + 1); |
| 84 | surface->FillRectangle(rc, ColourFromCode(code)); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | XPM::XPM(const char *textForm) { |
| 89 | Init(textForm); |
| 90 | } |
| 91 | |
| 92 | XPM::XPM(const char *const *linesForm) { |
| 93 | Init(linesForm); |
| 94 | } |
| 95 | |
| 96 | void XPM::Init(const char *textForm) { |
| 97 | // Test done is two parts to avoid possibility of overstepping the memory |
| 98 | // if memcmp implemented strangely. Must be 4 bytes at least at destination. |
| 99 | if ((0 == memcmp(textForm, "/* X" , 4)) && (0 == memcmp(textForm, "/* XPM */" , 9))) { |
| 100 | // Build the lines form out of the text form |
| 101 | std::vector<const char *> linesForm = LinesFormFromTextForm(textForm); |
| 102 | if (!linesForm.empty()) { |
| 103 | Init(linesForm.data()); |
| 104 | } |
| 105 | } else { |
| 106 | // It is really in line form |
| 107 | Init(reinterpret_cast<const char * const *>(textForm)); |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | void XPM::Init(const char *const *linesForm) { |
| 112 | height = 1; |
| 113 | width = 1; |
| 114 | nColours = 1; |
| 115 | pixels.clear(); |
| 116 | codeTransparent = ' '; |
| 117 | if (!linesForm) |
| 118 | return; |
| 119 | |
| 120 | std::fill(colourCodeTable, std::end(colourCodeTable), ColourRGBA(0, 0, 0)); |
| 121 | const char *line0 = linesForm[0]; |
| 122 | width = atoi(line0); |
| 123 | line0 = NextField(line0); |
| 124 | height = atoi(line0); |
| 125 | pixels.resize(width*height); |
| 126 | line0 = NextField(line0); |
| 127 | nColours = atoi(line0); |
| 128 | line0 = NextField(line0); |
| 129 | if (atoi(line0) != 1) { |
| 130 | // Only one char per pixel is supported |
| 131 | return; |
| 132 | } |
| 133 | |
| 134 | for (int c=0; c<nColours; c++) { |
| 135 | const char *colourDef = linesForm[c+1]; |
| 136 | const char code = colourDef[0]; |
| 137 | colourDef += 4; |
| 138 | ColourRGBA colour(0, 0, 0, 0); |
| 139 | if (*colourDef == '#') { |
| 140 | colour = ColourFromHex(colourDef+1); |
| 141 | } else { |
| 142 | codeTransparent = code; |
| 143 | } |
| 144 | colourCodeTable[static_cast<unsigned char>(code)] = colour; |
| 145 | } |
| 146 | |
| 147 | for (ptrdiff_t y=0; y<height; y++) { |
| 148 | const char *lform = linesForm[y+nColours+1]; |
| 149 | const size_t len = MeasureLength(lform); |
| 150 | for (size_t x = 0; x<len; x++) |
| 151 | pixels[y * width + x] = lform[x]; |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | void XPM::Draw(Surface *surface, const PRectangle &rc) { |
| 156 | if (pixels.empty()) { |
| 157 | return; |
| 158 | } |
| 159 | // Centre the pixmap |
| 160 | const int startY = static_cast<int>(rc.top + (rc.Height() - height) / 2); |
| 161 | const int startX = static_cast<int>(rc.left + (rc.Width() - width) / 2); |
| 162 | for (int y=0; y<height; y++) { |
| 163 | int prevCode = 0; |
| 164 | int xStartRun = 0; |
| 165 | for (int x=0; x<width; x++) { |
| 166 | const int code = pixels[y * width + x]; |
| 167 | if (code != prevCode) { |
| 168 | FillRun(surface, prevCode, startX + xStartRun, startY + y, startX + x); |
| 169 | xStartRun = x; |
| 170 | prevCode = code; |
| 171 | } |
| 172 | } |
| 173 | FillRun(surface, prevCode, startX + xStartRun, startY + y, startX + width); |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | ColourRGBA XPM::PixelAt(int x, int y) const noexcept { |
| 178 | if (pixels.empty() || (x < 0) || (x >= width) || (y < 0) || (y >= height)) { |
| 179 | // Out of bounds -> transparent black |
| 180 | return ColourRGBA(0, 0, 0, 0); |
| 181 | } |
| 182 | const int code = pixels[y * width + x]; |
| 183 | return ColourFromCode(code); |
| 184 | } |
| 185 | |
| 186 | std::vector<const char *> XPM::LinesFormFromTextForm(const char *textForm) { |
| 187 | // Build the lines form out of the text form |
| 188 | std::vector<const char *> linesForm; |
| 189 | int countQuotes = 0; |
| 190 | int strings=1; |
| 191 | int j=0; |
| 192 | for (; countQuotes < (2*strings) && textForm[j] != '\0'; j++) { |
| 193 | if (textForm[j] == '\"') { |
| 194 | if (countQuotes == 0) { |
| 195 | // First field: width, height, number of colours, chars per pixel |
| 196 | const char *line0 = textForm + j + 1; |
| 197 | // Skip width |
| 198 | line0 = NextField(line0); |
| 199 | // Add 1 line for each pixel of height |
| 200 | strings += atoi(line0); |
| 201 | line0 = NextField(line0); |
| 202 | // Add 1 line for each colour |
| 203 | strings += atoi(line0); |
| 204 | } |
| 205 | if (countQuotes / 2 >= strings) { |
| 206 | break; // Bad height or number of colours! |
| 207 | } |
| 208 | if ((countQuotes & 1) == 0) { |
| 209 | linesForm.push_back(textForm + j + 1); |
| 210 | } |
| 211 | countQuotes++; |
| 212 | } |
| 213 | } |
| 214 | if (textForm[j] == '\0' || countQuotes / 2 > strings) { |
| 215 | // Malformed XPM! Height + number of colours too high or too low |
| 216 | linesForm.clear(); |
| 217 | } |
| 218 | return linesForm; |
| 219 | } |
| 220 | |
| 221 | RGBAImage::RGBAImage(int width_, int height_, float scale_, const unsigned char *pixels_) : |
| 222 | height(height_), width(width_), scale(scale_) { |
| 223 | if (pixels_) { |
| 224 | pixelBytes.assign(pixels_, pixels_ + CountBytes()); |
| 225 | } else { |
| 226 | pixelBytes.resize(CountBytes()); |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | RGBAImage::RGBAImage(const XPM &xpm) { |
| 231 | height = xpm.GetHeight(); |
| 232 | width = xpm.GetWidth(); |
| 233 | scale = 1; |
| 234 | pixelBytes.resize(CountBytes()); |
| 235 | for (int y=0; y<height; y++) { |
| 236 | for (int x=0; x<width; x++) { |
| 237 | SetPixel(x, y, xpm.PixelAt(x, y)); |
| 238 | } |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | int RGBAImage::CountBytes() const noexcept { |
| 243 | return width * height * 4; |
| 244 | } |
| 245 | |
| 246 | const unsigned char *RGBAImage::Pixels() const noexcept { |
| 247 | return pixelBytes.data(); |
| 248 | } |
| 249 | |
| 250 | void RGBAImage::SetPixel(int x, int y, ColourRGBA colour) noexcept { |
| 251 | unsigned char *pixel = pixelBytes.data() + (y * width + x) * 4; |
| 252 | // RGBA |
| 253 | pixel[0] = colour.GetRed(); |
| 254 | pixel[1] = colour.GetGreen(); |
| 255 | pixel[2] = colour.GetBlue(); |
| 256 | pixel[3] = colour.GetAlpha(); |
| 257 | } |
| 258 | |
| 259 | // Transform a block of pixels from RGBA to BGRA with premultiplied alpha. |
| 260 | // Used for DrawRGBAImage on some platforms. |
| 261 | void RGBAImage::BGRAFromRGBA(unsigned char *pixelsBGRA, const unsigned char *pixelsRGBA, size_t count) noexcept { |
| 262 | for (size_t i = 0; i < count; i++) { |
| 263 | const unsigned char alpha = pixelsRGBA[3]; |
| 264 | // Input is RGBA, output is BGRA with premultiplied alpha |
| 265 | pixelsBGRA[2] = pixelsRGBA[0] * alpha / UCHAR_MAX; |
| 266 | pixelsBGRA[1] = pixelsRGBA[1] * alpha / UCHAR_MAX; |
| 267 | pixelsBGRA[0] = pixelsRGBA[2] * alpha / UCHAR_MAX; |
| 268 | pixelsBGRA[3] = alpha; |
| 269 | pixelsRGBA += bytesPerPixel; |
| 270 | pixelsBGRA += bytesPerPixel; |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | RGBAImageSet::RGBAImageSet() : height(-1), width(-1) { |
| 275 | } |
| 276 | |
| 277 | /// Remove all images. |
| 278 | void RGBAImageSet::Clear() noexcept { |
| 279 | images.clear(); |
| 280 | height = -1; |
| 281 | width = -1; |
| 282 | } |
| 283 | |
| 284 | /// Add an image. |
| 285 | void RGBAImageSet::AddImage(int ident, std::unique_ptr<RGBAImage> image) { |
| 286 | images[ident] = std::move(image); |
| 287 | height = -1; |
| 288 | width = -1; |
| 289 | } |
| 290 | |
| 291 | /// Get image by id. |
| 292 | RGBAImage *RGBAImageSet::Get(int ident) { |
| 293 | ImageMap::iterator it = images.find(ident); |
| 294 | if (it != images.end()) { |
| 295 | return it->second.get(); |
| 296 | } |
| 297 | return nullptr; |
| 298 | } |
| 299 | |
| 300 | /// Give the largest height of the set. |
| 301 | int RGBAImageSet::GetHeight() const noexcept { |
| 302 | if (height < 0) { |
| 303 | for (const std::pair<const int, std::unique_ptr<RGBAImage>> &image : images) { |
| 304 | if (height < image.second->GetHeight()) { |
| 305 | height = image.second->GetHeight(); |
| 306 | } |
| 307 | } |
| 308 | } |
| 309 | return (height > 0) ? height : 0; |
| 310 | } |
| 311 | |
| 312 | /// Give the largest width of the set. |
| 313 | int RGBAImageSet::GetWidth() const noexcept { |
| 314 | if (width < 0) { |
| 315 | for (const std::pair<const int, std::unique_ptr<RGBAImage>> &image : images) { |
| 316 | if (width < image.second->GetWidth()) { |
| 317 | width = image.second->GetWidth(); |
| 318 | } |
| 319 | } |
| 320 | } |
| 321 | return (width > 0) ? width : 0; |
| 322 | } |
| 323 | |