Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
FontRuntime.cpp
Go to the documentation of this file.
1#include "FontRuntime.hpp"
2#include <SDL3/SDL.h>
3
4#include <algorithm>
5#include <cstring>
6#include <limits>
7#include <system_error>
8#include <utility>
9
10namespace monopoly::fonts
11{
12 namespace
13 {
14 [[nodiscard]] Error makeError(ErrorCode code,
15 std::filesystem::path path, std::string detail)
16 {
17 return {code, std::move(path), std::move(detail)};
18 }
19
20 [[nodiscard]] std::string utf8Path(const std::filesystem::path& path)
21 {
22 const auto value = path.u8string();
23 return {reinterpret_cast<const char*>(value.data()), value.size()};
24 }
25
26 [[nodiscard]] TTF_FontStyleFlags styleFlags(const Settings& settings) noexcept
27 {
29 if (settings.weight >= 600) result |= TTF_STYLE_BOLD;
30 if (settings.italic) result |= TTF_STYLE_ITALIC;
31 if (settings.underline) result |= TTF_STYLE_UNDERLINE;
32 if (settings.strikeOut) result |= TTF_STYLE_STRIKETHROUGH;
33 return result;
34 }
35
36 [[nodiscard]] std::expected<std::string, Error> utf16ToUtf8(
37 std::u16string_view text, const std::filesystem::path& fontPath)
38 {
39 std::string result;
40 result.reserve(text.size() * 3U);
41 const auto append = [&](std::uint32_t codePoint)
42 {
43 if (codePoint <= 0x7FU)
44 result.push_back(static_cast<char>(codePoint));
45 else if (codePoint <= 0x7FFU)
46 {
47 result.push_back(static_cast<char>(0xC0U | (codePoint >> 6U)));
48 result.push_back(static_cast<char>(0x80U | (codePoint & 0x3FU)));
49 }
50 else if (codePoint <= 0xFFFFU)
51 {
52 result.push_back(static_cast<char>(0xE0U | (codePoint >> 12U)));
53 result.push_back(static_cast<char>(0x80U | ((codePoint >> 6U) & 0x3FU)));
54 result.push_back(static_cast<char>(0x80U | (codePoint & 0x3FU)));
55 }
56 else
57 {
58 result.push_back(static_cast<char>(0xF0U | (codePoint >> 18U)));
59 result.push_back(static_cast<char>(0x80U | ((codePoint >> 12U) & 0x3FU)));
60 result.push_back(static_cast<char>(0x80U | ((codePoint >> 6U) & 0x3FU)));
61 result.push_back(static_cast<char>(0x80U | (codePoint & 0x3FU)));
62 }
63 };
64
65 for (std::size_t index = 0; index < text.size(); ++index)
66 {
67 const auto value = static_cast<std::uint32_t>(text[index]);
68 if (value >= 0xD800U && value <= 0xDBFFU)
69 {
70 if (index + 1 >= text.size())
71 return std::unexpected(makeError(ErrorCode::InvalidTextEncoding,
72 fontPath, "truncated UTF-16 surrogate pair"));
73 const auto low = static_cast<std::uint32_t>(text[++index]);
74 if (low < 0xDC00U || low > 0xDFFFU)
75 return std::unexpected(makeError(ErrorCode::InvalidTextEncoding,
76 fontPath, "invalid UTF-16 surrogate pair"));
77 append(0x10000U + ((value - 0xD800U) << 10U) + (low - 0xDC00U));
78 continue;
79 }
80 if (value >= 0xDC00U && value <= 0xDFFFU)
81 return std::unexpected(makeError(ErrorCode::InvalidTextEncoding,
82 fontPath, "isolated UTF-16 low surrogate"));
84 }
85 return result;
86 }
87
88 [[nodiscard]] data::LegacyBitmapRGBA8 clipBitmap(
89 const data::LegacyBitmapRGBA8& source, ClipRect clip)
90 {
91 data::LegacyBitmapRGBA8 result;
92 if (clip.x >= source.width || clip.y >= source.height ||
93 clip.width == 0 || clip.height == 0)
94 return result;
95
96 result.width = std::min(clip.width, source.width - clip.x);
97 result.height = std::min(clip.height, source.height - clip.y);
98 result.pixels.resize(static_cast<std::size_t>(result.width) *
99 result.height * 4U);
100
101 const auto sourceStride = static_cast<std::size_t>(source.width) * 4U;
102 const auto destinationStride = static_cast<std::size_t>(result.width) * 4U;
103 for (std::uint32_t y = 0; y < result.height; ++y)
104 {
105 const auto sourceOffset =
106 (static_cast<std::size_t>(clip.y + y) * sourceStride) +
107 (static_cast<std::size_t>(clip.x) * 4U);
108 std::memcpy(result.pixels.data() +
109 static_cast<std::size_t>(y) * destinationStride,
110 source.pixels.data() + sourceOffset, destinationStride);
111 }
112 return result;
113 }
114
115 [[nodiscard]] bool sameAsciiName(std::u8string_view left,
116 std::u8string_view right) noexcept
117 {
118 if (left.size() != right.size()) return false;
119 for (std::size_t i = 0; i < left.size(); ++i)
120 {
121 auto a = left[i];
122 auto b = right[i];
123 if (a >= u8'A' && a <= u8'Z') a += u8'a' - u8'A';
124 if (b >= u8'A' && b <= u8'Z') b += u8'a' - u8'A';
125 if (a != b) return false;
126 }
127 return true;
128 }
129 }
130
131 std::expected<std::string, Error> transcodeUtf8(std::u16string_view text)
132 {
133 return utf16ToUtf8(text, {});
134 }
135
136 std::expected<std::string, Error> transcodeUtf8(std::wstring_view text)
137 {
138 if constexpr (sizeof(wchar_t) == 2)
139 {
140 std::u16string utf16;
141 utf16.reserve(text.size());
142 for (const auto value : text)
143 utf16.push_back(static_cast<char16_t>(value));
144 return transcodeUtf8(utf16);
145 }
146 else
147 {
148 std::string result;
149 result.reserve(text.size() * 4U);
150 const auto append = [&](std::uint32_t codePoint)
151 {
152 if (codePoint <= 0x7FU)
153 result.push_back(static_cast<char>(codePoint));
154 else if (codePoint <= 0x7FFU)
155 {
156 result.push_back(static_cast<char>(0xC0U | (codePoint >> 6U)));
157 result.push_back(static_cast<char>(0x80U | (codePoint & 0x3FU)));
158 }
159 else if (codePoint <= 0xFFFFU)
160 {
161 result.push_back(static_cast<char>(0xE0U | (codePoint >> 12U)));
162 result.push_back(static_cast<char>(0x80U | ((codePoint >> 6U) & 0x3FU)));
163 result.push_back(static_cast<char>(0x80U | (codePoint & 0x3FU)));
164 }
165 else
166 {
167 result.push_back(static_cast<char>(0xF0U | (codePoint >> 18U)));
168 result.push_back(static_cast<char>(0x80U | ((codePoint >> 12U) & 0x3FU)));
169 result.push_back(static_cast<char>(0x80U | ((codePoint >> 6U) & 0x3FU)));
170 result.push_back(static_cast<char>(0x80U | (codePoint & 0x3FU)));
171 }
172 };
173 for (const auto value : text)
174 {
175 const auto codePoint = static_cast<std::uint32_t>(value);
176 if (codePoint > 0x10FFFFU ||
177 (codePoint >= 0xD800U && codePoint <= 0xDFFFU))
178 return std::unexpected(makeError(ErrorCode::InvalidTextEncoding, {},
179 "invalid wide-character Unicode code point"));
181 }
182 return result;
183 }
184 }
185
187 {
188 ttfInitialized_ = TTF_Init();
189 }
190
192 {
193 clearPresentationFaces();
194 if (font_) TTF_CloseFont(font_);
195 if (ttfInitialized_) TTF_Quit();
196 }
197
199 {
200 return ttfInitialized_ && font_ != nullptr;
201 }
202
204 {
205 return settings_;
206 }
207
208 std::expected<void, Error> Runtime::reopen()
209 {
210 if (!ttfInitialized_)
211 return std::unexpected(makeError(ErrorCode::TtfInitFailed, {}, SDL_GetError()));
212 if (settings_.fontPath.empty())
213 return std::unexpected(makeError(ErrorCode::FontNotFound, {},
214 "no font path has been selected"));
215
216 const auto path = utf8Path(settings_.fontPath);
217 const SDL_PropertiesID properties = SDL_CreateProperties();
218 if (!properties)
219 return std::unexpected(makeError(ErrorCode::FontOpenFailed,
220 settings_.fontPath, SDL_GetError()));
221 const bool configured =
224 static_cast<float>(settings_.size)) &&
227 TTF_Font* replacement = configured ? TTF_OpenFontWithProperties(properties) : nullptr;
228 SDL_DestroyProperties(properties);
229 if (!replacement)
230 return std::unexpected(makeError(ErrorCode::FontOpenFailed,
231 settings_.fontPath, SDL_GetError()));
232
233 TTF_SetFontStyle(replacement, styleFlags(settings_));
234 // Successful replacement establishes a new loaded-face generation.
235 clearPresentationFaces();
236 if (font_) TTF_CloseFont(font_);
237 font_ = replacement;
238 return {};
239 }
240
241 std::expected<void, Error> Runtime::setFont(
242 const std::filesystem::path& path, std::string familyName)
243 {
244 const auto previous = settings_;
245 settings_.fontPath = path;
246 settings_.familyName = std::move(familyName);
247 if (auto result = reopen(); !result)
248 {
249 settings_ = previous;
250 return result;
251 }
252 return {};
253 }
254
255 std::expected<void, Error> Runtime::setSize(int points)
256 {
258 return std::unexpected(makeError(ErrorCode::InvalidSize, {},
259 "font size must be between 1 and 512 points"));
260
261 const int previous = settings_.size;
262 settings_.size = points;
263 if (font_ && !TTF_SetFontSize(font_, static_cast<float>(points)))
264 {
265 settings_.size = previous;
266 (void)TTF_SetFontSize(font_, static_cast<float>(previous));
267 return std::unexpected(makeError(ErrorCode::InvalidSize,
268 settings_.fontPath, SDL_GetError()));
269 }
270 return {};
271 }
272
273 void Runtime::applyStyle() noexcept
274 {
275 if (font_) TTF_SetFontStyle(font_, styleFlags(settings_));
276 }
277
278 void Runtime::setWeight(int weight)
279 {
280 settings_.weight = std::clamp(weight, 0, 1000);
281 applyStyle();
282 }
283
285 {
286 settings_.italic = enabled;
287 applyStyle();
288 }
289
291 {
292 settings_.underline = enabled;
293 applyStyle();
294 }
295
297 {
298 settings_.strikeOut = enabled;
299 applyStyle();
300 }
301
303 {
304 clearPresentationFaces();
305 settings_.weight = 400;
306 settings_.italic = false;
307 settings_.underline = false;
308 settings_.strikeOut = false;
309 applyStyle();
310 }
311
312 std::expected<void, Error> Runtime::saveSettings(std::size_t slot)
313 {
314 if (slot >= SlotCount)
315 return std::unexpected(makeError(ErrorCode::InvalidSlot, {},
316 "font setting slot is outside the retail 0..9 range"));
317 slots_[slot] = settings_;
318 return {};
319 }
320
321 const Settings* Runtime::savedSettings(std::size_t slot) const noexcept
322 {
323 return slot < SlotCount && slots_[slot] ? &*slots_[slot] : nullptr;
324 }
325
326 std::expected<void, Error> Runtime::restoreSettings(std::size_t slot)
327 {
328 if (slot >= SlotCount || !slots_[slot])
329 return std::unexpected(makeError(ErrorCode::InvalidSlot, {},
330 "font setting slot is unavailable"));
331
332 const auto previous = settings_;
333 settings_ = *slots_[slot];
334 if (settings_.fontPath != previous.fontPath)
335 {
336 if (auto result = reopen(); !result)
337 {
338 settings_ = previous;
339 return result;
340 }
341 }
342 else
343 {
344 if (font_ && !TTF_SetFontSize(font_, static_cast<float>(settings_.size)))
345 {
346 settings_ = previous;
347 (void)TTF_SetFontSize(font_, static_cast<float>(previous.size));
348 applyStyle();
349 return std::unexpected(makeError(ErrorCode::InvalidSize,
350 settings_.fontPath, SDL_GetError()));
351 }
352 applyStyle();
353 }
354 return {};
355 }
356
357 std::expected<Metrics, Error> Runtime::measure(std::string_view utf8) const
358 {
359 if (utf8.empty()) return Metrics{};
360 if (!ready())
361 return std::unexpected(makeError(ErrorCode::MeasureFailed,
362 settings_.fontPath, "font runtime is not ready"));
363 int width{};
364 int height{};
365 if (!TTF_GetStringSize(font_, utf8.data(), utf8.size(), &width, &height))
366 return std::unexpected(makeError(ErrorCode::MeasureFailed,
367 settings_.fontPath, SDL_GetError()));
368
369 if (width > 0) ++width; // L_Fonts.cpp adds one pixel to measured width.
370 return Metrics{width, height};
371 }
372
373 std::expected<Metrics, Error> Runtime::measure(std::u16string_view utf16) const
374 {
375 const auto utf8 = utf16ToUtf8(utf16, settings_.fontPath);
376 if (!utf8) return std::unexpected(utf8.error());
377 return measure(*utf8);
378 }
379
380 std::expected<std::vector<std::string>, Error> Runtime::wrap(
381 std::string_view utf8, int width) const
382 {
383 if (width <= 0)
384 return std::unexpected(makeError(ErrorCode::MeasureFailed,
385 settings_.fontPath, "word-wrap width must be positive"));
386 if (utf8.empty()) return std::vector<std::string>{{}};
387
388 auto nextBoundary = [](std::string_view text, std::size_t offset) noexcept
389 {
390 if (offset >= text.size()) return text.size();
391 ++offset;
392 while (offset < text.size() &&
393 (static_cast<unsigned char>(text[offset]) & 0xC0U) == 0x80U)
394 ++offset;
395 return offset;
396 };
397 auto replaceNonBreakingMarkers = [](std::string value)
398 {
399 std::replace(value.begin(), value.end(), '_', ' ');
400 return value;
401 };
402
403 std::vector<std::string> lines;
404 std::string remaining(utf8);
405 for (;;)
406 {
407 const auto whole = measure(remaining);
408 if (!whole) return std::unexpected(whole.error());
409 if (whole->width <= width)
410 {
411 // CHAT_WordWrap does not translate '_' in its final remainder;
412 // preserve that historical quirk instead of normalizing it.
413 lines.push_back(std::move(remaining));
414 break;
415 }
416
417 std::size_t cut = std::string::npos;
418 std::size_t resume = std::string::npos;
419 std::size_t search = remaining.size();
420 while (search != 0)
421 {
422 const auto space = remaining.rfind(' ', search - 1);
423 if (space == std::string::npos) break;
424 std::size_t prefixEnd = space;
425 while (prefixEnd > 0 && remaining[prefixEnd - 1] == ' ')
426 --prefixEnd;
427 if (prefixEnd != 0)
428 {
429 const auto prefixMetrics = measure(
430 std::string_view(remaining).substr(0, prefixEnd));
431 if (!prefixMetrics)
432 return std::unexpected(prefixMetrics.error());
433 if (prefixMetrics->width <= width)
434 {
435 cut = prefixEnd;
436 resume = space + 1;
437 break;
438 }
439 }
440 search = space;
441 }
442
443 // CHAT_WordWrap retries the original text when the space scan
444 // leaves one Windows wchar_t. One complete UTF-8 scalar of at
445 // most three bytes is one UTF-16 unit; an astral scalar is two.
446 const bool oneUtf16Unit = cut != std::string::npos && cut <= 3 &&
447 cut == nextBoundary(remaining, 0);
448 if (cut == std::string::npos || oneUtf16Unit)
449 {
450 std::size_t best{};
451 for (std::size_t end = nextBoundary(remaining, 0);
452 end <= remaining.size() && end != 0;)
453 {
454 const auto prefixMetrics = measure(
455 std::string_view(remaining).substr(0, end));
456 if (!prefixMetrics)
457 return std::unexpected(prefixMetrics.error());
458 if (prefixMetrics->width > width && best != 0)
459 break;
460 best = end; // always consume at least one code point.
461 if (end == remaining.size()) break;
462 end = nextBoundary(remaining, end);
463 }
464 cut = best;
465 resume = best;
466 }
467
468 lines.push_back(replaceNonBreakingMarkers(remaining.substr(0, cut)));
469 remaining.erase(0, resume);
470 if (remaining.empty())
471 {
472 lines.emplace_back();
473 break;
474 }
475 }
476 return lines;
477 }
478
479 void Runtime::clearPresentationFaces() noexcept
480 {
481 for (auto& face : presentationFaces_) face.font.reset();
482 presentationClock_ = 0;
483 }
484
486 {
487 return std::count_if(presentationFaces_.begin(), presentationFaces_.end(),
488 [](const auto& face) { return bool(face.font); });
489 }
490
491 std::expected<data::LegacyBitmapRGBA8, Error> Runtime::renderPresentation(
492 std::string_view utf8, std::uint32_t colorRef, int size, int weight,
493 bool italic, bool underline, bool strikeOut, bool antialiased)
494 {
495 if (size <= 0 || size > 512)
496 return std::unexpected(makeError(ErrorCode::InvalidSize, {},
497 "font size must be between 1 and 512 points"));
498 if (!ready())
499 return std::unexpected(makeError(ErrorCode::RenderFailed,
500 settings_.fontPath, "font runtime is not ready"));
501 if (utf8.empty()) return data::LegacyBitmapRGBA8{};
502 weight = std::clamp(weight, 0, 1000);
503 for (auto& face : presentationFaces_)
504 if (face.font && face.size == size && face.weight == weight &&
505 face.italic == italic && face.underline == underline && face.strikeOut == strikeOut)
506 {
507 face.lastUsed = ++presentationClock_;
508 return renderFace(face.font.get(), utf8, colorRef, antialiased);
509 }
510 std::unique_ptr<TTF_Font, CloseFont> clone(TTF_CopyFont(font_));
511 if (!clone)
512 return std::unexpected(makeError(ErrorCode::FontOpenFailed, settings_.fontPath, SDL_GetError()));
513 if (!TTF_SetFontSize(clone.get(), float(size)))
514 return std::unexpected(makeError(ErrorCode::InvalidSize, settings_.fontPath, SDL_GetError()));
515 Settings requested = settings_;
516 requested.weight = weight; requested.italic = italic;
517 requested.underline = underline; requested.strikeOut = strikeOut;
519 auto slot = std::find_if(presentationFaces_.begin(), presentationFaces_.end(),
520 [](const auto& face) { return !face.font; });
521 if (slot == presentationFaces_.end())
522 slot = std::min_element(presentationFaces_.begin(), presentationFaces_.end(),
523 [](const auto& a, const auto& b) { return a.lastUsed < b.lastUsed; });
524 *slot = PresentationFace{std::move(clone), size, weight, italic, underline, strikeOut, ++presentationClock_};
525 return renderFace(slot->font.get(), utf8, colorRef, antialiased);
526 }
527
528 std::expected<data::LegacyBitmapRGBA8, Error> Runtime::render(
529 std::string_view utf8, std::uint32_t colorRef, bool antialiased) const
530 {
531 return renderFace(font_, utf8, colorRef, antialiased);
532 }
533
534 std::expected<data::LegacyBitmapRGBA8, Error> Runtime::renderFace(
535 TTF_Font* face, std::string_view utf8, std::uint32_t colorRef, bool antialiased) const
536 {
537 if (utf8.empty()) return data::LegacyBitmapRGBA8{};
538 if (!ready())
539 return std::unexpected(makeError(ErrorCode::RenderFailed,
540 settings_.fontPath, "font runtime is not ready"));
541
542 const std::uint8_t red = static_cast<std::uint8_t>(colorRef & 0xFFU);
543 const std::uint8_t green = static_cast<std::uint8_t>((colorRef >> 8U) & 0xFFU);
544 const std::uint8_t blue = static_cast<std::uint8_t>((colorRef >> 16U) & 0xFFU);
545 const auto encodedAlpha = static_cast<std::uint8_t>((colorRef >> 24U) & 0xFFU);
546 const std::uint8_t alpha = encodedAlpha == 0 ? 255U : encodedAlpha;
548
552 if (!rendered)
553 return std::unexpected(makeError(ErrorCode::RenderFailed,
554 settings_.fontPath, SDL_GetError()));
555
558 if (!converted)
559 return std::unexpected(makeError(ErrorCode::SurfaceConversionFailed,
560 settings_.fontPath, SDL_GetError()));
561
562 if (converted->w <= 0 || converted->h <= 0 || !converted->pixels)
563 {
565 return std::unexpected(makeError(ErrorCode::SurfaceConversionFailed,
566 settings_.fontPath, "SDL returned an empty text surface"));
567 }
568
569 data::LegacyBitmapRGBA8 result;
570 result.width = static_cast<std::uint32_t>(converted->w + 1);
571 result.height = static_cast<std::uint32_t>(converted->h);
572 result.pixels.assign(static_cast<std::size_t>(result.width) *
573 result.height * 4U, 0U);
574
575 const auto rowBytes = static_cast<std::size_t>(converted->w) * 4U;
576 const auto* pixels = static_cast<const std::uint8_t*>(converted->pixels);
577 for (int y = 0; y < converted->h; ++y)
578 {
579 const auto* source = pixels + static_cast<std::ptrdiff_t>(y) * converted->pitch;
580 auto* destination = result.pixels.data() +
581 static_cast<std::size_t>(y) * result.width * 4U;
582 std::memcpy(destination, source, rowBytes);
583 }
585 return result;
586 }
587
588 std::expected<data::LegacyBitmapRGBA8, Error> Runtime::render(
589 std::u16string_view utf16, std::uint32_t colorRef, bool antialiased) const
590 {
591 const auto utf8 = utf16ToUtf8(utf16, settings_.fontPath);
592 if (!utf8) return std::unexpected(utf8.error());
593 return render(*utf8, colorRef, antialiased);
594 }
595
596 std::expected<data::LegacyBitmapRGBA8, Error> Runtime::renderClipped(
597 std::string_view utf8, std::uint32_t colorRef, ClipRect clip, bool antialiased) const
598 {
599 const auto image = render(utf8, colorRef, antialiased);
600 if (!image) return std::unexpected(image.error());
601 return clipBitmap(*image, clip);
602 }
603
604 std::expected<data::LegacyBitmapRGBA8, Error> Runtime::renderClipped(
605 std::u16string_view utf16, std::uint32_t colorRef, ClipRect clip, bool antialiased) const
606 {
607 const auto image = render(utf16, colorRef, antialiased);
608 if (!image) return std::unexpected(image.error());
609 return clipBitmap(*image, clip);
610 }
611
612 std::expected<void, Error> Runtime::blitText(
613 data::LegacyBitmapRGBA8& destination, std::string_view utf8,
614 int x, int y, std::uint32_t colorRef) const
615 {
616 if (utf8.empty()) return {};
617 const auto rendered = render(utf8, colorRef);
618 if (!rendered) return std::unexpected(rendered.error());
619 if (destination.width == 0 || destination.height == 0 ||
620 rendered->width == 0 || rendered->height == 0)
621 return {};
622
623 const std::int64_t sourceX64 = x < 0 ? -static_cast<std::int64_t>(x) : 0;
624 const std::int64_t sourceY64 = y < 0 ? -static_cast<std::int64_t>(y) : 0;
625 if (sourceX64 >= rendered->width || sourceY64 >= rendered->height)
626 return {};
627
628 const auto destinationX = static_cast<std::uint32_t>(std::max(x, 0));
629 const auto destinationY = static_cast<std::uint32_t>(std::max(y, 0));
630 if (destinationX >= destination.width || destinationY >= destination.height)
631 return {};
632
633 const auto sourceX = static_cast<std::uint32_t>(sourceX64);
634 const auto sourceY = static_cast<std::uint32_t>(sourceY64);
635 const ClipRect clip{
636 sourceX,
637 sourceY,
638 std::min(rendered->width - sourceX, destination.width - destinationX),
639 std::min(rendered->height - sourceY, destination.height - destinationY)
640 };
642 if (clipped.width == 0 || clipped.height == 0) return {};
643
644 const auto pixelCount =
645 static_cast<std::uint64_t>(destination.width) * destination.height;
646 if (pixelCount > std::numeric_limits<std::size_t>::max() / 4U ||
647 destination.pixels.size() != static_cast<std::size_t>(pixelCount) * 4U)
648 return std::unexpected(makeError(ErrorCode::SurfaceConversionFailed,
649 settings_.fontPath, "invalid destination RGBA8 surface"));
650
651 const auto sourceOverChannel = [](
652 std::uint8_t source, std::uint8_t destination,
653 std::uint32_t sourceAlpha, std::uint32_t destinationAlpha,
654 std::uint32_t outputAlpha) noexcept
655 {
656 if (!outputAlpha) return std::uint8_t{0};
657 const auto destinationContribution =
658 (static_cast<std::uint32_t>(destination) * destinationAlpha *
659 (255U - sourceAlpha) + 127U) / 255U;
660 const auto premultiplied =
661 static_cast<std::uint32_t>(source) * sourceAlpha +
663 return static_cast<std::uint8_t>(std::min(
664 (premultiplied + outputAlpha / 2U) / outputAlpha, 255U));
665 };
666
667 for (std::uint32_t row = 0; row < clipped.height; ++row)
668 {
669 for (std::uint32_t column = 0; column < clipped.width; ++column)
670 {
671 const auto sourceOffset =
672 (static_cast<std::size_t>(row) * clipped.width + column) * 4U;
673 const auto destinationOffset =
674 (static_cast<std::size_t>(destinationY + row) *
675 destination.width + destinationX + column) * 4U;
676 const auto sourceAlpha = clipped.pixels[sourceOffset + 3U];
677 if (!sourceAlpha) continue;
678
679 const auto destinationAlpha =
680 destination.pixels[destinationOffset + 3U];
681 const auto outputAlpha = sourceAlpha +
682 (static_cast<std::uint32_t>(destinationAlpha) *
683 (255U - sourceAlpha) + 127U) / 255U;
684 for (std::size_t channel = 0; channel < 3U; ++channel)
687 clipped.pixels[sourceOffset + channel],
690 destination.pixels[destinationOffset + 3U] =
691 static_cast<std::uint8_t>(outputAlpha);
692 }
693 }
694 return {};
695 }
696
697 std::expected<void, Error> Runtime::blitText(
698 data::LegacyBitmapRGBA8& destination, std::u16string_view utf16,
699 int x, int y, std::uint32_t colorRef) const
700 {
701 const auto utf8 = utf16ToUtf8(utf16, settings_.fontPath);
702 if (!utf8) return std::unexpected(utf8.error());
703 return blitText(destination, *utf8, x, y, colorRef);
704 }
705
706 std::expected<std::filesystem::path, Error>
707 resolveRetailArial(std::span<const std::filesystem::path> roots)
708 {
709 constexpr std::u8string_view wanted = u8"Arial.ttf";
710 for (const auto& root : roots)
711 {
712 std::error_code error;
713 std::filesystem::directory_iterator iterator(root, error);
714 if (error) continue;
715 for (const auto& entry : iterator)
716 {
717 if (!sameAsciiName(entry.path().filename().u8string(), wanted))
718 continue;
719 const auto status = entry.status(error);
720 if (!error && std::filesystem::is_regular_file(status))
721 return entry.path();
722 error.clear();
723 }
724 }
725 return std::unexpected(makeError(ErrorCode::FontNotFound, {},
726 "Arial.ttf was not found in the explicit retail font roots"));
727 }
728}
std::vector< std::uint8_t > data
std::size_t offset
std::size_t root
std::string_view text
std::atomic< float > left
std::atomic< float > right
LegacyBitmapRGBA8 image
fonts::Runtime & font
bool enabled
Definition Engine.cpp:170
PlayerNumber slot
std::shared_ptr< const data::HmdTextureImage > source
std::uint32_t height
std::uint32_t width
unsigned index
std::array< data::DataTag, 3 > roots
std::string path
std::string error
std::uint16_t value
Definition Timers.cpp:16
std::expected< void, Error > setFont(const std::filesystem::path &path, std::string familyName={})
std::size_t presentationFaceCount() const noexcept
const Settings * savedSettings(std::size_t slot) const noexcept
std::expected< Metrics, Error > measure(std::string_view utf8) const
std::expected< data::LegacyBitmapRGBA8, Error > renderPresentation(std::string_view utf8, std::uint32_t colorRef, int size, int weight, bool italic, bool underline=false, bool strikeOut=false, bool antialiased=true)
const Settings & settings() const noexcept
std::expected< data::LegacyBitmapRGBA8, Error > render(std::string_view utf8, std::uint32_t colorRef, bool antialiased=false) const
static constexpr std::size_t SlotCount
void setWeight(int weight)
void setUnderline(bool enabled)
std::expected< void, Error > setSize(int points)
std::expected< void, Error > saveSettings(std::size_t slot)
std::expected< data::LegacyBitmapRGBA8, Error > renderClipped(std::string_view utf8, std::uint32_t colorRef, ClipRect clip, bool antialiased=false) const
bool ready() const noexcept
std::expected< std::vector< std::string >, Error > wrap(std::string_view utf8, int width) const
std::expected< void, Error > restoreSettings(std::size_t slot)
void setStrikeOut(bool enabled)
void setItalic(bool enabled)
std::expected< void, Error > blitText(data::LegacyBitmapRGBA8 &destination, std::string_view utf8, int x, int y, std::uint32_t colorRef) const
std::expected< std::string, Error > transcodeUtf8(std::u16string_view text)
std::expected< std::filesystem::path, Error > resolveRetailArial(std::span< const std::filesystem::path > roots)
std::filesystem::path fontPath