is_create.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. #include <sys/stat.h>
  2. #include <pngwriter.h>
  3. #include "instaserv.h"
  4. static ServiceReference<InstaServCore> instaServ("InstaServService", "InstaServ");
  5. class ISCreate;
  6. struct RGB
  7. {
  8. RGB();
  9. RGB(long value);
  10. int r, g, b;
  11. };
  12. struct ARGB: public RGB
  13. {
  14. ARGB();
  15. ARGB(long value);
  16. double a;
  17. };
  18. RGB::RGB()
  19. {
  20. r = g = b = 0;
  21. }
  22. RGB::RGB(long value)
  23. {
  24. const double factor = 65535.d / 255.d;
  25. r = ((unsigned char)(value >> 16)) * factor;
  26. g = ((unsigned char) (value >> 8)) * factor;
  27. b = ((unsigned char) value) * factor;
  28. }
  29. ARGB::ARGB()
  30. {
  31. a = r = g = b = 0;
  32. }
  33. ARGB::ARGB(long value): RGB(value)
  34. {
  35. a = ((unsigned char)(value >> 24) / 255.d);
  36. }
  37. struct TextItem {
  38. std::string text;
  39. int px;
  40. int py;
  41. int width;
  42. int height;
  43. TextItem(pngwriter &writer, pngwriterfont& font, const std::string& str, int strFrom, int strTo, int px, int py, int fontSize)
  44. {
  45. text = str.substr(strFrom, strTo -strFrom);
  46. this->px = px;
  47. this->py = py;
  48. width = writer.get_text_width_utf8(font, fontSize, str.c_str());
  49. height = fontSize;
  50. }
  51. };
  52. class TextLines
  53. {
  54. public:
  55. TextLines(char _fontSize): fontSize(_fontSize), lineSpacing(_fontSize *.55d){}
  56. void push_back(const TextItem& line)
  57. {
  58. lines.push_back(line);
  59. height += line.height;
  60. }
  61. std::vector<TextItem> lines;
  62. int getHeight() { return height + (lines.empty() ? 0 : (lineSpacing*(lines.size() -1))); }
  63. int getFontSize() { return fontSize; }
  64. int getLineSpacing() { return lineSpacing; }
  65. private:
  66. int height = {};
  67. const int lineSpacing;
  68. const int fontSize;
  69. };
  70. class ImageWriter
  71. {
  72. public:
  73. ImageWriter& SetBackground(const Anope::string& path);
  74. ImageWriter& SetText(const std::vector<Anope::string>& text);
  75. ImageWriter& SetQuotePosition(size_t top, size_t left, size_t bottom, size_t right);
  76. ImageWriter& SetOutputPath(const Anope::string& filename);
  77. ImageWriter& SetBgColor(long);
  78. ImageWriter& SetFgColor(long);
  79. ImageWriter& SetFontSize(char, char);
  80. ImageWriter& SetOffsetTop(long);
  81. ImageWriter& SetFontPath(const Anope::string &);
  82. bool Build(User* u);
  83. protected:
  84. Anope::string background;
  85. std::vector<Anope::string> text;
  86. Anope::string outputName;
  87. ARGB bgColor;
  88. RGB fgColor;
  89. long offsetTop;
  90. size_t top;
  91. size_t left;
  92. size_t bottom;
  93. size_t right;
  94. char minFontSize;
  95. char maxFontSize;
  96. Anope::string fontPath;
  97. void WriteText(pngwriter &writer, pngwriterfont &font, char fontSize, const TextItem& line, int offsetY, const RGB &rgb) const;
  98. bool WriteDryRun(pngwriter &writer, pngwriterfont& font, char fontSize, const Anope::string &str, TextLines& lines, const RGB &rgb);
  99. TextLines *WriteDryRun(pngwriter &writer, pngwriterfont& font, char minFontSize, char maxFontSize, const std::vector<Anope::string>& lines, const RGB &rgb);
  100. int GetTextLength(pngwriter &writer, pngwriterfont &font, char fontSize, const std::string &str, int maxWidth) const;
  101. };
  102. ImageWriter& ImageWriter::SetFontSize(char min, char max)
  103. {
  104. Log(LOG_DEBUG) << "Font size: [" << (unsigned) min << "; " << (unsigned) max << "]";
  105. minFontSize = min;
  106. maxFontSize = max;
  107. return *this;
  108. }
  109. ImageWriter& ImageWriter::SetFontPath(const Anope::string& value)
  110. {
  111. Log(LOG_DEBUG) << "Font path: " << value;
  112. fontPath = value;
  113. return *this;
  114. }
  115. ImageWriter& ImageWriter::SetBackground(const Anope::string& path)
  116. {
  117. Log(LOG_DEBUG) << "Background path: " << path;
  118. background = path;
  119. return *this;
  120. }
  121. ImageWriter& ImageWriter::SetText(const std::vector<Anope::string>& txt)
  122. {
  123. text = txt;
  124. return *this;
  125. }
  126. ImageWriter& ImageWriter::SetOutputPath(const Anope::string& path)
  127. {
  128. Log(LOG_DEBUG) << "Output path: " << path;
  129. outputName = path;
  130. return *this;
  131. }
  132. ImageWriter& ImageWriter::SetBgColor(long value)
  133. {
  134. Log(LOG_DEBUG) << "Color for background: " << value;
  135. bgColor = ARGB(value);
  136. return *this;
  137. }
  138. ImageWriter& ImageWriter::SetFgColor(long value)
  139. {
  140. Log(LOG_DEBUG) << "Color for text: " << value;
  141. fgColor = RGB(value);
  142. return *this;
  143. }
  144. ImageWriter& ImageWriter::SetOffsetTop(long value)
  145. {
  146. Log(LOG_DEBUG) << "Offset Top: " << value;
  147. offsetTop = value;
  148. return *this;
  149. }
  150. ImageWriter& ImageWriter::SetQuotePosition(size_t top, size_t left, size_t right, size_t bottom)
  151. {
  152. Log(LOG_DEBUG) << "Position: " << top << ',' << left << ',' << right << ',' << bottom;
  153. this->top = top;
  154. this->left = left;
  155. this->bottom = bottom;
  156. this->right = right;
  157. return *this;
  158. }
  159. int ImageWriter::GetTextLength(pngwriter &writer, pngwriterfont &font, char fontSize, const std::string &str, int maxWidth) const
  160. {
  161. const unsigned len = str.size();
  162. for (unsigned i =1; i < len; ++i) {
  163. const int w = writer.get_text_width_utf8(font, fontSize, str.substr(0, i).c_str());
  164. if (w > maxWidth)
  165. return i -1;
  166. }
  167. return len;
  168. }
  169. void ImageWriter::WriteText(pngwriter &writer, pngwriterfont &font, char fontSize, const TextItem& line, int offsetY, const RGB &rgb) const
  170. {
  171. writer.plot_text_utf8(font, fontSize, line.px, offsetY +line.py, 0, const_cast<char*>(line.text.c_str()), rgb.r, rgb.g, rgb.b);
  172. }
  173. bool ImageWriter::WriteDryRun(pngwriter &writer, pngwriterfont& font, char fontSize, const Anope::string &str, TextLines& lines, const RGB &rgb)
  174. {
  175. const char marginSize = lines.getLineSpacing();
  176. int maxLen = GetTextLength(writer, font, fontSize, str.c_str(), right -left -2*marginSize);
  177. int written = 0;
  178. do {
  179. int py = top -lines.getHeight() -fontSize;
  180. if (!lines.lines.empty())
  181. py -= lines.getLineSpacing();
  182. lines.push_back(TextItem(writer, font, str.c_str(), written, written +maxLen, left +marginSize, py, fontSize));
  183. if (lines.getHeight() +lines.getLineSpacing() > top -bottom)
  184. return false;
  185. written += maxLen;
  186. } while (written < str.length());
  187. return true;
  188. }
  189. TextLines *ImageWriter::WriteDryRun(pngwriter &writer, pngwriterfont& font, char minFontSize, char maxFontSize, const std::vector<Anope::string> &lines, const RGB &rgb)
  190. {
  191. TextLines *prev = nullptr;
  192. char currentFontSize = minFontSize;
  193. do {
  194. TextLines lines(currentFontSize);
  195. for (const Anope::string& str: text)
  196. if (!WriteDryRun(writer, font, currentFontSize, str, lines, fgColor))
  197. return prev;
  198. delete prev;
  199. prev = new TextLines(lines);
  200. } while (currentFontSize++ < maxFontSize);
  201. return prev;
  202. }
  203. bool ImageWriter::Build(User *u)
  204. {
  205. std::string err;
  206. pngwriterfont font(fontPath.c_str(), err);
  207. if (!font.ready())
  208. {
  209. Log(LOG_MODULE) << "Error: cannot load font (" << err << ")";
  210. if (u)
  211. u->SendMessage((*instaServ)->GetBotInfo(), "Error: Cannot load font (" +err +")");
  212. return false;
  213. }
  214. pngwriter writer(1, 1, 0, outputName.c_str());
  215. if (!writer.readfromfile(background.c_str()))
  216. {
  217. Log(LOG_MODULE) << "Error: cannot load background image";
  218. if (u)
  219. u->SendMessage((*instaServ)->GetBotInfo(), "Error: Cannot load background image");
  220. return false;
  221. }
  222. writer.filledsquare_blend(left, top, right, bottom,
  223. bgColor.a, bgColor.r, bgColor.g, bgColor.b);
  224. size_t i =0;
  225. TextLines *lines;
  226. lines = WriteDryRun(writer, font, minFontSize, maxFontSize, text, fgColor);
  227. if (!lines)
  228. {
  229. Log(LOG_MODULE) << "Error: Too much text does not fit in image";
  230. if (u)
  231. u->SendMessage((*instaServ)->GetBotInfo(), "Error: Too much text does not fit in image");
  232. return false;
  233. }
  234. Log(LOG_DEBUG) << "Performing real write with font size = " << (unsigned) lines->getFontSize();
  235. int offsetY = -((top +offsetTop -bottom -lines->getHeight() -lines->getLineSpacing()) /2);
  236. if (offsetY > 0)
  237. offsetY = 0;
  238. for (const TextItem& line: lines->lines)
  239. WriteText(writer, font, lines->getFontSize(), line, offsetY, fgColor);
  240. delete lines;
  241. writer.close();
  242. chmod(outputName.c_str(), 0644);
  243. return true;
  244. }
  245. class InstaMessageBufferImpl: public InstaMessageBuffer, public Timer
  246. {
  247. std::vector<Anope::string> lines;
  248. User *u;
  249. ISCreate *module;
  250. public:
  251. InstaMessageBufferImpl(ISCreate *m, User *user);
  252. void Tick(time_t t) anope_override;
  253. void Add(Anope::string &msg) anope_override;
  254. size_t LineCount() const anope_override;
  255. void OnEndBuffer() anope_override;
  256. };
  257. class FileRemover: public Timer
  258. {
  259. const std::string path;
  260. public:
  261. FileRemover(const std::string &path);
  262. void Tick(time_t t) anope_override;
  263. };
  264. class CommandISCreate : public Command
  265. {
  266. ISCreate *creator;
  267. public:
  268. CommandISCreate(ISCreate *c, const Anope::string &sname = "instaserv/create") : Command((Module*) c, sname, 0, 0), creator(c)
  269. {
  270. this->SetDesc(_("Create a new quote"));
  271. this->SetSyntax("");
  272. }
  273. void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
  274. {
  275. if (!source.GetUser())
  276. return;
  277. source.Reply("Type in your quote, end with a line only containing `EOF'");
  278. instaServ->SetBuffer(source.GetUser(), new InstaMessageBufferImpl(creator, source.GetUser()));
  279. }
  280. bool OnHelp(CommandSource &source, const Anope::string &) anope_override
  281. {
  282. this->SendSyntax(source);
  283. source.Reply(_(" \n"
  284. "Create a new quote image"));
  285. return true;
  286. }
  287. };
  288. class ISCreate : public Module
  289. {
  290. CommandISCreate commandiscreate;
  291. public:
  292. ISCreate(const Anope::string &modname, const Anope::string &creator);
  293. size_t GetMaxLines()
  294. {
  295. return 25;
  296. }
  297. };
  298. ISCreate::ISCreate(const Anope::string &modname, const Anope::string &creator): Module(modname, creator, VENDOR), commandiscreate(this)
  299. {}
  300. FileRemover::FileRemover(const std::string &_path): Timer(*instaServ, Config->GetModule("is_create")->Get<time_t>("expireafter", "30m")), path(_path)
  301. {}
  302. void FileRemover::Tick(time_t t)
  303. {
  304. Log(LOG_DEBUG) << "InstaServ: removing " << path;
  305. ::unlink(path.c_str());
  306. }
  307. InstaMessageBufferImpl::InstaMessageBufferImpl(ISCreate *m, User *user) :Timer(*instaServ, Config->GetModule("is_create")->Get<time_t>("typingtimeout", "5m")), u(user), module(m)
  308. {}
  309. void InstaMessageBufferImpl::Tick(time_t t)
  310. {
  311. u->SendMessage((*instaServ)->GetBotInfo(), "Error: timeout reached.");
  312. (*instaServ)->OnExpire(u);
  313. }
  314. void InstaMessageBufferImpl::Add(Anope::string &msg)
  315. {
  316. if (LineCount() < module->GetMaxLines())
  317. lines.emplace_back(msg);
  318. else
  319. {
  320. std::stringstream ss;
  321. ss << "Cannot paste more than " << module->GetMaxLines() << " lines";
  322. u->SendMessage((*instaServ)->GetBotInfo(), ss.str());
  323. }
  324. }
  325. size_t InstaMessageBufferImpl::LineCount() const
  326. {
  327. return lines.size();
  328. }
  329. static Anope::string hash(const Anope::string& username)
  330. {
  331. unsigned long long int hash = time(NULL);
  332. const char* userstr = username.c_str();
  333. do {
  334. hash = (hash << 5) +hash +*userstr;
  335. ++userstr;
  336. } while (*userstr);
  337. std::stringstream ss;
  338. ss << std::hex << hash;
  339. return ss.str();
  340. }
  341. static unsigned readConfigColor(const Configuration::Block& config, const Anope::string& key, unsigned def)
  342. {
  343. const Anope::string value = config.Get<Anope::string>(key, "");
  344. if (value == "")
  345. return def;
  346. std::stringstream ss;
  347. ss << std::hex << value.c_str();
  348. long intVal;
  349. ss >> intVal;
  350. return intVal;
  351. }
  352. void InstaMessageBufferImpl::OnEndBuffer()
  353. {
  354. ImageWriter builder;
  355. const Configuration::Block* config = Config->GetModule("is_create");
  356. const Anope::string filename = hash(u->nick) +".png";
  357. const Anope::string path = config->Get<Anope::string>("output", "") +"/" +filename;
  358. builder.SetBackground(config->Get<Anope::string>("background", ""))
  359. .SetText(lines)
  360. .SetOutputPath(path)
  361. .SetOffsetTop(config->Get<long>("offsetTop", "0"))
  362. .SetQuotePosition(
  363. config->Get<unsigned>("quoteTop", "0"),
  364. config->Get<unsigned>("quoteLeft", "0"),
  365. config->Get<unsigned>("quoteRight", "0"),
  366. config->Get<unsigned>("quoteBottom", "0"))
  367. .SetBgColor(readConfigColor(*config, "bgColor", 0xAAA3A5A7))
  368. .SetFgColor(readConfigColor(*config, "fgColor", 0x474255))
  369. .SetFontPath(config->Get<Anope::string>("font", ""))
  370. .SetFontSize(config->Get<unsigned>("minFontSize", 16), config->Get<unsigned>("maxFontSize", 32));
  371. if (builder.Build(u))
  372. {
  373. u->SendMessage((*instaServ)->GetBotInfo(), "Image exported as " +config->Get<Anope::string>("httpRoot", "") +filename);
  374. new FileRemover(path.c_str());
  375. }
  376. (*instaServ)->OnExpire(u);
  377. TimerManager::DelTimer(this);
  378. }
  379. MODULE_INIT(ISCreate)