From b2f6a0235c5de32a3fcd359e28f4d1e3bd6950df Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 4 May 2018 23:12:54 +0200 Subject: Add web page preview (image or html content), not finished --- .kdev4/dchat.kdev4 | 1 + README.md | 2 - include/Cache.hpp | 27 +++++++--- include/WebPagePreview.hpp | 15 ++++++ src/Cache.cpp | 106 +++++++++++++++++++++++++------------ src/Channel.cpp | 2 +- src/MessageBoard.cpp | 2 + src/Text.cpp | 129 +++++++++++++++++++++++++++++++++++++-------- src/WebPagePreview.cpp | 11 ++++ 9 files changed, 228 insertions(+), 67 deletions(-) create mode 100644 include/WebPagePreview.hpp create mode 100644 src/WebPagePreview.cpp diff --git a/.kdev4/dchat.kdev4 b/.kdev4/dchat.kdev4 index 4d1ab04..c0f72bd 100644 --- a/.kdev4/dchat.kdev4 +++ b/.kdev4/dchat.kdev4 @@ -16,6 +16,7 @@ Name=Clang 3=/home/dec05eba/.cache/sibs/lib/sibs-serializer/0.2.0 4=/home/dec05eba/.cache/sibs/lib/ntpclient/0.2.1/include 5=/home/dec05eba/.cache/sibs/lib/fmt/4.1.0 +6=/home/dec05eba/.cache/sibs/lib/libpreview/0.1.0/include [Project] VersionControlSupport=kdevgit diff --git a/README.md b/README.md index 8900e0c..7eaff6b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # DcHaT Decentralized chat using odhtdb. -Max emoji file size is 512kb. - Currently only runs on GNU/Linux. # Dependencies opendht, boost, libnsgif, mpv, sfml diff --git a/include/Cache.hpp b/include/Cache.hpp index 71226cc..311658b 100644 --- a/include/Cache.hpp +++ b/include/Cache.hpp @@ -16,8 +16,9 @@ namespace TinyProcessLib namespace dchat { class Gif; + class WebPagePreview; - struct ImageByUrlResult + struct ContentByUrlResult { enum class Type { @@ -26,19 +27,29 @@ namespace dchat FAILED_DOWNLOAD }; - ImageByUrlResult() : texture(nullptr), type(Type::DOWNLOADING), isGif(false) {} - ImageByUrlResult(sf::Texture *_texture, Type _type) : texture(_texture), type(_type), isGif(false) {} - ImageByUrlResult(Gif *_gif, Type _type) : gif(_gif), type(_type), isGif(true) {} + enum class CachedType + { + NONE, + TEXTURE, + GIF, + WEB_PAGE_PREVIEW + }; + + ContentByUrlResult() : texture(nullptr), type(Type::DOWNLOADING), cachedType(CachedType::NONE) {} + ContentByUrlResult(sf::Texture *_texture, Type _type) : texture(_texture), type(_type), cachedType(CachedType::TEXTURE) {} + 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 { sf::Texture *texture; Gif *gif; + WebPagePreview *webPagePreview; }; Type type; - bool isGif; + CachedType cachedType; }; class Cache @@ -56,10 +67,10 @@ namespace dchat static void loadBindsFromFile(); static void replaceBindsInFile(const std::unordered_map &binds); - // Get cached image or downloads it. + // Get cached content or download it. // Default download file limit is 12MB - // Returns ImageByUrlResult describing texture status. - const ImageByUrlResult getImageByUrl(const std::string &url, int downloadLimitBytes = 12582912); + // Returns ContentByUrlResult describing texture status. + const ContentByUrlResult getContentByUrl(const std::string &url, int downloadLimitBytes = 12582912); private: struct ImageDownloadInfo { diff --git a/include/WebPagePreview.hpp b/include/WebPagePreview.hpp new file mode 100644 index 0000000..749308c --- /dev/null +++ b/include/WebPagePreview.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "Text.hpp" + +namespace dchat +{ + class WebPagePreview + { + public: + WebPagePreview(const sf::String &title); + + Text title; + }; +} diff --git a/src/Cache.cpp b/src/Cache.cpp index 0610a18..cba346b 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -4,12 +4,14 @@ #include "../include/FileUtil.hpp" #include "../include/Gif.hpp" #include "../include/Chatbar.hpp" +#include "../include/WebPagePreview.hpp" #include #include #include #include #include #include +#include #if OS_FAMILY == OS_FAMILY_POSIX #include @@ -22,7 +24,7 @@ using namespace TinyProcessLib; namespace dchat { - unordered_map imageUrlCache; + unordered_map contentUrlCache; boost::filesystem::path getHomeDir() { @@ -115,38 +117,71 @@ namespace dchat fileReplace(getDchatDir() / "binds", StringView((const char*)serializer.getBuffer().data(), serializer.getBuffer().size())); } - ImageByUrlResult loadImageFromFile(const boost::filesystem::path &filepath) + static ContentByUrlResult loadImageFromFile(const boost::filesystem::path &filepath) { StringView fileContent; try { fileContent = getFileContent(filepath); - if(Gif::isDataGif(fileContent)) - { - Gif *gif = new Gif(move(fileContent)); - return { gif, ImageByUrlResult::Type::CACHED }; - } - else + + sf::String webPageTitle; + bool foundHtmlContent = false; + preview_state state; + preview_init(&state); + size_t offset = 0; + do { - sf::Texture *texture = new sf::Texture(); - if(texture->loadFromMemory(fileContent.data, fileContent.size)) + // 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) { - delete fileContent.data; - fileContent.data = nullptr; - texture->setSmooth(true); - texture->generateMipmap(); - return { texture, ImageByUrlResult::Type::CACHED }; + if(Gif::isDataGif(fileContent)) + { + Gif *gif = new Gif(move(fileContent)); + return { gif, ContentByUrlResult::Type::CACHED }; + } + else + { + sf::Texture *texture = new sf::Texture(); + if(texture->loadFromMemory(fileContent.data, fileContent.size)) + { + delete fileContent.data; + fileContent.data = nullptr; + texture->setSmooth(true); + texture->generateMipmap(); + return { texture, ContentByUrlResult::Type::CACHED }; + } + delete texture; + } + break; } - delete texture; - delete fileContent.data; - fileContent.data = nullptr; + else if(state.step_result == PREVIEW_FOUND_TITLE) + { + foundHtmlContent = true; + webPageTitle = sf::String::fromUtf8(state.title, state.title + state.title_length); + } + else if(state.step_result == PREVIEW_FOUND_PARAGRAPH) + { + foundHtmlContent = true; + } + } while(offset < fileContent.size); + + delete fileContent.data; + fileContent.data = nullptr; + + if(foundHtmlContent) + { + // TODO: Use move semantics for webPageTitle when SFML supports it + WebPagePreview *webPagePreview = new WebPagePreview(webPageTitle); + 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 { (sf::Texture*)nullptr, ImageByUrlResult::Type::FAILED_DOWNLOAD }; + return { (sf::Texture*)nullptr, ContentByUrlResult::Type::FAILED_DOWNLOAD }; } Cache::Cache() : @@ -168,13 +203,13 @@ namespace dchat odhtdb::Hash urlHash(it->url.data(), it->url.size()); filepath /= urlHash.toString(); - ImageByUrlResult imageByUrlResult = loadImageFromFile(filepath); + ContentByUrlResult contentByUrlResult = loadImageFromFile(filepath); imageDownloadMutex.lock(); - imageUrlCache[it->url] = imageByUrlResult; + contentUrlCache[it->url] = contentByUrlResult; imageDownloadMutex.unlock(); - if(imageByUrlResult.type == ImageByUrlResult::Type::CACHED) + if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) { - printf("Download %s from url: %s\n", imageByUrlResult.isGif ? "gif" : "image", it->url.c_str()); + printf("Download content from url: %s\n", it->url.c_str()); } } it = imageDownloadProcesses.erase(it); @@ -208,11 +243,11 @@ namespace dchat downloadWaitThread.join(); } - const ImageByUrlResult Cache::getImageByUrl(const string &url, int downloadLimitBytes) + const ContentByUrlResult Cache::getContentByUrl(const string &url, int downloadLimitBytes) { lock_guard lock(imageDownloadMutex); - auto it = imageUrlCache.find(url); - if(it != imageUrlCache.end()) + auto it = contentUrlCache.find(url); + if(it != contentUrlCache.end()) return it->second; // TODO: Verify hashed url is not too long for filepath on windows @@ -220,16 +255,21 @@ namespace dchat odhtdb::Hash urlHash(url.data(), url.size()); filepath /= urlHash.toString(); - ImageByUrlResult imageByUrlResult = loadImageFromFile(filepath); - if(imageByUrlResult.type == ImageByUrlResult::Type::CACHED) + ContentByUrlResult contentByUrlResult = loadImageFromFile(filepath); + if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) + { + 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)) { - imageUrlCache[url] = imageByUrlResult; - printf("Loaded image from file cache: %s, is gif: %s\n", url.c_str(), imageByUrlResult.isGif ? "yes" : "no"); - return imageByUrlResult; + contentUrlCache[url] = contentByUrlResult; + return contentByUrlResult; } - ImageByUrlResult result((sf::Texture*)nullptr, ImageByUrlResult::Type::DOWNLOADING); - imageUrlCache[url] = result; + ContentByUrlResult result((sf::Texture*)nullptr, ContentByUrlResult::Type::DOWNLOADING); + contentUrlCache[url] = result; string downloadLimitBytesStr = to_string(downloadLimitBytes); diff --git a/src/Channel.cpp b/src/Channel.cpp index e25057a..f8e79c6 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -30,7 +30,7 @@ namespace dchat } { - Message *message = new Message(&systemUser, u8"pepedab https://discordemoji.com/assets/emoji/PepeDab.gif coggers https://discordemoji.com/assets/emoji/COGGERS.gif"); + Message *message = new Message(&systemUser, u8"pepedab https://discordemoji.com/assets/emoji/PepeDab.gif coggers https://discordemoji.com/assets/emoji/COGGERS.gif check out this url http://www.grandtournation.com/6808/start-date-of-the-grand-tour-season-3-confirmed-mark-your-calendars/ owo"); messageBoard.addMessage(message); } diff --git a/src/MessageBoard.cpp b/src/MessageBoard.cpp index 250f234..cee3e50 100644 --- a/src/MessageBoard.cpp +++ b/src/MessageBoard.cpp @@ -68,6 +68,7 @@ namespace dchat void MessageBoard::drawDefault(sf::RenderWindow &window, Cache &cache) { + const float LINE_SPACING = 5.0f * Settings::getScaling(); const float MESSAGE_PADDING_TOP = 25.0f; const float MESSAGE_PADDING_BOTTOM = 30.0f; @@ -116,6 +117,7 @@ namespace dchat message->text.setCharacterSize(18 * Settings::getScaling()); message->text.setMaxWidth(backgroundSize.x); message->text.setPosition(sf::Vector2f(floor(position.x), floor(position.y))); + message->text.setLineSpacing(LINE_SPACING); message->text.draw(window, cache); position.y += (message->text.getHeight() + MESSAGE_PADDING_BOTTOM * Settings::getScaling()); diff --git a/src/Text.cpp b/src/Text.cpp index c433ddc..bc23235 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -1,6 +1,7 @@ #include "../include/Text.hpp" #include "../include/Cache.hpp" #include "../include/Gif.hpp" +#include "../include/WebPagePreview.hpp" #include #include @@ -10,6 +11,7 @@ namespace dchat const float EMOJI_PADDING = 5.0f; const float EMOJI_SCALE_WITH_TEXT = 1.7f; const float EMOJI_SCALE_STANDALONE = 5.0f; + const float IMAGE_HEIGHT_SCALE = 15.0f; const sf::Color URL_COLOR(15, 192, 252); @@ -40,7 +42,6 @@ namespace dchat { setString(_str); } - void Text::setString(const sf::String &str) { if(str != this->str) @@ -101,7 +102,7 @@ namespace dchat void Text::setLineSpacing(float lineSpacing) { - if(lineSpacing != this->lineSpacing) + if(fabs(lineSpacing - this->lineSpacing) > 0.001f) { this->lineSpacing = lineSpacing; dirty = true; @@ -414,8 +415,22 @@ namespace dchat glyphPos.x += glyph.advance; } + + if(textElement.type != TextElement::Type::TEXT) + { + prevCodePoint = 0; + } + + if(textElement.type == TextElement::Type::URL) + { + glyphPos.y += vspace + lineSpacing; + textElement.position.y = glyphPos.y; + + glyphPos.x = 0.0f; + glyphPos.y += floor(vspace * IMAGE_HEIGHT_SCALE) + lineSpacing; + } } - totalHeight = glyphPos.y + lineSpacing + vspace; + totalHeight = glyphPos.y + vspace + lineSpacing; } void Text::draw(sf::RenderTarget &target, Cache &cache) @@ -447,43 +462,111 @@ namespace dchat { if(textElement.type == TextElement::Type::EMOJI) { + float emojiSize = vspace * (textElement.ownLine ? EMOJI_SCALE_STANDALONE : EMOJI_SCALE_WITH_TEXT); + sf::Vector2f pos = position; pos += textElement.position; pos.x = floor(pos.x); pos.y = floor(pos.y); - float emojiSize = vspace * (textElement.ownLine ? EMOJI_SCALE_STANDALONE : EMOJI_SCALE_WITH_TEXT); sf::Vector2f size(emojiSize, emojiSize); // TODO: Optimize this (add unordered_map that takes StringViewUtf32 as key) auto u8Str = sf::String::fromUtf32(textElement.text.data, textElement.text.data + textElement.text.size).toUtf8(); const std::string &utf8Str = *(std::basic_string*)&u8Str; - const ImageByUrlResult imageByUrlResult = cache.getImageByUrl(utf8Str); - if(imageByUrlResult.type == ImageByUrlResult::Type::CACHED) + const ContentByUrlResult contentByUrlResult = cache.getContentByUrl(utf8Str); + if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) { - if(imageByUrlResult.isGif) + switch(contentByUrlResult.cachedType) { - auto gifSize = imageByUrlResult.gif->getSize(); - float widthToHeightRatio = (float)gifSize.x / (float)gifSize.y; - - imageByUrlResult.gif->setPosition(pos); - imageByUrlResult.gif->setScale(sf::Vector2f(size.x / (float)gifSize.x * widthToHeightRatio, size.y / (float)gifSize.y)); - imageByUrlResult.gif->draw(target); + case ContentByUrlResult::CachedType::GIF: + { + auto gifSize = contentByUrlResult.gif->getSize(); + float widthToHeightRatio = (float)gifSize.x / (float)gifSize.y; + + contentByUrlResult.gif->setPosition(pos); + contentByUrlResult.gif->setScale(sf::Vector2f(size.x / (float)gifSize.x * widthToHeightRatio, size.y / (float)gifSize.y)); + contentByUrlResult.gif->draw(target); + break; + } + case ContentByUrlResult::CachedType::TEXTURE: + { + auto textureSize = contentByUrlResult.texture->getSize(); + float widthToHeightRatio = (float)textureSize.x / (float)textureSize.y; + + // TODO: Store this sprite somewhere, might not be efficient to create a new sprite object every frame + sf::Sprite sprite(*contentByUrlResult.texture); + sprite.setPosition(pos); + sprite.setScale(size.x / (float)textureSize.x * widthToHeightRatio, size.y / (float)textureSize.y); + target.draw(sprite); + break; + } + default: + // Ignore html in emoji.... + break; } - else + } + else + { + sf::RectangleShape rect(size); + rect.setFillColor(sf::Color::White); + rect.setPosition(pos); + target.draw(rect); + } + } + else if(textElement.type == TextElement::Type::URL) + { + sf::Vector2f pos = position; + pos.y += floor(textElement.position.y); + float imageHeight = floor(vspace * IMAGE_HEIGHT_SCALE); + + // TODO: Optimize this (add unordered_map that takes StringViewUtf32 as key) + auto u8Str = sf::String::fromUtf32(textElement.text.data, textElement.text.data + textElement.text.size).toUtf8(); + const std::string &utf8Str = *(std::basic_string*)&u8Str; + const ContentByUrlResult contentByUrlResult = cache.getContentByUrl(utf8Str); + if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) + { + switch(contentByUrlResult.cachedType) { - auto textureSize = imageByUrlResult.texture->getSize(); - float widthToHeightRatio = (float)textureSize.x / (float)textureSize.y; - - // TODO: Store this sprite somewhere, might not be efficient to create a new sprite object every frame - sf::Sprite sprite(*imageByUrlResult.texture); - sprite.setPosition(pos); - sprite.setScale(size.x / (float)textureSize.x * widthToHeightRatio, size.y / (float)textureSize.y); - target.draw(sprite); + case ContentByUrlResult::CachedType::GIF: + { + auto gifSize = contentByUrlResult.gif->getSize(); + float widthToHeightRatio = (float)gifSize.x / (float)gifSize.y; + + contentByUrlResult.gif->setPosition(pos); + contentByUrlResult.gif->setScale(sf::Vector2f(imageHeight / (float)gifSize.x * widthToHeightRatio, imageHeight / (float)gifSize.y)); + contentByUrlResult.gif->draw(target); + break; + } + case ContentByUrlResult::CachedType::TEXTURE: + { + auto textureSize = contentByUrlResult.texture->getSize(); + float widthToHeightRatio = (float)textureSize.x / (float)textureSize.y; + + // TODO: Store this sprite somewhere, might not be efficient to create a new sprite object every frame + sf::Sprite sprite(*contentByUrlResult.texture); + sprite.setPosition(pos); + sprite.setScale(imageHeight / (float)textureSize.x * widthToHeightRatio, imageHeight / (float)textureSize.y); + target.draw(sprite); + break; + } + case ContentByUrlResult::CachedType::WEB_PAGE_PREVIEW: + { + const float previewWidth = floor(imageHeight * 1.77f); + + // No need to perform culling here, that is done in @Text draw function + contentByUrlResult.webPagePreview->title.setCharacterSize(characterSize); + contentByUrlResult.webPagePreview->title.setMaxWidth(previewWidth); + contentByUrlResult.webPagePreview->title.setPosition(pos); + contentByUrlResult.webPagePreview->title.setLineSpacing(0.0f); + contentByUrlResult.webPagePreview->title.setFillColor(URL_COLOR); + contentByUrlResult.webPagePreview->title.draw(target, cache); + break; + } } } else { - sf::RectangleShape rect(size); + sf::RectangleShape rect(sf::Vector2f(imageHeight, imageHeight)); rect.setFillColor(sf::Color::White); rect.setPosition(pos); target.draw(rect); diff --git a/src/WebPagePreview.cpp b/src/WebPagePreview.cpp new file mode 100644 index 0000000..0d23285 --- /dev/null +++ b/src/WebPagePreview.cpp @@ -0,0 +1,11 @@ +#include "../include/WebPagePreview.hpp" +#include "../include/ResourceCache.hpp" + +namespace dchat +{ + WebPagePreview::WebPagePreview(const sf::String &_title) : + title(_title, ResourceCache::getFont("fonts/Roboto-Regular.ttf"), 10, 0) + { + + } +} -- cgit v1.2.3