aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2018-05-04 23:12:54 +0200
committerdec05eba <dec05eba@protonmail.com>2018-05-04 23:12:57 +0200
commitb2f6a0235c5de32a3fcd359e28f4d1e3bd6950df (patch)
tree0cd53dcd44cf744c88ef3d0b71c2f97172d8d0d0
parent640d8df5277af4ac4b545cc6d4cf2830509e61b9 (diff)
Add web page preview (image or html content), not finished
-rw-r--r--.kdev4/dchat.kdev41
-rw-r--r--README.md2
-rw-r--r--include/Cache.hpp27
-rw-r--r--include/WebPagePreview.hpp15
-rw-r--r--src/Cache.cpp106
-rw-r--r--src/Channel.cpp2
-rw-r--r--src/MessageBoard.cpp2
-rw-r--r--src/Text.cpp129
-rw-r--r--src/WebPagePreview.cpp11
9 files changed, 228 insertions, 67 deletions
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<std::string, std::string> &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 <SFML/System/String.hpp>
+#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 <boost/filesystem/convenience.hpp>
#include <unordered_map>
#include <process.hpp>
#include <odhtdb/Hash.hpp>
#include <sibs/SafeSerializer.hpp>
#include <sibs/SafeDeserializer.hpp>
+#include <libpreview.h>
#if OS_FAMILY == OS_FAMILY_POSIX
#include <pwd.h>
@@ -22,7 +24,7 @@ using namespace TinyProcessLib;
namespace dchat
{
- unordered_map<string, ImageByUrlResult> imageUrlCache;
+ unordered_map<string, ContentByUrlResult> 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<mutex> 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 <SFML/Graphics/RectangleShape.hpp>
#include <cmath>
@@ -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<char>*)&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<char>*)&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)
+ {
+
+ }
+}