SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
DistanceMapping.cpp
Go to the documentation of this file.
2
3#include <variant>
4
5#include <sourcepp/Macros.h>
8
9using namespace sourcepp;
10using namespace vtfpp;
11
13namespace {
14
15using namespace ImageConversion;
16using math::Vec2i;
17using math::Vec2f32;
18
19enum Leaf : uint8_t {
20 IN, // leaf is opaque
21 OUT, // leaf is transparent
22 FIGUREITOUT, // leaf is mixed but small so you should brute force scan.
23};
24
25uint16_t i32tou16sat(int32_t i, int32_t cap = UINT16_MAX) {
26 return static_cast<uint16_t>(std::clamp(i, 0, cap));
27}
28
29uint16_t ftoabsu16(float f) {
30 return i32tou16sat(static_cast<int32_t>(fabs(f)));
31}
32
33using IndexFunction = std::function<int32_t(Vec2i, uint16_t, uint16_t, uint8_t, uint8_t)>;
34
35int32_t indexClamped(Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs) {
36 int32_t rX = i32tou16sat(v[0], w - 1);
37 int32_t rY = i32tou16sat(v[1], h - 1);
38 return (rY * w + rX) * pxLen + alphaOffs;
39}
40
41int32_t indexReflected(Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs) {
42 int32_t rX = v[0] >= w ? w - v[0] % w : v[0];
43 int32_t rY = v[1] >= h ? h - v[1] % h : v[1];
44 return (rY * w + rX) * pxLen + alphaOffs;
45}
46
47int32_t indexWrapped(Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs) {
48 int32_t rX = (v[0] + w) % w;
49 int32_t rY = (v[1] + h) % h;
50 return (rY * w + rX) * pxLen + alphaOffs;
51}
52
53IndexFunction indexBy(ImageConversion::ResizeEdge edge)
54{
55 switch (edge) {
56 default:
57 break;
59 return indexReflected;
61 return indexWrapped;
62 }
63 return indexClamped;
64}
65
66// big ugly immutable record of everything pertaining to how we read alpha from an image
67class SampleParam {
68public:
69 SampleParam(
70 const float *srcImg,
71 uint16_t inWidth,
72 uint16_t inHeight,
73 uint16_t reduceX,
74 uint16_t reduceY,
75 uint8_t pxLen,
76 uint8_t alphaOffs,
77 float distanceSpread,
78 bool sampleCentered,
79 float alphaThreshold,
81 )
82 : srcImg(srcImg)
83 , pxLen(pxLen)
84 , alphaOffs(alphaOffs)
85 , imgWidth(inWidth)
86 , imgHeight(inHeight)
87 , reduceX(reduceX)
88 , reduceY(reduceY)
89 , searchRadius(ftoabsu16(2.0f * std::max<float>(reduceX, reduceY) * distanceSpread))
90 // the second power of two after search radius
91 , granularity(uint16_t{4} << ftoabsu16(ceil(log2(static_cast<float>(this->searchRadius)))))
92 , offsX(sampleCentered ? reduceX / 2 : 0)
93 , offsY(sampleCentered ? reduceY / 2 : 0)
94 , alphaThreshold(alphaThreshold)
95 , indexFn(indexBy(edge))
96 {}
97
98 float sample(Vec2i v) const {
99 return this->srcImg[this->indexFn(v, this->imgWidth, this->imgHeight, this->pxLen, this->alphaOffs)];
100 }
101
102 Leaf thresh(float alpha) const {
103 return alpha >= this->alphaThreshold ? IN : OUT;
104 }
105
106 const float *const srcImg;
107 const float alphaThreshold;
108 const uint16_t imgHeight;
109 const uint16_t imgWidth;
110 const uint16_t searchRadius;
111 const uint16_t granularity;
112 const uint16_t offsX, offsY;
113 const uint16_t reduceX, reduceY;
114 const uint8_t pxLen;
115 const uint8_t alphaOffs;
116 const IndexFunction indexFn;
117};
118
119// immutable map detailing where we can skip computation
120struct QuadTree {
121 QuadTree(const SampleParam *param, uint32_t &totalComputation) : root(this->scanImg(0, 0, param->imgWidth, param->imgHeight, param, totalComputation))
122 {}
123 ~QuadTree() {
124 Node::wither(root);
125 }
126
127 struct Node;
128 using Branch = std::variant<Leaf, Node *>;
129 struct Node {
130 const std::array<const Branch, 4> branches;
131 Node(Branch nw, Branch ne, Branch sw, Branch se) : branches {nw, ne, sw, se} {};
132 ~Node() {
133 for ( Branch branch : this->branches ) {
134 wither(branch);
135 }
136 }
137 static void wither(Branch c) {
138 if (holds_alternative<Node *>(c)) {
139 delete get<Node*>(c);
140 }
141 }
142 };
143
144 static bool brancheq(Branch a, Branch b) {
145 return holds_alternative<Leaf>(a) && holds_alternative<Leaf>(b) && get<Leaf>(a) == get<Leaf>(b);
146 }
147
148 const Branch root;
149private:
150 // TODO: cleverer scan to partition top-down?
151 static Branch scanImg(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const SampleParam *param, uint32_t &totalComputation) {
152 if (w <= param->granularity || h <= param->granularity) {
153 // extend sensitivity to minimum safe distance. a safe leaf is a safe leaf, no need to peek.
154 int32_t ix = x - param->searchRadius + param->offsX, tx = x + w + param->searchRadius + param->offsX;
155 int32_t iy = y - param->searchRadius + param->offsY, ty = y + h + param->searchRadius + param->offsY;
156 Leaf curShade = param->thresh(param->sample({ix, iy}));
157
158 for (auto jx = ix; jx < tx; jx++) {
159 for (auto jy = iy; jy < ty; jy++) {
160 if (param->thresh(param->sample({jx, jy})) != curShade) {
161 totalComputation += w / param->reduceX * h / param->reduceY;
162 return FIGUREITOUT;
163 }
164 }
165 }
166
167 return curShade;
168 }
169
170 uint16_t hw = w / 2, hh = h / 2;
171 Branch iNW = scanImg(x, y, hw, hh, param, totalComputation);
172 Branch iNE = scanImg(x + hw, y, hw, hh, param, totalComputation);
173 Branch iSW = scanImg(x, y + hh, hw, hh, param, totalComputation);
174 Branch iSE = scanImg(x + hw, y + hh, hw, hh, param, totalComputation);
175
176 if (brancheq(iNW, iNE) && brancheq(iNE, iSW) && brancheq(iSW, iSE)) {
177 return get<Leaf>(iNW);
178 }
179 return new Node(iNW, iNE, iSW, iSE);
180 }
181};
182
183// traverse a QuadTree at one row of a leaf at a time
184class RasterCursor {
185public:
186 RasterCursor(const QuadTree *tree, uint16_t imgWidth, uint16_t imgHeight, uint16_t reduceX = 1, uint16_t reduceY = 1, uint16_t offsX = 0, uint16_t offsY = 0)
187 : tree(tree)
188 , imgWidth(imgWidth)
189 , imgHeight(imgHeight)
190 , reduceX(reduceX)
191 , reduceY(reduceY)
192 , offsX(offsX)
193 , curX(offsX)
194 , curY(offsY)
195 {
196 this->curShade = this->reorient();
197 }
198 RasterCursor(const QuadTree *tree, const SampleParam *param)
199 : RasterCursor(tree, param->imgWidth, param->imgHeight, param->reduceX, param->reduceY, param->offsX, param->offsY)
200 {}
201
202 bool getContiguousRun(Leaf &srcShade, uint16_t &srcY, uint16_t &srcX, uint16_t &dstW) {
203 srcX = this->curX;
204 srcY = this->curY;
205 dstW = 0;
206 srcShade = this->curShade = this->reorient();
207
208 while (this->curX < this->imgWidth && QuadTree::brancheq(srcShade, this->curShade)) {
209 auto blkSize = this->imgWidth >> this->depth;
210 this->curX += blkSize;
211 dstW += blkSize / this->reduceX;
212 this->curShade = this->reorient();
213 }
214
215 if (this->curX >= this->imgWidth) {
216 this->curX = this->offsX;
217 this->curY += this->reduceY;
218 }
219
220 return this->curY < this->imgHeight;
221 }
222private:
223 Leaf reorient() {
224 this->depth = 0;
225 this->curBranch = this->tree->root;
226 auto remX = this->curX, remY = this->curY;
227
228 while (std::holds_alternative<QuadTree::Node *>(this->curBranch)) {
229 auto benchX = this->imgWidth >> (this->depth + 1), benchY = this->imgHeight >> (this->depth + 1);
230 if (benchX < 2 || benchY < 2) {
231 return this->curShade; // would be nice to make this unrepresentable
232 }
233 auto bGtX = remX >= benchX, bGtY = remY >= benchY;
234 remX -= benchX * bGtX;
235 remY -= benchY * bGtY;
236 this->curBranch = std::get<QuadTree::Node *>(this->curBranch)->branches[bGtX | bGtY << 1];
237 this->depth++;
238 }
239
240 return get<Leaf>(this->curBranch);
241 }
242
243 const QuadTree *tree;
244 uint16_t curX, curY, depth;
245 const uint16_t imgWidth, imgHeight, reduceX, reduceY, offsX;
246 Leaf curShade;
247 QuadTree::Branch curBranch;
248};
249
250float vecDist(Vec2f32 v0, Vec2f32 v1) {
251 return std::hypot(v0[0] - v1[0], v1[0] - v1[1]);
252}
253Vec2i vec2ipmul(Vec2i v0, Vec2i v1) {
254 return {v0[0] * v1[0], v0[1] * v1[1]};
255}
256
257using math::pi_f32;
258
259void paintMap(
260 const SampleParam *param,
261 float *dstImg,
262 uint8_t dstPxLen,
263 uint8_t dstAlphaOffs,
264 bool distanceAA,
265 bool euclidean,
266 const float *tangentDiffusion,
267 bool *valveQuirks
268) {
269 uint32_t totalComputation = 0;
270
271 const QuadTree tree(param, totalComputation);
272 RasterCursor cursor(&tree, param);
273
274 uint32_t iOut = dstAlphaOffs;
275 Leaf srcShade = FIGUREITOUT;
276 uint16_t srcY = 0, srcX = 0, dstW = 0;
277
278 float fRadius = static_cast<float>(param->searchRadius);
279 uint16_t dstWidth = param->imgWidth / param->reduceX;
280 uint16_t dstHeight = param->imgHeight / param->reduceY;
281
282 uint32_t iGradW = 0;
283 std::vector<float> gradients = tangentDiffusion ? std::vector<float>(totalComputation, -pi_f32 - 0.05f) : std::vector<float>(0);
284 float lastAng = 0;
285 float hypot1 = 1;
286
287 bool keepPainting = true;
288 do {
289 keepPainting = cursor.getContiguousRun(srcShade, srcY, srcX, dstW);
290 float fillDistance = 1.0f;
291 switch (srcShade) {
292 case OUT:
293 fillDistance = 0.0f;
294 case IN:
295 for (uint16_t i = 0; i < dstW; i++, iOut += dstPxLen) {
296 dstImg[iOut] = fillDistance;
297 }
298 break;
299 case FIGUREITOUT:
300 default:
301 // oh no we actually have to do work
302 for (uint16_t i = 0; i < dstW; i++, iGradW++, iOut += dstPxLen) {
303 int32_t curX = srcX + i * param->reduceX;
304 float alphaRef = param->sample({curX, srcY});
305 Leaf stateRef = param->thresh(alphaRef);
306 float nearest = std::numeric_limits<float>::max();
307 for (int32_t sX = curX - param->searchRadius, tX = curX + param->searchRadius; sX < tX; sX++) {
308 for (int32_t sY = srcY - param->searchRadius, tY = srcY + param->searchRadius; sY < tY; sY++) {
309 float alphaSmp = param->sample({sX, sY});
310 Leaf stateSmp = param->thresh(alphaSmp);
311 if (stateSmp != stateRef) {
312 float dist = std::hypot<float>(sX - curX, sY - srcY);
313
314 if (tangentDiffusion || distanceAA || euclidean) {
315 lastAng = atan2(sX - curX, sY - srcY);
316 }
317 if (distanceAA || euclidean) {
318 hypot1 = 1 / cos(fabs(fmod(lastAng + pi_f32 * 2.25f, pi_f32 / 2.0f) - pi_f32 / 4.0f));
319 }
320 if (distanceAA) {
321 dist += hypot1 * (1 - fabs(alphaSmp - alphaRef));
322 }
323 if (euclidean && dist > fRadius + hypot1) {
324 continue;
325 }
326 nearest = fmin(nearest, dist);
327 }
328 }
329 }
330
331 nearest = fmin(0.5f, nearest * 0.5f / fRadius);
332 if (stateRef == OUT) {
333 nearest = -nearest;
334 }
335
336 dstImg[iOut] = 0.5 + nearest;
337 if (tangentDiffusion) {
338 gradients[iGradW] = lastAng;
339 }
340 }
341 }
342 } while (keepPainting);
343
344 if (tangentDiffusion) {
345 // attempt to diffuse quantization error tangent to the gradient of the
346 // distance map. you could imagine some approach that involves grouping
347 // greyscale pixels into distinct contours, and playing ring around the
348 // rosy for each group, but there's no practical way to avoid littering
349 // residual bits of error into pixels you've already quantized, ruining
350 // the whole point. the usual suspects like floyd-steinberg use kernels
351 // shaped to only write *ahead* of a conventional raster scan. here, we
352 // do the same by determining a shape, bounded by the following "mask":
353 //
354 // ⎡ 0 0 0 ⎤
355 // ⎢ 0 0 1 ⎥
356 // ⎣ 1 1 1 ⎦,
357 //
358 // mapping adjacent pixels to the unit circle's bounding box like this:
359 //
360 // ⎡ NW:(-1, 1) N:(0, 1) NE:(1, 1) ⎤
361 // ⎢ W:(-1, 0) E:(1, 0) ⎥
362 // ⎣ SW:(-1, -1) S:(0, -1) SE:(1, -1) ⎦,
363 //
364 // choose the two points nearest (x, y) on the unit circle at θ + π / 2
365 // (i.e. the right-hand normal of the gradient direction θ). distribute
366 // the quantization error E between the two, weighted by the opposite's
367 // distance over the sum of both distances (it feels as though a better
368 // weighting should exist but that's what i get for dropping out), then
369 // fold the whole thing over to only entries ahead of us in scan order:
370 //
371 // ⎡ ⎤
372 // k = ⎢ E+W ⎥
373 // ⎣ SW+NE S+N SE+NW ⎦,
374 //
375 // yielding our per-destination-pixel kernel k. a neat property of this
376 // is that we can continue ignoring pixels we previously ignored thanks
377 // to the quadtree (and thus, never set any angle for): we're diffusing
378 // error tangent to the gradient, and that way lie only pixels where we
379 // also already set an angle. we can index angles independently, which,
380 // depending on the contents of the source image, might require a small
381 // fraction of the space compared to the distance map (or just as much)
382 static constexpr std::array<const int8_t, 8> hardSigns{0, 1, 1, 1, 0, -1, -1, -1};
383 // of course, in our case, y grows downward, so we really want is E..NW
384 // which, conveniently enough, is just 0..3, so no aux lookup table. if
385 // we wanted SW..E, we'd index into {0, 5, 6, 7}. the pixel offsets are
386 // the only result where we need to be picky about the final signs; the
387 // preceding calculations can run in our flipped coordinates just fine.
388 static constexpr float eighth = pi_f32 / 4.0f;
389
390 RasterCursor diffCursor(&tree, dstWidth, dstHeight);
391 uint32_t diffOut = dstAlphaOffs;
392 Leaf diffShade = FIGUREITOUT;
393 const uint32_t dstBufLen = dstWidth * dstHeight * dstPxLen;
394 uint16_t diffY = 0, diffX = 0, diffW = 0;
395 uint32_t iGradR = 0;
396
397 bool keepDiffusing = true;
398 do {
399 keepDiffusing = diffCursor.getContiguousRun(diffShade, diffY, diffX, diffW);
400 if (diffShade != FIGUREITOUT) {
401 continue;
402 }
403 bool rightEdge = diffX + diffW >= dstWidth;
404
405 for (uint16_t i = 0; i < diffW; i++, iGradR++, diffOut += dstPxLen) {
406 float theta = gradients[iGradR];
407 if (theta < -pi_f32 - 0.025f) {
408 continue;
409 }
410 float thetaNormal = fmod(theta + 2.5f * pi_f32, 2.0f * pi_f32);
411 uint8_t nEighths = static_cast<uint32_t>(std::floor(thetaNormal / eighth)) % 8;
412 double quantum = *tangentDiffusion;
413 double curPx = dstImg[diffOut];
414 double quantized = std::round(curPx / quantum) * quantum;
415 float quantErr = curPx - quantized;
416 dstImg[diffOut] = quantized;
417 bool atRight = rightEdge && i >= diffW - 1;
418 bool bottomEdge = diffY >= dstHeight - 1;
419 if (atRight && bottomEdge) {
420 break;
421 }
422 Vec2f32 normal{cos(thetaNormal), sin(thetaNormal)};
423 // distance calculation uses the whole circle
424 float dist0 = vecDist(normal, {hardSigns[(nEighths + 2) % 8], hardSigns[(nEighths + 0) % 8]});
425 float dist1 = vecDist(normal, {hardSigns[(nEighths + 3) % 8], hardSigns[(nEighths + 1) % 8]});
426 // offset selection uses the part of the circle that is yet to be scanned
427 Vec2i offs0{hardSigns[nEighths % 4 + 2], hardSigns[nEighths % 4 + 0]};
428 Vec2i offs1{hardSigns[nEighths % 4 + 3], hardSigns[nEighths % 4 + 1]};
429 Vec2i pos{diffX + i, diffY};
430 if (bottomEdge) {
431 offs0 = vec2ipmul(offs0, {1, 0});
432 offs1 = vec2ipmul(offs1, {1, 0});
433 }
434 dstImg[param->indexFn(pos + offs0, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist1 / (dist0 + dist1);
435 dstImg[param->indexFn(pos + offs1, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist0 / (dist0 + dist1);
436 }
437 } while (keepDiffusing);
438 }
439
440 if (valveQuirks) {
441 *valveQuirks = false;
442
443 auto blackOutAndWarn = [&](auto oi) {
444 float& px = dstImg[oi * dstPxLen + dstAlphaOffs];
445 if (fabs(px) > 0.000001f) {
446 *valveQuirks = true;
447 }
448 px = 0.0f;
449 };
450
451 for (auto x = 0; x < dstWidth; x++) {
452 blackOutAndWarn(x);
453 blackOutAndWarn(x + (dstHeight - 1) * dstWidth);
454 }
455 for (auto y = 0; y < dstHeight; y++) {
456 blackOutAndWarn(y * dstWidth);
457 blackOutAndWarn(y * dstWidth + dstWidth - 1);
458 }
459 }
460}
461
462bool operator!(Flags flags) {
463 return flags == Flags::NONE;
464};
465
466bool isSingleChannel(ImageFormat format) {
467 using namespace ImageFormatDetails;
468 return red(format) == bpp(format) && bpp(format);
469}
470
471} // namespace
472} // namespace vtfpp::DistanceMapping::detail
473
474[[nodiscard]] std::vector<std::byte> DistanceMapping::alphaToDistance(std::span<const std::byte> imageData, ImageFormat inFormat, ImageFormat outFormat, uint16_t width, uint16_t height, uint16_t reduceX, uint16_t reduceY, bool srgb, float distanceSpread, float alphaThreshold, DistanceMapping::Flags flags, DistanceMapping::Dither dither, ImageConversion::ResizeFilter filter, ImageConversion::ResizeEdge edge, bool *valveQuirks) {
475 using namespace vtfpp::DistanceMapping::detail;
476 using namespace ImageFormatDetails;
477
478 auto mkFormat = [](ImageFormat format) {
479 return isSingleChannel(format) ?
480 std::make_tuple(ImageFormat::R32F, uint8_t{1}, uint8_t{0})
481 : (decompressedAlpha(format) || alpha(format)) ?
482 std::make_tuple(ImageFormat::RGBA32323232F, uint8_t{4}, uint8_t{3})
483 :
484 std::make_tuple(ImageFormat::EMPTY, uint8_t{0}, uint8_t{0});
485 };
486 auto [intermediateRead, srcPxLen, srcAlphaOffs] = mkFormat(inFormat);
487 auto [intermediateWrite, dstPxLen, dstAlphaOffs] = mkFormat(outFormat);
488
489 if (
490 intermediateRead == ImageFormat::EMPTY
491 || intermediateWrite == ImageFormat::EMPTY
492 || !math::isPowerOf2(reduceX)
493 || !math::isPowerOf2(reduceY)
494 || ftoabsu16(distanceSpread * reduceX) < 1
495 || ftoabsu16(distanceSpread * reduceY) < 1
496 || std::clamp(alphaThreshold, 0.0f, 1.0f) != alphaThreshold
498 ) {
499 return {};
500 }
501
502 bool inputNeedsConversion = intermediateRead != inFormat;
503 std::vector<std::byte> i_love_raii = inputNeedsConversion ? convertImageDataToFormat(imageData, inFormat, intermediateRead, width, height) : std::vector<std::byte>(0);
504 std::span<const std::byte> srcImg = inputNeedsConversion ? i_love_raii : imageData;
505
506 SampleParam param(reinterpret_cast<const float *>(srcImg.data()), width, height, reduceX, reduceY, srcPxLen, srcAlphaOffs, distanceSpread, !!(flags & Flags::SAMPLECENTERED), alphaThreshold, edge);
507
508 uint16_t dstWidth = width / reduceX, dstHeight = height / reduceY;
509
510 std::vector<std::byte> dstImg
511 = (!isSingleChannel(inFormat) && !isSingleChannel(outFormat)) ?
513 resizeImageData(imageData, inFormat, width, dstWidth, height, dstHeight, srgb, true, filter, edge),
514 inFormat, intermediateWrite, dstWidth, dstHeight
515 )
516 :
517 std::vector<std::byte>(sizeof(float) * dstPxLen * dstWidth * dstHeight, std::byte{0x00});
518
519 float quantum = 1.0f / static_cast<float>(1 << decompressedAlpha(outFormat));
520 bool doGradientDither = dither == Dither::GRADIENT_TANGENT && !decimal(outFormat);
521 paintMap(
522 &param,
523 reinterpret_cast<float *>(dstImg.data()),
524 dstPxLen,
525 dstAlphaOffs,
526 !!(flags & Flags::DISTANCEAA),
527 !!(flags & Flags::EUCLIDEAN),
528 doGradientDither ? &quantum : nullptr,
529 valveQuirks
530 );
531
532 return (outFormat == intermediateWrite) ? dstImg : convertImageDataToFormat(dstImg, intermediateWrite, outFormat, dstWidth, dstHeight);
533}
constexpr bool isPowerOf2(std::unsigned_integral auto n)
Definition Math.h:46
constexpr auto pi_f32
Definition Math.h:30
@ GRADIENT_TANGENT
Experimental dithering approach that diffuses quantization error perpendicular to the distance gradie...
std::vector< std::byte > alphaToDistance(std::span< const std::byte > imageData, ImageFormat inFormat, ImageFormat outFormat, uint16_t width, uint16_t height, uint16_t reduceX, uint16_t reduceY, bool srgb, float distanceSpread=1.f, float alphaThreshold=0.04f, Flags flags=Flags::NONE, Dither dither=Dither::NONE, ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::NICE, ImageConversion::ResizeEdge edge=ImageConversion::ResizeEdge::CLAMP, bool *valveQuirks=nullptr)
In one operation, convert an image's alpha channel, or, for single-channel formats,...
@ SAMPLECENTERED
Search from the center of pixels (in destination coordinate space) rather than in their north-west co...
@ EUCLIDEAN
The distance-mapping algorithm is a brute-force scan of a square area. If this is enabled,...
@ DISTANCEAA
Experimental; interpret the alpha channel as antialiased. Can result in a more precise distance map,...
std::vector< std::byte > convertImageDataToFormat(std::span< const std::byte > imageData, ImageFormat oldFormat, ImageFormat newFormat, uint16_t width, uint16_t height, float quality=DEFAULT_COMPRESSED_QUALITY)
Converts an image from one format to another.
std::vector< std::byte > resizeImageData(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t newWidth, uint16_t height, uint16_t newHeight, bool srgb, bool premultipliedAlpha, ResizeFilter filter, ResizeEdge edge=ResizeEdge::CLAMP)
Resize given image data to the new dimensions.