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
19uint16_t ftoabsu16(float f) {
20 return static_cast<uint16_t>(std::clamp<uint16_t>(static_cast<int32_t>(fabs(f)), 0, UINT16_MAX));
21}
22
23// literally just stb. grep '_full( int n' in stb_image_resize2.h
24int32_t edgeClamp(int32_t n, int32_t max) {
25 if (n < 0) {
26 return 0;
27 }
28 return (n >= max) ? max - 1 : n;
29}
30int32_t edgeReflect(int32_t n, int32_t max) {
31 if (n < 0) {
32 return n > -max ? -n : max - 1;
33 }
34 if (n >= max) {
35 const int32_t max2 = 2 * max;
36 return n >= max2 ? 0 : max2 - n - 1;
37 }
38 return n;
39}
40int32_t edgeWrap(int32_t n, int32_t max) {
41 if (n >= 0) {
42 return n % max;
43 }
44 const int32_t m = (-n) % max;
45 return m ? max - m : 0;
46}
47
48int32_t indexWith(ImageConversion::ResizeEdge edge, Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs )
49{
50 switch (edge) {
51 default:
52 break;
54 return (edgeReflect(v[1], h) * w + edgeReflect(v[0], w)) * pxLen + alphaOffs;
56 return (edgeWrap(v[1], h) * w + edgeWrap(v[0], w)) * pxLen + alphaOffs;
57 }
58 return (edgeClamp(v[1], h) * w + edgeClamp(v[0], w)) * pxLen + alphaOffs;
59}
60
61// big ugly immutable record of everything pertaining to how we read alpha from an image
62class SampleParam {
63public:
64 SampleParam(
65 const float *srcImg,
66 uint16_t inWidth,
67 uint16_t inHeight,
68 uint16_t reduceX,
69 uint16_t reduceY,
70 uint8_t pxLen,
71 uint8_t alphaOffs,
72 float distanceSpread,
73 bool sampleCentered,
74 float alphaThreshold,
76 )
77 : srcImg(srcImg)
78 , pxLen(pxLen)
79 , alphaOffs(alphaOffs)
80 , imgWidth(inWidth)
81 , imgHeight(inHeight)
82 , reduceX(reduceX)
83 , reduceY(reduceY)
84 , searchRadius(ftoabsu16(2.0f * std::max<float>(reduceX, reduceY) * distanceSpread))
85 , offsX(sampleCentered ? reduceX / 2 : 0)
86 , offsY(sampleCentered ? reduceY / 2 : 0)
87 , alphaThreshold(alphaThreshold)
88 , edge(edge)
89 {}
90
91 // n.b.: "padded" by search radius so the distance transform can read past
92 // the edges in accordance with the edge policy
93 float samplePadded(Vec2i v) const {
94 return this->srcImg[indexWith(this->edge, v - this->searchRadius, this->imgWidth, this->imgHeight, this->pxLen, this->alphaOffs)];
95 }
96
97 bool isInPadded(Vec2i v) const {
98 return this->samplePadded(v) >= this->alphaThreshold;
99 }
100
101 const float *const srcImg;
102 const float alphaThreshold;
103 const uint16_t imgHeight;
104 const uint16_t imgWidth;
105 const uint16_t reduceX, reduceY;
106 const uint16_t searchRadius;
107 const uint16_t offsX, offsY;
108 const uint8_t pxLen;
109 const uint8_t alphaOffs;
111};
112
113float vecDist(Vec2f32 v0, Vec2f32 v1) {
114 return std::hypot(v0[0] - v1[0], v0[1] - v1[1]);
115}
116
117using math::pi_f32;
118
119static constexpr float BIG_EPSILON = 0.05f;
120static constexpr float SMALL_EPSILON = 1e-6f;
121
122// scuffed at sharp angled points but uh. horseshoes, government work, etc
123float aaOffsetPadded(const SampleParam *param, Vec2i v, bool outside) {
124 const float alpha = param->samplePadded(v);
125 const float reach = outside ? 1.0f : 0.5f;
126 float slope = 0.0f;
127 for (Vec2i offset : {Vec2i{-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {-1, 1}, {0, 1}, {1, 1}}) {
128 slope = fmax(slope, fabs(param->samplePadded(v + offset) - alpha) / std::hypot<float>(offset[0], offset[1]));
129 }
130 const float gap = fabs(alpha - 0.5f);
131 if (slope < SMALL_EPSILON || gap > reach * slope) {
132 return outside ? 0.0f : -1.0f;
133 }
134 return fmin(gap / slope, reach);
135}
136
137template<typename T>
138T sq(T f) {
139 return f * f;
140}
141
142static constexpr float inff = std::numeric_limits<float>::infinity();
143static constexpr double infd = std::numeric_limits<double>::infinity();
144
145// Distance Transforms of Sampled Functions
146// Pedro F. Felzenszwalb, Daniel P. Huttenlocher.
147// https://cs.brown.edu/people/pfelzens/papers/dt-final.pdf
148//
149// mostly just the 1D transform on page 6 (420) but:
150// - optionally report argmin of the envelope bc aa/dither need pixel positions
151// - this still isn't perfect information on its own, but *is* enough to
152// determine whether whipping out the ol raster scan is necessary
153// - cut off outside a particular window
154void distanceTransform1D(
155 const float *f,
156 int32_t n,
157 float *d,
158 int32_t *argmin,
159 int32_t window,
160 int32_t *v,
161 double *z,
162 int32_t first,
163 int32_t stride
164) {
165 int32_t q0 = 0;
166 while (q0 < n && std::isinf(f[q0])) {
167 q0++;
168 }
169 if (q0 == n) {
170 const int32_t m = (n - first + stride - 1) / stride;
171 std::fill(d, d + m, inff);
172 if (argmin) {
173 std::fill(argmin, argmin + m, -1);
174 }
175 return;
176 }
177
178 int32_t k = 0;
179 v[0] = q0;
180 z[0] = -infd;
181 z[1] = +infd;
182 for (int32_t q = q0 + 1; q < n; q++) {
183 if (std::isinf(f[q])) {
184 continue;
185 }
186 six: // i love academic pseudo algol very cool
187 const double s = ((f[q] + sq<double>(q)) - (f[v[k]] + sq<double>(v[k]))) / (2.0 * (q - v[k]));
188 if (s <= z[k]) {
189 k--;
190 goto six;
191 }
192 k++;
193 v[k] = q;
194 z[k] = s;
195 z[k + 1] = infd;
196 }
197
198 k = 0;
199 for (int32_t q = first, j = 0; q < n; q += stride, j++) {
200 while (z[k + 1] < q) {
201 k++;
202 }
203 const bool out = v[k] < q - window || v[k] > q + window - 1;
204 d[j] = out ? inff : f[v[k]] + sq<float>(q - v[k]);
205 if (argmin) {
206 argmin[j] = out ? -1 : v[k];
207 }
208 }
209}
210
211void paintMap(
212 const SampleParam *param,
213 float *dstImg,
214 uint8_t dstPxLen,
215 uint8_t dstAlphaOffs,
216 bool distanceAA,
217 const float *tangentDiffusion,
218 bool *valveQuirks
219) {
220 const auto [padW, padH] = (math::Vec2ui{param->imgWidth, param->imgHeight} + 2 * param->searchRadius).values;
221 std::vector<uint8_t> mask(padW * padH); // vector<bool> was slower dont @ me
222 for (uint32_t padY = 0; padY < padH; padY++) {
223 for (uint32_t padX = 0; padX < padW; padX++) {
224 mask[padX * padH + padY] = param->isInPadded(Vec2i{padX, padY});
225 }
226 }
227
228 float fRadius = static_cast<float>(param->searchRadius);
229 uint16_t dstWidth = param->imgWidth / param->reduceX;
230 uint16_t dstHeight = param->imgHeight / param->reduceY;
231
232 std::vector<float> gradients(tangentDiffusion ? static_cast<size_t>(dstWidth) * dstHeight : 0, -pi_f32 - BIG_EPSILON);
233
234 const bool needArgmin = tangentDiffusion || distanceAA;
235 const size_t padMax = std::max(padW, padH);
236 std::vector<float> df2D(padW * dstHeight);
237 std::vector<float> df1D(padMax);
238 std::vector<int32_t> argmin2D(needArgmin ? padW * dstHeight : 0);
239 std::vector<int32_t> argmin1D(padMax);
240 std::vector<float> col(padH);
241 std::vector<int32_t> seedTally(needArgmin ? padW + 1 : 0);
242 std::vector<int32_t> v(padMax);
243 std::vector<double> z(padMax + 1);
244
245 for (uint8_t seed : {1, 0}) {
246 for (uint32_t padX = 0; padX < padW; padX++) {
247 for (uint32_t padY = 0; padY < padH; padY++) {
248 col[padY] = mask[padX * padH + padY] == seed ? 0.0f : inff;
249 }
250 distanceTransform1D(
251 col.data(), padH, df1D.data(),
252 needArgmin ? argmin1D.data() : nullptr,
253 param->searchRadius,
254 v.data(), z.data(),
255 param->offsY + param->searchRadius, param->reduceY
256 );
257 for (uint32_t dstY = 0; dstY < dstHeight; dstY++) {
258 df2D[dstY * padW + padX] = df1D[dstY];
259 if (needArgmin) {
260 argmin2D[dstY * padW + padX] = argmin1D[dstY];
261 }
262 }
263 }
264
265 for (uint32_t dstY = 0; dstY < dstHeight; dstY++) {
266 const int32_t padY = dstY * param->reduceY + param->offsY + param->searchRadius;
267 distanceTransform1D(
268 df2D.data() + dstY * padW, padW, df1D.data(),
269 needArgmin ? argmin1D.data() : nullptr,
270 padW,
271 v.data(), z.data(),
272 param->offsX + param->searchRadius, param->reduceX
273 );
274
275 // running count for quick ez checks below; cf. seedInWindow
276 if (needArgmin) {
277 seedTally[0] = 0;
278 for (uint32_t c = 0; c < padW; c++) {
279 seedTally[c + 1] = seedTally[c] + !std::isinf(df2D[dstY * padW + c]);
280 }
281 }
282 for (uint32_t dstX = 0; dstX < dstWidth; dstX++) {
283 const int32_t padX = dstX * param->reduceX + param->offsX + param->searchRadius;
284 if (mask[padX * padH + padY] == seed) {
285 continue;
286 }
287 const int32_t windowStart = padX - param->searchRadius;
288 const int32_t windowEnd = padX + param->searchRadius;
289 const bool seedInWindow = needArgmin && seedTally[windowEnd] > seedTally[windowStart];
290 float nearest = sqrtf(df1D[dstX]);
291 int32_t nearestX = seedInWindow ? argmin1D[dstX] : -1;
292 if (seedInWindow && nearest >= fRadius) {
293 float nearest2 = std::numeric_limits<float>::max();
294 int32_t windowCol = -1;
295 for (int32_t c = windowStart; c < windowEnd; c++) {
296 if (const float dist2 = df2D[dstY * padW + c] + sq<float>(padX - c); dist2 < nearest2) {
297 nearest2 = dist2;
298 windowCol = c;
299 }
300 }
301 if (windowCol >= 0) {
302 nearest = sqrtf(nearest2);
303 nearestX = windowCol;
304 }
305 }
306 int32_t nearestY = -1;
307 if (distanceAA) {
308 if (seedInWindow) {
309 const float outer = nearest + 1;
310 const float outer2 = sq(outer);
311 const float inner2 = sq(nearest) - 1;
312 const int32_t loX = -std::min<int32_t>(outer, param->searchRadius);
313 const int32_t hiX = +std::min<int32_t>(outer + 1, param->searchRadius);
314 const float r2 = sq(param->searchRadius);
315 float preciseNearest = inff;
316
317 for (int32_t dX = loX; dX < hiX; dX++) {
318 const float dX2 = sq<float>(dX);
319 const float remHi = outer2 - dX2;
320 if (remHi < 0.0f || dX2 + r2 < inner2) {
321 continue;
322 }
323 const int32_t hi = sqrtf(remHi);
324 const int32_t lo = inner2 > dX2 ? ceilf(sqrtf(inner2 - dX2)) : 0;
325 const int32_t aaX = padX + dX;
326 const int32_t loY = -std::min<int32_t>(hi, param->searchRadius);
327 const int32_t hiY = +std::min<int32_t>(hi + 1, param->searchRadius);
328 for (int32_t dY = loY; dY < hiY; dY++) {
329 if (-lo < dY && dY < lo) {
330 dY = lo - 1;
331 continue;
332 }
333 const int32_t aaY = padY + dY;
334 if (mask[aaX * padH + aaY] != seed) {
335 continue;
336 }
337 // it would maybe make more "sense" to just say
338 // aa means you should set threshold 0.5, but
339 // threshold is useful to keep independent,
340 // because image editors are stupid and love
341 // discarding the rgb values of transparent
342 // pixels. even tgas. and as long as that stays
343 // the case, we want to enable the trick of
344 // exporting with a background at 1/255 alpha
345 // to force the image to retain rgb there.
346 // btw if you didn't know that trick and this
347 // is how you find out, you're welcome :^)
348 float offsSeed = aaOffsetPadded(param, {aaX, aaY}, true);
349 if (seed && param->samplePadded({aaX, aaY}) < 0.5f) {
350 offsSeed = -offsSeed;
351 }
352 const float preciseDist = static_cast<float>(std::hypot(dX, dY)) - offsSeed;
353 if (preciseDist < preciseNearest) {
354 preciseNearest = preciseDist;
355 nearestX = aaX;
356 nearestY = aaY;
357 }
358 }
359 }
360 if (preciseNearest < inff) {
361 nearest = preciseNearest;
362 }
363 }
364 const float offsRef = aaOffsetPadded(param, {padX, padY}, false);
365 if (offsRef >= 0.0f) {
366 nearest = fmin(nearest, offsRef);
367 }
368 }
369
370 if (nearestX >= 0 && nearestY < 0) {
371 nearestY = argmin2D[dstY * padW + nearestX];
372 }
373 nearest = fmax(nearest, 0.0f);
374 nearest = fmin(0.5f, nearest * 0.5f / fRadius);
375 if (seed) {
376 nearest = -nearest;
377 }
378
379 const uint32_t iDst = dstY * dstWidth + dstX;
380 dstImg[iDst * dstPxLen + dstAlphaOffs] = 0.5 + nearest;
381 if (tangentDiffusion) {
382 gradients[iDst] = nearestX >= 0 ? atan2(nearestY - padY, nearestX - padX) : (-pi_f32 - BIG_EPSILON);
383 }
384 }
385 }
386 }
387
388 if (tangentDiffusion) {
389 // attempt to diffuse quantization error tangent to the gradient of the
390 // distance map. you could imagine some approach that involves grouping
391 // greyscale pixels into distinct contours, and playing ring around the
392 // rosy for each group, but there's no practical way to avoid littering
393 // residual bits of error into pixels you've already quantized, ruining
394 // the whole point. the usual suspects like floyd-steinberg use kernels
395 // shaped to only write *ahead* of a conventional raster scan. here, we
396 // do the same by determining a shape, bounded by the following "mask":
397 //
398 // ⎡ 0 0 0 ⎤
399 // ⎢ 0 0 1 ⎥
400 // ⎣ 1 1 1 ⎦,
401 //
402 // mapping adjacent pixels to the unit circle's bounding box like this:
403 //
404 // ⎡ NW:(-1, 1) N:(0, 1) NE:(1, 1) ⎤
405 // ⎢ W:(-1, 0) E:(1, 0) ⎥
406 // ⎣ SW:(-1, -1) S:(0, -1) SE:(1, -1) ⎦,
407 //
408 // choose the two points nearest (x, y) on the unit circle at θ + π / 2
409 // (i.e. the right-hand normal of the gradient direction θ). distribute
410 // the quantization error E between the two, weighted by the opposite's
411 // distance over the sum of both distances (it feels as though a better
412 // weighting should exist but that's what i get for dropping out), then
413 // fold the whole thing over to only entries ahead of us in scan order:
414 //
415 // ⎡ ⎤
416 // k = ⎢ E+W ⎥
417 // ⎣ SW+NE S+N SE+NW ⎦,
418 //
419 // yielding our per-destination-pixel kernel k. a neat property of this
420 // is that we can ignore pixels that aren't within radius of one of the
421 // parabolas (and thus, never set any angle for): we're diffusing error
422 // tangent to the gradient, and that way lie only pixels where we would
423 // also find a parabola. we can't index angles *independently* anymore,
424 // but oh well who cares. at least we can still commit to this terrible
425 // bit of torturing every single line of this explanation to 68 columns
426 static constexpr std::array<const int8_t, 8> hardSigns{0, 1, 1, 1, 0, -1, -1, -1};
427 // of course, in our case, y grows downward, so we really want is E..NW
428 // which, conveniently enough, is just 0..3, so no aux lookup table. if
429 // we wanted SW..E, we'd index into {0, 5, 6, 7}. the pixel offsets are
430 // the only result where we need to be picky about the final signs; the
431 // preceding calculations can run in our flipped coordinates just fine.
432 static constexpr float eighth = pi_f32 / 4.0f;
433
434 const double quantum = *tangentDiffusion;
435 for (uint32_t diffY = 0; diffY < dstHeight; diffY++) {
436 for (uint32_t diffX = 0; diffX < dstWidth; diffX++) {
437 const float theta = gradients[diffY * dstWidth + diffX];
438 if (theta < -pi_f32 - BIG_EPSILON / 2.0f) {
439 continue;
440 }
441 float thetaNormal = fmod(theta + 2.5f * pi_f32, 2.0f * pi_f32);
442 uint8_t nEighths = static_cast<uint32_t>(std::floor(thetaNormal / eighth)) % 8;
443 Vec2i pos{diffX, diffY};
444 uint32_t iDiff = indexWith(param->edge, pos, dstWidth, dstHeight, dstPxLen, dstAlphaOffs);
445 double curPx = dstImg[iDiff];
446
447 double quantized = std::round(curPx / quantum) * quantum;
448 float quantErr = curPx - quantized;
449 dstImg[iDiff] = quantized;
450 bool atRight = diffX + 1 >= dstWidth;
451 bool bottomEdge = pos[1] >= dstHeight - 1;
452
453 if (atRight && bottomEdge) {
454 break;
455 }
456 Vec2f32 normal{cos(thetaNormal), sin(thetaNormal)};
457 // distance calculation uses the whole circle
458 float dist0 = vecDist(normal, {hardSigns[(nEighths + 2) % 8], hardSigns[(nEighths + 0) % 8]});
459 float dist1 = vecDist(normal, {hardSigns[(nEighths + 3) % 8], hardSigns[(nEighths + 1) % 8]});
460 // offset selection uses the part of the circle that is yet to be scanned
461 Vec2i offs0{hardSigns[(nEighths + 0) % 4 + 2], bottomEdge ? 0 : hardSigns[(nEighths + 0) % 4]};
462 Vec2i offs1{hardSigns[(nEighths + 1) % 4 + 2], bottomEdge ? 0 : hardSigns[(nEighths + 1) % 4]};
463
464 dstImg[indexWith(param->edge, pos + offs0, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist1 / (dist0 + dist1);
465 dstImg[indexWith(param->edge, pos + offs1, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist0 / (dist0 + dist1);
466 }
467 }
468 }
469
470 if (valveQuirks) {
471 *valveQuirks = false;
472
473 auto blackOutAndWarn = [&](auto oi) {
474 float& px = dstImg[oi * dstPxLen + dstAlphaOffs];
475 if (fabs(px) > SMALL_EPSILON) {
476 *valveQuirks = true;
477 }
478 px = 0.0f;
479 };
480
481 for (auto x = 0; x < dstWidth; x++) {
482 blackOutAndWarn(x);
483 blackOutAndWarn(x + (dstHeight - 1) * dstWidth);
484 }
485 for (auto y = 0; y < dstHeight; y++) {
486 blackOutAndWarn(y * dstWidth);
487 blackOutAndWarn(y * dstWidth + dstWidth - 1);
488 }
489 }
490}
491
492bool operator!(Flags flags) {
493 return flags == Flags::NONE;
494};
495
496bool isSingleChannel(ImageFormat format) {
497 using namespace ImageFormatDetails;
498 return red(format) == bpp(format) && bpp(format);
499}
500
501} // namespace
502} // namespace vtfpp::DistanceMapping::detail
503
504[[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) {
505 using namespace vtfpp::DistanceMapping::detail;
506 using namespace ImageFormatDetails;
507
508 auto mkFormat = [](ImageFormat format) {
509 return isSingleChannel(format) ?
510 std::make_tuple(ImageFormat::R32F, uint8_t{1}, uint8_t{0})
511 : (decompressedAlpha(format) || alpha(format)) ?
512 std::make_tuple(ImageFormat::RGBA32323232F, uint8_t{4}, uint8_t{3})
513 :
514 std::make_tuple(ImageFormat::EMPTY, uint8_t{0}, uint8_t{0});
515 };
516 auto [intermediateRead, srcPxLen, srcAlphaOffs] = mkFormat(inFormat);
517 auto [intermediateWrite, dstPxLen, dstAlphaOffs] = mkFormat(outFormat);
518
519 if (
520 intermediateRead == ImageFormat::EMPTY
521 || intermediateWrite == ImageFormat::EMPTY
522 || !math::isPowerOf2(reduceX)
523 || !math::isPowerOf2(reduceY)
524 || ftoabsu16(distanceSpread * reduceX) < 1
525 || ftoabsu16(distanceSpread * reduceY) < 1
526 || std::clamp(alphaThreshold, 0.0f, 1.0f) != alphaThreshold
528 ) {
529 return {};
530 }
531
532 bool inputNeedsConversion = intermediateRead != inFormat;
533 std::vector<std::byte> i_love_raii = inputNeedsConversion ? convertImageDataToFormat(imageData, inFormat, intermediateRead, width, height) : std::vector<std::byte>(0);
534 std::span<const std::byte> srcImg = inputNeedsConversion ? i_love_raii : imageData;
535
536 SampleParam param(reinterpret_cast<const float *>(srcImg.data()), width, height, reduceX, reduceY, srcPxLen, srcAlphaOffs, distanceSpread, !!(flags & Flags::SAMPLECENTERED), alphaThreshold, edge);
537
538 uint16_t dstWidth = width / reduceX, dstHeight = height / reduceY;
539
540 std::vector<std::byte> dstImg
541 = (!isSingleChannel(inFormat) && !isSingleChannel(outFormat)) ?
543 resizeImageData(imageData, inFormat, width, dstWidth, height, dstHeight, srgb, true, filter, edge),
544 inFormat, intermediateWrite, dstWidth, dstHeight
545 )
546 :
547 std::vector<std::byte>(sizeof(float) * dstPxLen * dstWidth * dstHeight, std::byte{0x00});
548
549 int8_t alphaBits = decompressedAlpha(outFormat);
550 float quantum = (alphaBits > 0) ? 1.0f / static_cast<float>((1 << alphaBits) - 1) : 1.0f;
551 bool doGradientDither = dither == Dither::GRADIENT_TANGENT && !decimal(outFormat);
552 paintMap(
553 &param,
554 reinterpret_cast<float *>(dstImg.data()),
555 dstPxLen,
556 dstAlphaOffs,
557 !!(flags & Flags::DISTANCEAA),
558 doGradientDither ? &quantum : nullptr,
559 valveQuirks
560 );
561
562 return (outFormat == intermediateWrite) ? dstImg : convertImageDataToFormat(dstImg, intermediateWrite, outFormat, dstWidth, dstHeight);
563}
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...
@ 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.