#include #include #include "instaserv.h" static ServiceReference instaServ("InstaServService", "InstaServ"); class ISCreate; struct RGB { RGB(); RGB(long value); int r, g, b; }; struct ARGB: public RGB { ARGB(); ARGB(long value); double a; }; RGB::RGB() { r = g = b = 0; } RGB::RGB(long value) { const double factor = 65535.d / 255.d; r = ((unsigned char)(value >> 16)) * factor; g = ((unsigned char) (value >> 8)) * factor; b = ((unsigned char) value) * factor; } ARGB::ARGB() { a = r = g = b = 0; } ARGB::ARGB(long value): RGB(value) { a = ((unsigned char)(value >> 24) / 255.d); } struct TextItem { std::string text; int px; int py; int width; int height; TextItem(pngwriter &writer, pngwriterfont& font, const std::string& str, int strFrom, int strTo, int px, int py, int fontSize) { text = str.substr(strFrom, strTo -strFrom); this->px = px; this->py = py; width = writer.get_text_width_utf8(font, fontSize, str.c_str()); height = fontSize; } }; class TextLines { public: TextLines(char _fontSize): fontSize(_fontSize), lineSpacing(_fontSize *.55d){} void push_back(const TextItem& line) { lines.push_back(line); height += line.height; } std::vector lines; int getHeight() { return height + (lines.empty() ? 0 : (lineSpacing*(lines.size() -1))); } int getFontSize() { return fontSize; } int getLineSpacing() { return lineSpacing; } private: int height = {}; const int lineSpacing; const int fontSize; }; class ImageWriter { public: ImageWriter& SetBackground(const Anope::string& path); ImageWriter& SetText(const std::vector& text); ImageWriter& SetQuotePosition(size_t top, size_t left, size_t bottom, size_t right); ImageWriter& SetOutputPath(const Anope::string& filename); ImageWriter& SetBgColor(long); ImageWriter& SetFgColor(long); ImageWriter& SetFontSize(char, char); ImageWriter& SetOffsetTop(long); ImageWriter& SetFontPath(const Anope::string &); bool Build(User* u); protected: Anope::string background; std::vector text; Anope::string outputName; ARGB bgColor; RGB fgColor; long offsetTop; size_t top; size_t left; size_t bottom; size_t right; char minFontSize; char maxFontSize; Anope::string fontPath; void WriteText(pngwriter &writer, pngwriterfont &font, char fontSize, const TextItem& line, int offsetY, const RGB &rgb) const; bool WriteDryRun(pngwriter &writer, pngwriterfont& font, char fontSize, const Anope::string &str, TextLines& lines, const RGB &rgb); TextLines *WriteDryRun(pngwriter &writer, pngwriterfont& font, char minFontSize, char maxFontSize, const std::vector& lines, const RGB &rgb); int GetTextLength(pngwriter &writer, pngwriterfont &font, char fontSize, const std::string &str, int maxWidth) const; }; ImageWriter& ImageWriter::SetFontSize(char min, char max) { Log(LOG_DEBUG) << "Font size: [" << (unsigned) min << "; " << (unsigned) max << "]"; minFontSize = min; maxFontSize = max; return *this; } ImageWriter& ImageWriter::SetFontPath(const Anope::string& value) { Log(LOG_DEBUG) << "Font path: " << value; fontPath = value; return *this; } ImageWriter& ImageWriter::SetBackground(const Anope::string& path) { Log(LOG_DEBUG) << "Background path: " << path; background = path; return *this; } ImageWriter& ImageWriter::SetText(const std::vector& txt) { text = txt; return *this; } ImageWriter& ImageWriter::SetOutputPath(const Anope::string& path) { Log(LOG_DEBUG) << "Output path: " << path; outputName = path; return *this; } ImageWriter& ImageWriter::SetBgColor(long value) { Log(LOG_DEBUG) << "Color for background: " << value; bgColor = ARGB(value); return *this; } ImageWriter& ImageWriter::SetFgColor(long value) { Log(LOG_DEBUG) << "Color for text: " << value; fgColor = RGB(value); return *this; } ImageWriter& ImageWriter::SetOffsetTop(long value) { Log(LOG_DEBUG) << "Offset Top: " << value; offsetTop = value; return *this; } ImageWriter& ImageWriter::SetQuotePosition(size_t top, size_t left, size_t right, size_t bottom) { Log(LOG_DEBUG) << "Position: " << top << ',' << left << ',' << right << ',' << bottom; this->top = top; this->left = left; this->bottom = bottom; this->right = right; return *this; } int ImageWriter::GetTextLength(pngwriter &writer, pngwriterfont &font, char fontSize, const std::string &str, int maxWidth) const { const unsigned len = str.size(); for (unsigned i =1; i < len; ++i) { const int w = writer.get_text_width_utf8(font, fontSize, str.substr(0, i).c_str()); if (w > maxWidth) return i -1; } return len; } void ImageWriter::WriteText(pngwriter &writer, pngwriterfont &font, char fontSize, const TextItem& line, int offsetY, const RGB &rgb) const { writer.plot_text_utf8(font, fontSize, line.px, offsetY +line.py, 0, const_cast(line.text.c_str()), rgb.r, rgb.g, rgb.b); } bool ImageWriter::WriteDryRun(pngwriter &writer, pngwriterfont& font, char fontSize, const Anope::string &str, TextLines& lines, const RGB &rgb) { const char marginSize = lines.getLineSpacing(); int maxLen = GetTextLength(writer, font, fontSize, str.c_str(), right -left -2*marginSize); int written = 0; do { int py = top -lines.getHeight() -fontSize; if (!lines.lines.empty()) py -= lines.getLineSpacing(); lines.push_back(TextItem(writer, font, str.c_str(), written, written +maxLen, left +marginSize, py, fontSize)); if (lines.getHeight() +lines.getLineSpacing() > top -bottom) return false; written += maxLen; } while (written < str.length()); return true; } TextLines *ImageWriter::WriteDryRun(pngwriter &writer, pngwriterfont& font, char minFontSize, char maxFontSize, const std::vector &lines, const RGB &rgb) { TextLines *prev = nullptr; char currentFontSize = minFontSize; do { TextLines lines(currentFontSize); for (const Anope::string& str: text) if (!WriteDryRun(writer, font, currentFontSize, str, lines, fgColor)) return prev; delete prev; prev = new TextLines(lines); } while (currentFontSize++ < maxFontSize); return prev; } bool ImageWriter::Build(User *u) { std::string err; pngwriterfont font(fontPath.c_str(), err); if (!font.ready()) { Log(LOG_MODULE) << "Error: cannot load font (" << err << ")"; if (u) u->SendMessage((*instaServ)->GetBotInfo(), "Error: Cannot load font (" +err +")"); return false; } pngwriter writer(1, 1, 0, outputName.c_str()); if (!writer.readfromfile(background.c_str())) { Log(LOG_MODULE) << "Error: cannot load background image"; if (u) u->SendMessage((*instaServ)->GetBotInfo(), "Error: Cannot load background image"); return false; } writer.filledsquare_blend(left, top, right, bottom, bgColor.a, bgColor.r, bgColor.g, bgColor.b); size_t i =0; TextLines *lines; lines = WriteDryRun(writer, font, minFontSize, maxFontSize, text, fgColor); if (!lines) { Log(LOG_MODULE) << "Error: Too much text does not fit in image"; if (u) u->SendMessage((*instaServ)->GetBotInfo(), "Error: Too much text does not fit in image"); return false; } Log(LOG_DEBUG) << "Performing real write with font size = " << (unsigned) lines->getFontSize(); int offsetY = -((top +offsetTop -bottom -lines->getHeight() -lines->getLineSpacing()) /2); if (offsetY > 0) offsetY = 0; for (const TextItem& line: lines->lines) WriteText(writer, font, lines->getFontSize(), line, offsetY, fgColor); delete lines; writer.close(); chmod(outputName.c_str(), 0644); return true; } class InstaMessageBufferImpl: public InstaMessageBuffer, public Timer { std::vector lines; User *u; ISCreate *module; public: InstaMessageBufferImpl(ISCreate *m, User *user); void Tick(time_t t) anope_override; void Add(Anope::string &msg) anope_override; size_t LineCount() const anope_override; void OnEndBuffer() anope_override; }; class FileRemover: public Timer { const std::string path; public: FileRemover(const std::string &path); void Tick(time_t t) anope_override; }; class CommandISCreate : public Command { ISCreate *creator; public: CommandISCreate(ISCreate *c, const Anope::string &sname = "instaserv/create") : Command((Module*) c, sname, 0, 0), creator(c) { this->SetDesc(_("Create a new quote")); this->SetSyntax(""); } void Execute(CommandSource &source, const std::vector ¶ms) anope_override { if (!source.GetUser()) return; source.Reply("Type in your quote, end with a line only containing `EOF'"); instaServ->SetBuffer(source.GetUser(), new InstaMessageBufferImpl(creator, source.GetUser())); } bool OnHelp(CommandSource &source, const Anope::string &) anope_override { this->SendSyntax(source); source.Reply(_(" \n" "Create a new quote image")); return true; } }; class ISCreate : public Module { CommandISCreate commandiscreate; public: ISCreate(const Anope::string &modname, const Anope::string &creator); size_t GetMaxLines() { return 25; } }; ISCreate::ISCreate(const Anope::string &modname, const Anope::string &creator): Module(modname, creator, VENDOR), commandiscreate(this) {} FileRemover::FileRemover(const std::string &_path): Timer(*instaServ, Config->GetModule("is_create")->Get("expireafter", "30m")), path(_path) {} void FileRemover::Tick(time_t t) { Log(LOG_DEBUG) << "InstaServ: removing " << path; ::unlink(path.c_str()); } InstaMessageBufferImpl::InstaMessageBufferImpl(ISCreate *m, User *user) :Timer(*instaServ, Config->GetModule("is_create")->Get("typingtimeout", "5m")), u(user), module(m) {} void InstaMessageBufferImpl::Tick(time_t t) { u->SendMessage((*instaServ)->GetBotInfo(), "Error: timeout reached."); (*instaServ)->OnExpire(u); } void InstaMessageBufferImpl::Add(Anope::string &msg) { if (LineCount() < module->GetMaxLines()) lines.emplace_back(msg); else { std::stringstream ss; ss << "Cannot paste more than " << module->GetMaxLines() << " lines"; u->SendMessage((*instaServ)->GetBotInfo(), ss.str()); } } size_t InstaMessageBufferImpl::LineCount() const { return lines.size(); } static Anope::string hash(const Anope::string& username) { unsigned long long int hash = time(NULL); const char* userstr = username.c_str(); do { hash = (hash << 5) +hash +*userstr; ++userstr; } while (*userstr); std::stringstream ss; ss << std::hex << hash; return ss.str(); } static unsigned readConfigColor(const Configuration::Block& config, const Anope::string& key, unsigned def) { const Anope::string value = config.Get(key, ""); if (value == "") return def; std::stringstream ss; ss << std::hex << value.c_str(); long intVal; ss >> intVal; return intVal; } void InstaMessageBufferImpl::OnEndBuffer() { ImageWriter builder; const Configuration::Block* config = Config->GetModule("is_create"); const Anope::string filename = hash(u->nick) +".png"; const Anope::string path = config->Get("output", "") +"/" +filename; builder.SetBackground(config->Get("background", "")) .SetText(lines) .SetOutputPath(path) .SetOffsetTop(config->Get("offsetTop", "0")) .SetQuotePosition( config->Get("quoteTop", "0"), config->Get("quoteLeft", "0"), config->Get("quoteRight", "0"), config->Get("quoteBottom", "0")) .SetBgColor(readConfigColor(*config, "bgColor", 0xAAA3A5A7)) .SetFgColor(readConfigColor(*config, "fgColor", 0x474255)) .SetFontPath(config->Get("font", "")) .SetFontSize(config->Get("minFontSize", 16), config->Get("maxFontSize", 32)); if (builder.Build(u)) { u->SendMessage((*instaServ)->GetBotInfo(), "Image exported as " +config->Get("httpRoot", "") +filename); new FileRemover(path.c_str()); } (*instaServ)->OnExpire(u); TimerManager::DelTimer(this); } MODULE_INIT(ISCreate)