libgui 3.00
libgui is a GUI library for the GameCube, Wii, and Wii U
 
Loading...
Searching...
No Matches
GuiImageData.cpp
1/****************************************************************************
2 * libgui
3 * Daryl Borth 2009-2026
4 * GuiImageData.cpp
5 ***************************************************************************/
6
7#include <png.h>
8#include <setjmp.h>
9#include <cmath>
10#include <cstdlib>
11#include <cstring>
12#include <memory>
13
14#include "Gui.h"
15
16namespace {
17 uint8_t * scratchBuffer = nullptr;
18 unsigned int scratchBufferSize = 0;
19
20 Mutex & getScratchMutex()
21 {
22 static Mutex scratchMutex;
23 return scratchMutex;
24 }
25
26 void ErrorCb(png_structp png_ptr, png_const_charp) { longjmp(png_jmpbuf(png_ptr), 1); }
27 void WarningCb(png_structp, png_const_charp) {}
28}
29
30void GuiImageData::setDecodeScratch(void * buffer, unsigned int size)
31{
32 scratchBuffer = static_cast<uint8_t *>(buffer);
33 scratchBufferSize = buffer ? size : 0;
34}
35
37{
38 return getScratchMutex();
39}
40
42{
43 texture = nullptr;
44 width = 0;
45 height = 0;
46 ownsTexture = false;
47 capWidth = 0;
48 capHeight = 0;
49}
50
51GuiImageData::GuiImageData(const uint8_t * i, int maxw, int maxh)
52{
53 texture = nullptr;
54 width = 0;
55 height = 0;
56 ownsTexture = false;
57 capWidth = 0;
58 capHeight = 0;
59
60 if(i)
61 decodeImage(i, &width, &height, maxw, maxh);
62}
63
64GuiImageData::GuiImageData(const uint8_t * i, uint8_t * dst, int maxw, int maxh)
65{
66 texture = dst;
67 width = 0;
68 height = 0;
69 ownsTexture = false;
70 capWidth = 0;
71 capHeight = 0;
72
73 if(i) {
74 decodeImage(i, &width, &height, maxw, maxh);
75 }
76}
77
78GuiImageData::GuiImageData(void * tex, int w, int h, bool takeOwnership)
79{
80 texture = tex;
81 width = w;
82 height = h;
83 ownsTexture = takeOwnership && tex;
84 capWidth = ownsTexture ? w : 0;
85 capHeight = ownsTexture ? h : 0;
86}
87
92
94{
95 if(ownsTexture && texture)
96 platform->getVideo()->getImageRenderer()->destroyTexture(texture);
97
98 texture = nullptr;
99 width = 0;
100 height = 0;
101 ownsTexture = false;
102 capWidth = 0;
103 capHeight = 0;
104}
105
108{
109 const uint8_t * data;
110 size_t offset;
111};
112
113static void ReadPngDataCb(png_structp png_ptr, png_bytep data, png_size_t length)
114{
115 PngMemoryData * memData = static_cast<PngMemoryData *>(png_get_io_ptr(png_ptr));
116 if(!memData)
117 return;
118
119 memcpy(data, memData->data + memData->offset, length);
120 memData->offset += length;
121}
122
123bool GuiImageData::reload(const uint8_t * pngData, int maxw, int maxh)
124{
125 if(!pngData)
126 return false;
127
128 int w = 0, h = 0;
129 return decodeImage(pngData, &w, &h, maxw, maxh);
130}
131
132GuiImageData::DecodedImage GuiImageData::decodeToRgba(const uint8_t * pngData, int maxw, int maxh)
133{
134 DecodedImage out;
135
136 if(!pngData)
137 return out;
138
139 MutexLock scratchGuard(getScratchMutex());
140 uint8_t * const localScratchBuffer = scratchBuffer;
141 const unsigned int localScratchBufferSize = scratchBufferSize;
142
143 if(!localScratchBuffer || localScratchBufferSize == 0)
144 return out;
145
146 if(png_sig_cmp(static_cast<png_const_bytep>(pngData), 0, 8))
147 return out;
148
149 png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, ErrorCb, WarningCb);
150 if(!png_ptr)
151 return out;
152
153 png_infop info_ptr = png_create_info_struct(png_ptr);
154 if(!info_ptr)
155 {
156 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
157 return out;
158 }
159
160 if(setjmp(png_jmpbuf(png_ptr)))
161 {
162 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
163 return out;
164 }
165
166 PngMemoryData memData = { pngData, 0 };
167 png_set_read_fn(png_ptr, &memData, ReadPngDataCb);
168
169 png_read_info(png_ptr, info_ptr);
170
171 png_uint_32 srcW, srcH;
172 int bit_depth, color_type, interlace_type;
173 png_get_IHDR(png_ptr, info_ptr, &srcW, &srcH, &bit_depth, &color_type, &interlace_type, nullptr, nullptr);
174
175 png_uint_32 w = srcW, h = srcH;
176 bool needsResize = (maxw > 0 && static_cast<int>(srcW) > maxw) || (maxh > 0 && static_cast<int>(srcH) > maxh);
177 if(needsResize)
178 {
179 double wScale = maxw > 0 ? static_cast<double>(maxw) / srcW : 1e30;
180 double hScale = maxh > 0 ? static_cast<double>(maxh) / srcH : 1e30;
181 double scale = wScale < hScale ? wScale : hScale;
182 w = static_cast<png_uint_32>(srcW * scale);
183 h = static_cast<png_uint_32>(srcH * scale);
184 if(w < 1) w = 1;
185 if(h < 1) h = 1;
186 }
187
188 if(bit_depth == 16)
189 png_set_strip_16(png_ptr);
190 if(color_type == PNG_COLOR_TYPE_PALETTE)
191 png_set_palette_to_rgb(png_ptr);
192 if(color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
193 png_set_expand_gray_1_2_4_to_8(png_ptr);
194 if(png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
195 png_set_tRNS_to_alpha(png_ptr);
196 if(color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)
197 png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
198 if(color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
199 png_set_gray_to_rgb(png_ptr);
200
201 png_read_update_info(png_ptr, info_ptr);
202 unsigned int rowBytes = png_get_rowbytes(png_ptr, info_ptr);
203
204 unsigned long long rowPtrBytes = static_cast<unsigned long long>(srcH) * sizeof(png_bytep);
205 unsigned long long srcRgbaBytes = static_cast<unsigned long long>(rowBytes) * srcH;
206 unsigned long long resizedRgbaBytes = needsResize ? static_cast<unsigned long long>(w) * h * 4 : 0;
207 unsigned long long totalScratchBytes = rowPtrBytes + srcRgbaBytes + resizedRgbaBytes;
208 if(totalScratchBytes > localScratchBufferSize)
209 {
210 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
211 return out;
212 }
213
214 // Final output buffer this call owns and returns - deliberately NOT part of the shared scratch allocation,
215 // since the caller (eg: a background thread) will go on using it.
216 std::unique_ptr<uint8_t, decltype(&free)> outRgba(static_cast<uint8_t *>(malloc(static_cast<size_t>(w) * h * 4)), free);
217 if(!outRgba)
218 {
219 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
220 return out;
221 }
222
223 png_bytep * row_pointers = reinterpret_cast<png_bytep *>(localScratchBuffer);
224 uint8_t * srcRgba = localScratchBuffer + rowPtrBytes;
225
226 for(png_uint_32 i = 0; i < srcH; i++)
227 row_pointers[i] = srcRgba + (static_cast<size_t>(i) * rowBytes);
228
229 png_read_image(png_ptr, row_pointers);
230
231 if(needsResize)
232 {
233 uint32_t xRatio = ((srcW << 16) / w) + 1;
234 uint32_t yRatio = ((srcH << 16) / h) + 1;
235
236 for(png_uint_32 y = 0; y < h; y++)
237 {
238 png_uint_32 sy = (y * yRatio) >> 16;
239 if(sy >= srcH) sy = srcH - 1;
240 const uint8_t * srcRow = srcRgba + static_cast<size_t>(sy) * rowBytes;
241 uint8_t * dstRow = outRgba.get() + static_cast<size_t>(y) * w * 4;
242
243 for(png_uint_32 x = 0; x < w; x++)
244 {
245 png_uint_32 sx = (x * xRatio) >> 16;
246 if(sx >= srcW) sx = srcW - 1;
247 memcpy(dstRow + x * 4, srcRow + sx * 4, 4);
248 }
249 }
250 }
251 else
252 {
253 memcpy(outRgba.get(), srcRgba, srcRgbaBytes);
254 }
255
256 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
257
258 out.rgba = std::move(outRgba);
259 out.width = w;
260 out.height = h;
261 out.assetScaled = (maxw == 0 && maxh == 0);
262 return out;
263}
264
266{
267 if(!decoded.valid())
268 return false;
269
270 int w = decoded.width;
271 int h = decoded.height;
272 bool assetScaled = decoded.assetScaled;
273
274 bool haveUsableTexture = texture && (!ownsTexture || platform->getVideo()->getImageRenderer()->canReuseTexture(capWidth, capHeight, w, h));
275
276 void * newTexture = texture;
277 if(!haveUsableTexture)
278 {
279 newTexture = platform->getVideo()->getImageRenderer()->createTexture(w, h);
280 if(!newTexture)
281 return false;
282 }
283
284 platform->getVideo()->getImageRenderer()->loadTextureData(newTexture, decoded.rgba.get(), w, h);
285
286 if(!haveUsableTexture)
287 {
288 if(ownsTexture && texture)
289 platform->getVideo()->getImageRenderer()->destroyTexture(texture);
290 texture = newTexture;
291 ownsTexture = true;
292 capWidth = w;
293 capHeight = h;
294 }
295
296 if(assetScaled)
297 {
298 // Report the design-canvas size. Keeps every GuiImage's on-screen footprint
299 // pinned to canvas pixels even though the backing texture is full native resolution
300 const PlatformConfig& config = platform->getConfig();
301 width = static_cast<int>(w / config.assetScaleX + 0.5f);
302 height = static_cast<int>(h / config.assetScaleY + 0.5f);
303 if(width < 1) width = 1;
304 if(height < 1) height = 1;
305 }
306 else
307 {
308 // Caller fit this decode to an explicit maxw/maxh box (eg. a cover-art thumbnail)
309 width = w;
310 height = h;
311 }
312
313 return true;
314}
315
316bool GuiImageData::loadClipped(const uint8_t * pngData, int boxW, int boxH, float radius, float inset, float offsetX, float offsetY)
317{
318 if(!pngData || boxW < 1 || boxH < 1)
319 return false;
320
321 DecodedImage src = decodeToRgba(pngData);
322 if(!src.valid())
323 return false;
324
325 // Work in the decoded image's own pixels (which are assetScale times the design size),
326 // but evaluate the mask in design pixels so the corner stays round when the two axes
327 // are scaled differently.
328 const PlatformConfig& config = platform->getConfig();
329 const float sx = config.assetScaleX;
330 const float sy = config.assetScaleY;
331 const int dstW = static_cast<int>(boxW * sx + 0.5f);
332 const int dstH = static_cast<int>(boxH * sy + 0.5f);
333
334 DecodedImage out;
335 out.rgba.reset(static_cast<uint8_t *>(calloc(static_cast<size_t>(dstW) * dstH, 4)));
336 if(!out.rgba)
337 return false;
338 out.width = dstW;
339 out.height = dstH;
340 out.assetScaled = true;
341
342 // source pixel that lands on destination (0,0): centred, then moved by the offset
343 const int shiftX = static_cast<int>(offsetX * sx + (offsetX < 0 ? -0.5f : 0.5f));
344 const int shiftY = static_cast<int>(offsetY * sy + (offsetY < 0 ? -0.5f : 0.5f));
345 const int originX = (src.width - dstW) / 2 - shiftX;
346 const int originY = (src.height - dstH) / 2 - shiftY;
347
348 // rounded-rectangle signed distance, in design pixels, for a box shrunk by `inset`
349 const float halfW = boxW * 0.5f - inset;
350 const float halfH = boxH * 0.5f - inset;
351 float r = radius - inset;
352 if(r > halfW) r = halfW;
353 if(r > halfH) r = halfH;
354 if(r < 0.0f) r = 0.0f;
355 const float fringe = sx < sy ? sx : sy; // physical pixels per design pixel
356
357 for(int y = 0; y < dstH; ++y)
358 {
359 const int srcY = y + originY;
360 if(srcY < 0 || srcY >= src.height)
361 continue;
362
363 const float qy = fabsf((y + 0.5f) / sy - boxH * 0.5f) - (halfH - r);
364
365 for(int x = 0; x < dstW; ++x)
366 {
367 const int srcX = x + originX;
368 if(srcX < 0 || srcX >= src.width)
369 continue;
370
371 const uint8_t * s = src.rgba.get() + (static_cast<size_t>(srcY) * src.width + srcX) * 4;
372 if(s[3] == 0)
373 continue;
374
375 const float qx = fabsf((x + 0.5f) / sx - boxW * 0.5f) - (halfW - r);
376 const float ox = qx > 0.0f ? qx : 0.0f;
377 const float oy = qy > 0.0f ? qy : 0.0f;
378 const float inner = qx > qy ? qx : qy;
379 const float dist = sqrtf(ox * ox + oy * oy) + (inner < 0.0f ? inner : 0.0f) - r;
380
381 float coverage = 0.5f - dist * fringe;
382 if(coverage <= 0.0f)
383 continue;
384 if(coverage > 1.0f)
385 coverage = 1.0f;
386
387 uint8_t * d = out.rgba.get() + (static_cast<size_t>(y) * dstW + x) * 4;
388 d[0] = s[0];
389 d[1] = s[1];
390 d[2] = s[2];
391 d[3] = static_cast<uint8_t>(s[3] * coverage + 0.5f);
392 }
393 }
394
395 return uploadDecoded(std::move(out));
396}
397
398bool GuiImageData::decodeImage(const uint8_t * pngData, int * outWidth, int * outHeight, int maxw, int maxh)
399{
400 DecodedImage decoded = decodeToRgba(pngData, maxw, maxh);
401 if(!decoded.valid())
402 return false;
403
404 if(!uploadDecoded(std::move(decoded)))
405 return false;
406
407 // width/height were just set by uploadDecoded() to the design-space size
408 // Read them back rather than reusing the raw decoded pixel size
409 if(outWidth) *outWidth = width;
410 if(outHeight) *outHeight = height;
411 return true;
412}
Umbrella header for the platform-agnostic libgui core.
int width
Width of image.
static DecodedImage decodeToRgba(const uint8_t *pngData, int maxw=0, int maxh=0)
Decodes a PNG buffer into plain RGBA8 pixels, resizing to fit maxw/maxh the same way the decoding con...
~GuiImageData()
Destructor.
void clear()
Releases any owned texture and resets this object to the same (empty) state as a default-constructed ...
bool uploadDecoded(DecodedImage &&decoded)
Uploads a DecodedImage produced by decodeToRgba() into this object's own texture, reusing the existin...
GuiImageData()
Constructor.
void * texture
Attached platform-native texture.
bool reload(const uint8_t *pngData, int maxw=0, int maxh=0)
Decodes new PNG data into this object's own texture, reusing the existing allocation whenever the pla...
static void setDecodeScratch(void *buffer, unsigned int size)
Sets the scratch buffer that every decode (the PNG-decoding constructors, reload(),...
bool loadClipped(const uint8_t *pngData, int boxW, int boxH, float radius, float inset=0.0f, float offsetX=0.0f, float offsetY=0.0f)
Decodes PNG data and fits it into a box of a given size, then masks it with a rounded rectangle: the ...
int height
Height of image.
static Mutex & scratchLock()
RAII lock guard - locks on construction, unlocks on destruction.
Definition Mutex.h:37
A simple mutual-exclusion lock.
Definition Mutex.h:14
CPU-only decode result from decodeToRgba(): raw RGBA8 pixel data that has NOT been uploaded to a plat...
std::unique_ptr< uint8_t, decltype(&free)> rgba
row-major RGBA8, or null if decode failed
bool assetScaled
False when maxw/maxh constrained this decode to a caller-specified pixel box.
Settings passed to Platform::init().
Definition Platform.h:50
float assetScaleX
Horizontal ratio of source art pixels to design-canvas pixels.
Definition Platform.h:56
float assetScaleY
Vertical counterpart of assetScaleX.
Definition Platform.h:58
Read cursor over an in-memory PNG, used as the libpng read callback's I/O pointer.
const uint8_t * data
Start of the PNG file in memory.
size_t offset
Bytes consumed so far.