From b1296f2c97c6fdc1c6a9922dc09c951b5cafdc12 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 29 Oct 2018 21:49:54 +0100 Subject: Initial commit --- .gitignore | 6 + .gitmodules | 3 + README.md | 7 + depends/odhtdb | 1 + include/FileUtil.hpp | 21 ++ include/dchat/Cache.hpp | 102 +++++++++ include/dchat/Clock.hpp | 16 ++ include/dchat/Color.hpp | 24 +++ include/dchat/Gif.hpp | 52 +++++ include/dchat/StringView.hpp | 95 +++++++++ include/dchat/Vec2.hpp | 32 +++ include/dchat/WebPagePreview.hpp | 13 ++ include/dchat/types.hpp | 22 ++ include/env.hpp | 59 ++++++ project.conf | 12 ++ src/Cache.cpp | 434 +++++++++++++++++++++++++++++++++++++++ src/Clock.cpp | 25 +++ src/FileUtil.cpp | 55 +++++ src/Gif.cpp | 189 +++++++++++++++++ tests/main.cpp | 7 + 20 files changed, 1175 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 160000 depends/odhtdb create mode 100644 include/FileUtil.hpp create mode 100644 include/dchat/Cache.hpp create mode 100644 include/dchat/Clock.hpp create mode 100644 include/dchat/Color.hpp create mode 100644 include/dchat/Gif.hpp create mode 100644 include/dchat/StringView.hpp create mode 100644 include/dchat/Vec2.hpp create mode 100644 include/dchat/WebPagePreview.hpp create mode 100644 include/dchat/types.hpp create mode 100644 include/env.hpp create mode 100644 project.conf create mode 100644 src/Cache.cpp create mode 100644 src/Clock.cpp create mode 100644 src/FileUtil.cpp create mode 100644 src/Gif.cpp create mode 100644 tests/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0dee329 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Compiled sibs files +sibs-build/ +compile_commands.json +tests/sibs-build/ +tests/compile_commands.json +.vscode/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8fd227f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "depends/odhtdb"] + path = depends/odhtdb + url = https://gitlab.com/DEC05EBA/odhtdb.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9f3e93 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Dchat core +Useful classes to handle the things that we want all dchat clients to have, for example: +``` +Channels +Asynchronous texture/gif/website preview loading +Video playback to view content on sites such as youtube (by using mpv) +``` diff --git a/depends/odhtdb b/depends/odhtdb new file mode 160000 index 0000000..55bb14a --- /dev/null +++ b/depends/odhtdb @@ -0,0 +1 @@ +Subproject commit 55bb14a7e0d034b375da73a9c2aae10881e32801 diff --git a/include/FileUtil.hpp b/include/FileUtil.hpp new file mode 100644 index 0000000..cb4e308 --- /dev/null +++ b/include/FileUtil.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "dchat/StringView.hpp" +#include +#include + +namespace dchat +{ + class FileException : public std::runtime_error + { + public: + FileException(const std::string &errMsg) : std::runtime_error(errMsg) {} + }; + + // Throws FileException on error. + // Returned value is allocated with malloc and should be free'd by caller. + StringView getFileContent(const boost::filesystem::path &filepath); + + // Throws FileException on error + void fileReplace(const boost::filesystem::path &filepath, const StringView data); +} diff --git a/include/dchat/Cache.hpp b/include/dchat/Cache.hpp new file mode 100644 index 0000000..debcd5b --- /dev/null +++ b/include/dchat/Cache.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include "types.hpp" +#include "WebPagePreview.hpp" +#include "StringView.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace TinyProcessLib +{ + class Process; +} + +namespace dchat +{ + class Gif; + class WebPagePreview; + + struct ContentByUrlResult + { + enum class Type + { + CACHED, + DOWNLOADING, + FAILED_DOWNLOAD + }; + + enum class CachedType + { + NONE, + TEXTURE_FILEPATH, + GIF, + WEB_PAGE_PREVIEW + }; + + ContentByUrlResult() : textureFilePath(nullptr), type(Type::DOWNLOADING), cachedType(CachedType::NONE) {} + ContentByUrlResult(boost::filesystem::path *_textureFilePath, Type _type) : textureFilePath(_textureFilePath), type(_type), cachedType(CachedType::TEXTURE_FILEPATH) {} + ContentByUrlResult(Gif *_gif, Type _type) : gif(_gif), type(_type), cachedType(CachedType::GIF) {} + ContentByUrlResult(WebPagePreview *_webPagePreview, Type _type) : webPagePreview(_webPagePreview), type(_type), cachedType(CachedType::WEB_PAGE_PREVIEW) {} + + // @texture is null if @type is DOWNLOADING or FAILED_DOWNLOAD + union + { + boost::filesystem::path *textureFilePath; + Gif *gif; + WebPagePreview *webPagePreview; + }; + + Type type; + CachedType cachedType; + i64 lastAccessed; + }; + + using LoadBindsCallbackFunc = std::function; + // @fileContent contains data allocated with new[], deallocate it with delete[] fileContent.data; + // Returned gif should be allocated with @new + using CreateGifFunc = std::function; + + class Cache + { + public: + // @createGifFunc can't be nullptr + Cache(CreateGifFunc createGifFunc); + ~Cache(); + + // Creates directory if it doesn't exist (recursively). Throws boost exception on failure + static boost::filesystem::path getDchatDir(); + + // Creates directory if it doesn't exist (recursively). Throws boost exception on failure + static boost::filesystem::path getImagesDir(); + + // @callbackFunc can't be nullptr + static void loadBindsFromFile(LoadBindsCallbackFunc callbackFunc); + static void replaceBindsInFile(const std::unordered_map &binds); + + // Get cached content or download it. + // Default download file limit is 12MB + // Returns ContentByUrlResult describing texture status. + const ContentByUrlResult getContentByUrl(const std::string &url, int downloadLimitBytes = 12582912); + private: + struct ImageDownloadInfo + { + TinyProcessLib::Process *process; + std::string url; + }; + + std::thread downloadWaitThread; + std::thread checkContentAccessTimeThread; + std::vector imageDownloadProcesses; + std::vector imageDownloadProcessesQueue; + std::mutex imageDownloadMutex; + bool alive; + std::unordered_map contentUrlCache; + CreateGifFunc createGifFunc; + }; +} diff --git a/include/dchat/Clock.hpp b/include/dchat/Clock.hpp new file mode 100644 index 0000000..97f78a8 --- /dev/null +++ b/include/dchat/Clock.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "types.hpp" + +namespace dchat +{ + class Clock + { + public: + Clock(); + void restart(); + i64 getElapsedTimeMillis() const; + private: + i64 startTime; + }; +} \ No newline at end of file diff --git a/include/dchat/Color.hpp b/include/dchat/Color.hpp new file mode 100644 index 0000000..2a5d121 --- /dev/null +++ b/include/dchat/Color.hpp @@ -0,0 +1,24 @@ +#pragma once + +namespace dchat +{ + class Color + { + public: + Color() : Color(255, 255, 255, 255) {} + Color(unsigned char red, unsigned char green, unsigned char blue, unsigned char alpha = 255) + { + data[0] = red; + data[1] = green; + data[2] = blue; + data[3] = alpha; + } + + unsigned char red() const { return data[0]; } + unsigned char green() const { return data[1]; } + unsigned char blue() const { return data[2]; } + unsigned char alpha() const { return data[3]; } + + unsigned char data[4]; + }; +} \ No newline at end of file diff --git a/include/dchat/Gif.hpp b/include/dchat/Gif.hpp new file mode 100644 index 0000000..f97ff2f --- /dev/null +++ b/include/dchat/Gif.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "StringView.hpp" +#include "Vec2.hpp" +#include "Color.hpp" +#include "Clock.hpp" +#include +#include +extern "C" +{ +#include +} + +namespace dchat +{ + class GifLoadException : public std::runtime_error + { + public: + GifLoadException(const std::string &errMsg) : std::runtime_error(errMsg) {} + }; + + class Gif + { + public: + // Throws GifLoadException on error + Gif(const boost::filesystem::path &filepath); + Gif(StringView fileContent); + virtual ~Gif(); + + Vec2u getSize() const; + void update(); + + static bool isDataGif(const StringView &data); + protected: + // Return false if texture creation failed + virtual bool createTexture(int width, int height) = 0; + virtual void setPosition(const Vec2f &position) = 0; + virtual Vec2f getPosition() const = 0; + virtual void setScale(const Vec2f &scale) = 0; + virtual void setColor(Color color) = 0; + // Size of texture data is same as the size that the texture was created with (also same size returned by @getSize function) + virtual void updateTexture(void *textureData) = 0; + private: + void init(); + private: + gif_animation gif; + StringView fileContent; + unsigned int currentFrame; + double timeElapsedCs; + Clock frameTimer; + }; +} diff --git a/include/dchat/StringView.hpp b/include/dchat/StringView.hpp new file mode 100644 index 0000000..659610a --- /dev/null +++ b/include/dchat/StringView.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include "types.hpp" +#include +#include + +namespace dchat +{ + template + class BasicStringView + { + public: + BasicStringView() : data(nullptr), size(0) + { + + } + + BasicStringView(const BasicStringView &other) : data(other.data), size(other.size) + { + + } + + BasicStringView(const CharType *_data) : data(_data), size(strlen(_data)) + { + + } + + BasicStringView(const CharType *_data, usize _size) : data(_data), size(_size) + { + + } + + BasicStringView& operator = (const BasicStringView &other) + { + data = other.data; + size = other.size; + return *this; + } + + BasicStringView(BasicStringView &&other) + { + data = other.data; + size = other.size; + + other.data = nullptr; + other.size = 0; + } + + bool equals(const BasicStringView &other) const + { + if(size != other.size) return false; + return memcmp(data, other.data, size * sizeof(CharType)) == 0; + } + + bool operator == (const BasicStringView &other) const + { + return equals(other); + } + + bool operator != (const BasicStringView &other) const + { + return !equals(other); + } + + CharType operator [] (usize index) const + { + assert(index < size); + return data[index]; + } + + // Returns -1 if substr not found. + // TODO: Make this more efficient + usize find(const BasicStringView &substr, usize offset = 0) const + { + if(substr.size == 0) + return -1; + + if(offset + substr.size > size) + return -1; + + for(usize i = offset; i < size - (substr.size - 1); ++i) + { + if(memcmp(data + i, substr.data, substr.size * sizeof(CharType)) == 0) + return i; + } + return -1; + } + + const CharType *data; + usize size; + }; + + using StringView = BasicStringView; + using StringViewUtf32 = BasicStringView; +} diff --git a/include/dchat/Vec2.hpp b/include/dchat/Vec2.hpp new file mode 100644 index 0000000..cb5f8c0 --- /dev/null +++ b/include/dchat/Vec2.hpp @@ -0,0 +1,32 @@ +#pragma once + +namespace dchat +{ + template + struct Vec2 + { + T x, y; + + Vec2() : x(), y() {} + Vec2(T _x, T _y) : x(_x), y(_y) {} +/* + Vec2(const Vec2 &other) + { + x = other.x; + y = other.y; + } + + Vec2& operator = (const Vec2 &other) + { + x = other.x; + y = other.y; + return *this; + } +*/ + }; + + using Vec2f = Vec2; + using Vec2d = Vec2; + using Vec2i = Vec2; + using Vec2u = Vec2; +} \ No newline at end of file diff --git a/include/dchat/WebPagePreview.hpp b/include/dchat/WebPagePreview.hpp new file mode 100644 index 0000000..df75419 --- /dev/null +++ b/include/dchat/WebPagePreview.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace dchat +{ + class WebPagePreview + { + public: + std::string title; + std::string description; + }; +} diff --git a/include/dchat/types.hpp b/include/dchat/types.hpp new file mode 100644 index 0000000..97bfc96 --- /dev/null +++ b/include/dchat/types.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace dchat +{ + typedef int8_t i8; + typedef int16_t i16; + typedef int32_t i32; + typedef int64_t i64; + + typedef uint8_t u8; + typedef uint16_t u16; + typedef uint32_t u32; + typedef uint64_t u64; + + typedef float f32; + typedef double f64; + + typedef intptr_t ssize; + typedef uintptr_t usize; +} diff --git a/include/env.hpp b/include/env.hpp new file mode 100644 index 0000000..4061518 --- /dev/null +++ b/include/env.hpp @@ -0,0 +1,59 @@ +#pragma once + +#define OS_FAMILY_WINDOWS 0 +#define OS_FAMILY_POSIX 1 + +#define OS_TYPE_WINDOWS 0 +#define OS_TYPE_LINUX 1 + +#if defined(_WIN32) || defined(_WIN64) + #if defined(_WIN64) + #define SYS_ENV_64BIT + #else + #define SYS_ENV_32BIT + #endif + #define OS_FAMILY OS_FAMILY_WINDOWS + #define OS_TYPE OS_TYPE_WINDOWS + + #ifndef UNICODE + #define UNICODE + #endif + + #ifndef _UNICODE + #define _UNICODE + #endif + + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + + #include +#endif + +#if defined(__linux__) || defined(__unix__) || defined(__APPLE__) || defined(_POSIX_VERSION) + #define OS_FAMILY OS_FAMILY_POSIX +#endif + +#if defined(__linux__) || defined(__CYGWIN__) + #define OS_TYPE OS_TYPE_LINUX +#endif + +#if defined(__GNUC__) + #if defined(__x86_64__) || defined(__pc64__) + #define SYS_ENV_64BIT + #else + #define SYS_ENV_32BIT + #endif +#endif + +#if !defined(SYS_ENV_32BIT) && !defined(SYS_ENV_64BIT) + #error "System is not detected as either 32-bit or 64-bit" +#endif + +#if !defined(OS_FAMILY) + #error "System not supported. Only Windows and Posix systems supported right now" +#endif + +#if !defined(OS_TYPE) + #error "System not supported. Only Windows and linux systems supported right now" +#endif diff --git a/project.conf b/project.conf new file mode 100644 index 0000000..7bfa3b1 --- /dev/null +++ b/project.conf @@ -0,0 +1,12 @@ +[package] +name = "dchat_core" +type = "dynamic" +version = "0.1.0" +platforms = ["any"] + +[dependencies] +tiny-process = "2" +boost-filesystem = "1.66" +libpreview = ">=0.2.0" +libgd = "2" +libnsgif = "0.2.0" \ No newline at end of file diff --git a/src/Cache.cpp b/src/Cache.cpp new file mode 100644 index 0000000..0bde89d --- /dev/null +++ b/src/Cache.cpp @@ -0,0 +1,434 @@ +#include "../include/dchat/Cache.hpp" +#include "../include/env.hpp" +#include "../include/FileUtil.hpp" +#include "../include/dchat/Gif.hpp" +#include +#include +#include +#include +#include +#include +#include + +#if OS_FAMILY == OS_FAMILY_POSIX +#include +#else +#include +#endif + +using namespace std; +using namespace TinyProcessLib; + +namespace dchat +{ + const i64 CONTENT_NOT_VISIBLE_AGE_MS = 30000; // Delete content from cache after a specified amount of time if the content is not visible on the screen + + static boost::filesystem::path getHomeDir() + { + #if OS_FAMILY == OS_FAMILY_POSIX + const char *homeDir = getenv("HOME"); + if(!homeDir) + { + passwd *pw = getpwuid(getuid()); + homeDir = pw->pw_dir; + } + return boost::filesystem::path(homeDir); + #elif OS_FAMILY == OS_FAMILY_WINDOWS + BOOL ret; + HANDLE hToken; + std::wstring homeDir; + DWORD homeDirLen = MAX_PATH; + homeDir.resize(homeDirLen); + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &hToken)) + return Result::Err("Failed to open process token"); + + if (!GetUserProfileDirectory(hToken, &homeDir[0], &homeDirLen)) + { + CloseHandle(hToken); + return Result::Err("Failed to get home directory"); + } + + CloseHandle(hToken); + homeDir.resize(wcslen(homeDir.c_str())); + return boost::filesystem::path(homeDir); + #endif + } + + boost::filesystem::path Cache::getDchatDir() + { + boost::filesystem::path dchatHomeDir = getHomeDir() / ".local" / "share" / "dchat"; + boost::filesystem::create_directories(dchatHomeDir); + return dchatHomeDir; + } + + boost::filesystem::path Cache::getImagesDir() + { + boost::filesystem::path imagesDir = getDchatDir() / "images"; + boost::filesystem::create_directories(imagesDir); + return imagesDir; + } + + void Cache::loadBindsFromFile(LoadBindsCallbackFunc callbackFunc) + { + assert(callbackFunc); + StringView fileContent; + try + { + fileContent = getFileContent(getDchatDir() / "binds"); + sibs::SafeDeserializer deserializer((const u8*)fileContent.data, fileContent.size); + + while(!deserializer.empty()) + { + u8 keySize = deserializer.extract(); + string key; + key.resize(keySize); + deserializer.extract((u8*)&key[0], keySize); + + u8 valueSize = deserializer.extract(); + string value; + value.resize(valueSize); + deserializer.extract((u8*)&value[0], valueSize); + + callbackFunc(key, value); + } + } + catch(FileException &e) + { + fprintf(stderr, "Failed to read binds from file, reason: %s\n", e.what()); + } + + delete[] fileContent.data; + } + + void Cache::replaceBindsInFile(const unordered_map &binds) + { + sibs::SafeSerializer serializer; + for(auto &it : binds) + { + serializer.add((u8)it.first.size()); + serializer.add((const u8*)it.first.data(), it.first.size()); + + serializer.add((u8)it.second.size()); + serializer.add((const u8*)it.second.data(), it.second.size()); + } + fileReplace(getDchatDir() / "binds", StringView((const char*)serializer.getBuffer().data(), serializer.getBuffer().size())); + } + + static bool downscaleImage(const boost::filesystem::path &filepath, void *data, const int size, const int newWidth, const int newHeight) + { + gdImagePtr imgPtr = gdImageCreateFromPngPtr(size, data); + if(!imgPtr) + return false; + + int width = gdImageSX(imgPtr); + if(width < newWidth) + { + gdImageDestroy(imgPtr); + return false; + } + + int height = gdImageSX(imgPtr); + if(height < newHeight) + { + gdImageDestroy(imgPtr); + return false; + } + + gdImageSetInterpolationMethod(imgPtr, GD_BILINEAR_FIXED); + gdImagePtr newImgPtr = gdImageScale(imgPtr, newWidth, newHeight); + if(!newImgPtr) + { + gdImageDestroy(imgPtr); + return false; + } + + bool success = (gdImageFile(newImgPtr, filepath.c_str()) == 0); + gdImageDestroy(imgPtr); + gdImageDestroy(newImgPtr); + return success; + } + + static ContentByUrlResult loadImageFromFile(const boost::filesystem::path &filepath, bool loadFromCache, CreateGifFunc createGifFunc) + { + StringView fileContent; + try + { + fileContent = getFileContent(filepath); + + std::string webPageTitle; + std::string webPageDescription; + bool foundHtmlContent = false; + preview_state state; + preview_init(&state); + size_t offset = 0; + do + { + // TODO: Get file content before doing this, the file might be in utf-16 encoding. That can happen for example if file contains html. + // Content type can be retrieved from HTTP response header when downloading content + offset += preview_step(&state, fileContent.data + offset, fileContent.size - offset); + if(state.step_result == PREVIEW_FOUND_IMAGE) + { + if(Gif::isDataGif(fileContent)) + { + Gif *gif = createGifFunc(fileContent); + return { gif, ContentByUrlResult::Type::CACHED }; + } + else + { + if(!loadFromCache) + { + if(!downscaleImage(filepath, (void*)fileContent.data, fileContent.size, 100, 100)) + { + fprintf(stderr, "Failed to resize image: %s, using original file\n", filepath.c_str()); + } + } + + delete[] fileContent.data; + fileContent.data = nullptr; + return { new boost::filesystem::path(filepath), ContentByUrlResult::Type::CACHED }; + } + break; + } + else if(state.step_result == PREVIEW_FOUND_TITLE) + { + foundHtmlContent = true; + webPageTitle = std::string(state.meta_content, state.meta_content + state.meta_content_length); + } + else if(state.step_result == PREVIEW_FOUND_DESCRIPTION) + { + foundHtmlContent = true; + webPageDescription = std::string(state.meta_content, state.meta_content + state.meta_content_length); + } + } while(offset < fileContent.size); + + delete[] fileContent.data; + fileContent.data = nullptr; + + if(foundHtmlContent) + { + WebPagePreview *webPagePreview = new WebPagePreview { webPageTitle, webPageDescription }; + return { webPagePreview, ContentByUrlResult::Type::CACHED }; + } + } + catch(std::exception &e) + { + fprintf(stderr, "Failed to load image %s, reason: %s\n", filepath.string().c_str(), e.what()); + } + return { (boost::filesystem::path*)nullptr, ContentByUrlResult::Type::FAILED_DOWNLOAD }; + } + + Cache::Cache(CreateGifFunc _createGifFunc) : + alive(true), + createGifFunc(_createGifFunc) + { + assert(createGifFunc); + downloadWaitThread = thread([this] + { + while(alive) + { + for(vector::iterator it = imageDownloadProcesses.begin(); it != imageDownloadProcesses.end();) + { + int exitStatus; + if(it->process->try_get_exit_status(exitStatus)) + { + boost::filesystem::path filepath = getImagesDir(); + odhtdb::Hash urlHash(it->url.data(), it->url.size()); + filepath /= urlHash.toString(); + + ContentByUrlResult contentByUrlResult; + bool failed = exitStatus != 0; + if(failed) + { + contentByUrlResult = { (boost::filesystem::path*)nullptr, ContentByUrlResult::Type::FAILED_DOWNLOAD }; + } + else + { + contentByUrlResult = loadImageFromFile(filepath, false, createGifFunc); + contentByUrlResult.lastAccessed = chrono::duration_cast(chrono::steady_clock::now().time_since_epoch()).count(); + if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) + { + printf("Download content from url: %s\n", it->url.c_str()); + } + } + + imageDownloadMutex.lock(); + contentUrlCache[it->url] = contentByUrlResult; + boost::filesystem::path downloadingFilepath = filepath; + downloadingFilepath += ".downloading"; + // Intentionally ignore failure, program should not crash if we fail to remove these files... + boost::system::error_code err; + boost::filesystem::remove(downloadingFilepath, err); + imageDownloadMutex.unlock(); + it = imageDownloadProcesses.erase(it); + } + else + ++it; + } + + while(alive && imageDownloadProcesses.empty() && imageDownloadProcessesQueue.empty()) + this_thread::sleep_for(chrono::milliseconds(20)); + + if(!imageDownloadProcessesQueue.empty()) + { + imageDownloadMutex.lock(); + for(auto imageDownloadInfo : imageDownloadProcessesQueue) + { + imageDownloadProcesses.push_back(imageDownloadInfo); + } + imageDownloadProcessesQueue.clear(); + imageDownloadMutex.unlock(); + } + + this_thread::sleep_for(chrono::milliseconds(20)); + } + }); + + checkContentAccessTimeThread = thread([this] + { + while(alive) + { + this_thread::sleep_for(chrono::milliseconds(500)); + lock_guard lock(imageDownloadMutex); + + i64 now = chrono::duration_cast(chrono::steady_clock::now().time_since_epoch()).count(); + for(unordered_map::iterator it = contentUrlCache.begin(); it != contentUrlCache.end();) + { + if(it->second.type == ContentByUrlResult::Type::CACHED && now - it->second.lastAccessed > CONTENT_NOT_VISIBLE_AGE_MS) + { + switch(it->second.cachedType) + { + case ContentByUrlResult::CachedType::TEXTURE_FILEPATH: + { + delete it->second.textureFilePath; + break; + } + case ContentByUrlResult::CachedType::GIF: + { + delete it->second.gif; + break; + } + case ContentByUrlResult::CachedType::WEB_PAGE_PREVIEW: + { + delete it->second.webPagePreview; + break; + } + default: + ++it; + continue; + } + it = contentUrlCache.erase(it); + } + else + ++it; + } + } + }); + } + + Cache::~Cache() + { + alive = false; + downloadWaitThread.join(); + checkContentAccessTimeThread.join(); + + for(auto &it : contentUrlCache) + { + if(!it.second.textureFilePath) + continue; + + switch(it.second.cachedType) + { + case ContentByUrlResult::CachedType::NONE: + break; + case ContentByUrlResult::CachedType::TEXTURE_FILEPATH: + { + delete it.second.textureFilePath; + break; + } + case ContentByUrlResult::CachedType::GIF: + { + delete it.second.gif; + break; + } + case ContentByUrlResult::CachedType::WEB_PAGE_PREVIEW: + { + delete it.second.webPagePreview; + break; + } + default: + // Did we forget to implement cleanup for a new cache content type? + assert(false); + break; + } + } + } + + static void createFileIgnoreError(const boost::filesystem::path &path) + { + try + { + fileReplace(path, StringView("", 0)); + } + catch(FileException &e) + { + fprintf(stderr, "Failed to create empty file: %s, reason: %s\n", path.string().c_str(), e.what()); + } + } + + const ContentByUrlResult Cache::getContentByUrl(const string &url, int downloadLimitBytes) + { + lock_guard lock(imageDownloadMutex); + auto it = contentUrlCache.find(url); + if(it != contentUrlCache.end()) + { + it->second.lastAccessed = chrono::duration_cast(chrono::steady_clock::now().time_since_epoch()).count(); + return it->second; + } + + // TODO: Verify hashed url is not too long for filepath on windows + boost::filesystem::path filepath = getImagesDir(); + odhtdb::Hash urlHash(url.data(), url.size()); + filepath /= urlHash.toString(); + + boost::filesystem::path downloadingFilepath = filepath; + downloadingFilepath += ".downloading"; + if(boost::filesystem::exists(downloadingFilepath)) + { + // Intentionally ignore failure, program should not crash if we fail to remove these files... + boost::system::error_code err; + boost::filesystem::remove(filepath, err); + boost::filesystem::remove(downloadingFilepath, err); + } + + // TODO: Do not load content in this thread. Return LOADING status and load it in another thread, because with a lot of images, chat can freeze + ContentByUrlResult contentByUrlResult = loadImageFromFile(filepath, true, createGifFunc); + if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) + { + contentByUrlResult.lastAccessed = chrono::duration_cast(chrono::steady_clock::now().time_since_epoch()).count(); + contentUrlCache[url] = contentByUrlResult; + printf("Loaded content from file cache: %s\n", url.c_str()); + return contentByUrlResult; + } + else if(contentByUrlResult.type == ContentByUrlResult::Type::FAILED_DOWNLOAD && boost::filesystem::exists(filepath)) + { + contentUrlCache[url] = contentByUrlResult; + return contentByUrlResult; + } + + createFileIgnoreError(downloadingFilepath); + ContentByUrlResult result((boost::filesystem::path*)nullptr, ContentByUrlResult::Type::DOWNLOADING); + contentUrlCache[url] = result; + + string downloadLimitBytesStr = to_string(downloadLimitBytes); + + Process::string_type cmd = "curl -L --silent -o '"; + cmd += filepath.native(); + cmd += "' --max-filesize " + downloadLimitBytesStr + " --range 0-" + downloadLimitBytesStr + " --url '" + url + "'"; + // TODO: Use this instead of curl on windows: certutil.exe -urlcache -split -f "https://url/to/file" path/and/name/to/save/as/file + Process *process = new Process(cmd, "", nullptr, nullptr, false); + ImageDownloadInfo imageDownloadInfo { process, url }; + imageDownloadProcessesQueue.emplace_back(imageDownloadInfo); + return result; + } +} diff --git a/src/Clock.cpp b/src/Clock.cpp new file mode 100644 index 0000000..1dc9323 --- /dev/null +++ b/src/Clock.cpp @@ -0,0 +1,25 @@ +#include "../include/dchat/Clock.hpp" +#include + +namespace dchat +{ + static i64 getCurrentTimeMillis() + { + return std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + } + + Clock::Clock() + { + restart(); + } + + void Clock::restart() + { + startTime = getCurrentTimeMillis(); + } + + i64 Clock::getElapsedTimeMillis() const + { + return getCurrentTimeMillis() - startTime; + } +} \ No newline at end of file diff --git a/src/FileUtil.cpp b/src/FileUtil.cpp new file mode 100644 index 0000000..08efd00 --- /dev/null +++ b/src/FileUtil.cpp @@ -0,0 +1,55 @@ +#include "../include/FileUtil.hpp" +#include "../include/env.hpp" +#include + +namespace dchat +{ + StringView getFileContent(const boost::filesystem::path &filepath) + { +#if OS_FAMILY == OS_FAMILY_POSIX + FILE *file = fopen(filepath.c_str(), "rb"); +#else + FILE *file = _wfopen(filepath.c_str(), L"rb"); +#endif + if(!file) + { + int error = errno; + std::string errMsg = "Failed to open file: "; + errMsg += filepath.string(); + errMsg += "; reason: "; + errMsg += strerror(error); + throw FileException(errMsg); + } + + fseek(file, 0, SEEK_END); + size_t fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + char *fileData = new char[fileSize]; + fread(fileData, 1, fileSize, file); + fclose(file); + return { fileData, fileSize }; + } + + void fileReplace(const boost::filesystem::path &filepath, const StringView data) + { +#if OS_FAMILY == OS_FAMILY_POSIX + FILE *file = fopen(filepath.string().c_str(), "wb+"); +#else + FILE *file = _wfopen(filepath.wstring().c_str(), L"wb+"); +#endif + if(!file) + { + int error = errno; + std::string errMsg = "Failed to replace file: "; + errMsg += filepath.string(); + errMsg += ", reason: "; + errMsg += strerror(error); + throw FileException(errMsg); + } + + setbuf(file, NULL); + fwrite(data.data, 1, data.size, file); + fclose(file); + } +} diff --git a/src/Gif.cpp b/src/Gif.cpp new file mode 100644 index 0000000..0c52f7d --- /dev/null +++ b/src/Gif.cpp @@ -0,0 +1,189 @@ +#include "../include/dchat/Gif.hpp" +#include "../include/FileUtil.hpp" + +using namespace std; + +namespace dchat +{ + static void* bitmapCreate(int width, int height) + { + return calloc(width * height, 4); + } + + static void bitmapDestroy(void *bitmap) + { + free(bitmap); + } + + static unsigned char* bitmapGetBuffer(void *bitmap) + { + return (unsigned char*)bitmap; + } + + static void bitmapSetOpaque(void *bitmap, bool opaque) + { + + } + + static bool bitmapTestOpaque(void *bitmap) + { + return false; + } + + static void bitmapModified(void *bitmap) + { + + } + + static const char* gifResultToString(gif_result code) + { + switch(code) + { + case GIF_INSUFFICIENT_FRAME_DATA: + return "GIF_INSUFFICIENT_FRAME_DATA"; + case GIF_FRAME_DATA_ERROR: + return "GIF_FRAME_DATA_ERROR"; + case GIF_INSUFFICIENT_DATA: + return "GIF_INSUFFICIENT_DATA"; + case GIF_DATA_ERROR: + return "GIF_DATA_ERROR"; + case GIF_INSUFFICIENT_MEMORY: + return "GIF_INSUFFICIENT_MEMORY"; + default: + return "Unknown gif result code"; + } + } + + Gif::Gif(const boost::filesystem::path &filepath) : + currentFrame(0), + timeElapsedCs(0.0) + { + try + { + fileContent = getFileContent(filepath); + } + catch(FileException &e) + { + throw GifLoadException(e.what()); + } + + try + { + init(); + } + catch(GifLoadException &e) + { + delete[] fileContent.data; + throw e; + } + } + + Gif::Gif(StringView _fileContent) : + fileContent(move(_fileContent)), + currentFrame(0), + timeElapsedCs(0.0) + { + try + { + init(); + } + catch(GifLoadException &e) + { + delete[] fileContent.data; + throw e; + } + } + + void Gif::init() + { + gif_bitmap_callback_vt bitmapCallbacks = + { + bitmapCreate, + bitmapDestroy, + bitmapGetBuffer, + bitmapSetOpaque, + bitmapTestOpaque, + bitmapModified + }; + + gif_create(&gif, &bitmapCallbacks); + + gif_result code; + do + { + code = gif_initialise(&gif, fileContent.size, (unsigned char*)fileContent.data); + if(code != GIF_OK && code != GIF_WORKING) + { + string errMsg = "Failed to initialize gif, reason: "; + errMsg += gifResultToString(code); + throw GifLoadException(errMsg); + } + } + while(code != GIF_OK); + + if(!createTexture(gif.width, gif.height)) + throw GifLoadException("Failed to create texture for gif"); + } + + Gif::~Gif() + { + gif_finalise(&gif); + delete[] fileContent.data; + } + + Vec2u Gif::getSize() const + { + return { gif.width, gif.height }; + } + + void Gif::update() + { + double timeElapsedMilli = (double)frameTimer.getElapsedTimeMillis(); + // If gif is not redrawn for a while, then we reset timer (gif is paused). This happens when gif is not visible and then appears visible + // (because it's visible in window). The reason this is done is to prevent too much time between rendering gif frames, as processing a gif + // requires to process all frames between two points in time, if elapsed frame time is too high, then we would require to process several + // frames of gif in one application render frame. + if(timeElapsedMilli > 1000.0) + timeElapsedMilli = 0.0; + double frameDeltaCs = timeElapsedMilli * 0.1; // Centisecond + frameTimer.restart(); + timeElapsedCs += frameDeltaCs; + + unsigned char *image = nullptr; + u32 startFrame = currentFrame; + while(true) + { + u32 i = currentFrame % gif.frame_count; + gif_result code = gif_decode_frame(&gif, i); + if(code != GIF_OK) + { + printf("Warning: gif_decode_frame: %s\n", gifResultToString(code)); + break; + } + + gif_frame &frame = gif.frames[i]; + // frame_delay is in centiseconds + unsigned int frameDelay = frame.frame_delay; + if(frameDelay == 0) + frameDelay = 7; + double fFrameDelay = (double)frameDelay; + if(timeElapsedCs >= fFrameDelay) + timeElapsedCs -= fFrameDelay; + else + break; + + image = (unsigned char*)gif.frame_image; + ++currentFrame; + } + + if(currentFrame != startFrame) + { + updateTexture(image); + } + } + + bool Gif::isDataGif(const StringView &data) + { + return data.size >= 6 && (memcmp(data.data, "GIF87a", 6) == 0 || memcmp(data.data, "GIF89a", 6) == 0); + } +} diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..9ad80a6 --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char **argv) +{ + printf("hello, world!\n"); + return 0; +} -- cgit v1.2.3