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 | |