Files
Gitree/src/managers/avatar_cache.cpp

190 lines
6.4 KiB
C++

#include "avatar_cache.h"
#include <GLFW/glfw3.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <iomanip>
#include <sstream>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#include <bcrypt.h>
#include <objbase.h>
#include <urlmon.h>
#include <wincodec.h>
#endif
AvatarCache::AvatarCache(const std::filesystem::path &user_data_directory)
: directory_(user_data_directory / "avatars")
{
std::filesystem::create_directories(directory_);
}
AvatarCache::~AvatarCache()
{
shutdown();
}
std::string AvatarCache::hashEmail(const std::string &email) const
{
std::string normalized = email;
normalized.erase(normalized.begin(), std::find_if(normalized.begin(), normalized.end(),
[](unsigned char value)
{ return !std::isspace(value); }));
normalized.erase(std::find_if(normalized.rbegin(), normalized.rend(),
[](unsigned char value)
{ return !std::isspace(value); })
.base(),
normalized.end());
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char value)
{ return static_cast<char>(std::tolower(value)); });
#ifdef _WIN32
BCRYPT_ALG_HANDLE algorithm = nullptr;
BCRYPT_HASH_HANDLE hash = nullptr;
std::array<unsigned char, 16> digest{};
DWORD object_size = 0;
DWORD written = 0;
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_MD5_ALGORITHM, nullptr, 0) < 0 ||
BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&object_size),
sizeof(object_size), &written, 0) < 0)
{
if (algorithm)
BCryptCloseAlgorithmProvider(algorithm, 0);
return {};
}
std::vector<unsigned char> object(object_size);
if (BCryptCreateHash(algorithm, &hash, object.data(), object_size, nullptr, 0, 0) < 0 ||
BCryptHashData(hash, reinterpret_cast<PUCHAR>(normalized.data()),
static_cast<ULONG>(normalized.size()), 0) < 0 ||
BCryptFinishHash(hash, digest.data(), static_cast<ULONG>(digest.size()), 0) < 0)
{
if (hash)
BCryptDestroyHash(hash);
BCryptCloseAlgorithmProvider(algorithm, 0);
return {};
}
BCryptDestroyHash(hash);
BCryptCloseAlgorithmProvider(algorithm, 0);
std::ostringstream output;
for (unsigned char value : digest)
output << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(value);
return output.str();
#else
return std::to_string(std::hash<std::string>{}(normalized));
#endif
}
unsigned int AvatarCache::textureFor(const std::string &email)
{
if (email.empty())
return 0;
const std::string hash = hashEmail(email);
if (hash.empty())
return 0;
Entry &entry = entries_[hash];
if (entry.texture)
return entry.texture;
if (entry.requested)
return 0;
entry.requested = true;
entry.file = directory_ / (hash + ".img");
if (std::filesystem::exists(entry.file))
{
entry.texture = loadTexture(entry.file);
return entry.texture;
}
#ifdef _WIN32
const std::filesystem::path file = entry.file;
const std::wstring url = L"https://www.gravatar.com/avatar/" +
std::wstring(hash.begin(), hash.end()) + L"?s=64&d=identicon&r=g";
entry.download = std::async(std::launch::async, [file, url]
{ return SUCCEEDED(URLDownloadToFileW(nullptr, url.c_str(), file.wstring().c_str(), 0, nullptr)); });
#endif
return 0;
}
void AvatarCache::update()
{
for (auto &[hash, entry] : entries_)
{
(void)hash;
if (entry.texture || !entry.download.valid())
continue;
if (entry.download.wait_for(std::chrono::seconds(0)) != std::future_status::ready)
continue;
if (entry.download.get())
entry.texture = loadTexture(entry.file);
}
}
unsigned int AvatarCache::loadTexture(const std::filesystem::path &file) const
{
#ifdef _WIN32
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
IWICImagingFactory *factory = nullptr;
IWICBitmapDecoder *decoder = nullptr;
IWICBitmapFrameDecode *frame = nullptr;
IWICFormatConverter *converter = nullptr;
UINT width = 0;
UINT height = 0;
std::vector<unsigned char> pixels;
unsigned int texture = 0;
if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&factory))) &&
SUCCEEDED(factory->CreateDecoderFromFilename(file.wstring().c_str(), nullptr, GENERIC_READ,
WICDecodeMetadataCacheOnLoad, &decoder)) &&
SUCCEEDED(decoder->GetFrame(0, &frame)) &&
SUCCEEDED(factory->CreateFormatConverter(&converter)) &&
SUCCEEDED(converter->Initialize(frame, GUID_WICPixelFormat32bppRGBA,
WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) &&
SUCCEEDED(converter->GetSize(&width, &height)))
{
pixels.resize(static_cast<size_t>(width) * height * 4);
if (SUCCEEDED(converter->CopyPixels(nullptr, width * 4,
static_cast<UINT>(pixels.size()), pixels.data())))
{
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<int>(width), static_cast<int>(height),
0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glBindTexture(GL_TEXTURE_2D, 0);
}
}
if (converter)
converter->Release();
if (frame)
frame->Release();
if (decoder)
decoder->Release();
if (factory)
factory->Release();
CoUninitialize();
return texture;
#else
(void)file;
return 0;
#endif
}
void AvatarCache::shutdown()
{
for (auto &[hash, entry] : entries_)
{
(void)hash;
if (entry.download.valid())
entry.download.wait();
if (entry.texture)
glDeleteTextures(1, &entry.texture);
entry.texture = 0;
}
entries_.clear();
}