web_buffer_svg.c 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142
  1. // SPDX-License-Identifier: GPL-3.0-or-later
  2. #include "web_buffer_svg.h"
  3. #define BADGE_HORIZONTAL_PADDING 4
  4. #define VERDANA_KERNING 0.2
  5. #define VERDANA_PADDING 1.0
  6. /*
  7. * verdana11_widths[] has been generated with this method:
  8. * https://github.com/badges/shields/blob/master/measure-text.js
  9. */
  10. static double verdana11_widths[256] = {
  11. [0] = 0.0,
  12. [1] = 0.0,
  13. [2] = 0.0,
  14. [3] = 0.0,
  15. [4] = 0.0,
  16. [5] = 0.0,
  17. [6] = 0.0,
  18. [7] = 0.0,
  19. [8] = 0.0,
  20. [9] = 0.0,
  21. [10] = 0.0,
  22. [11] = 0.0,
  23. [12] = 0.0,
  24. [13] = 0.0,
  25. [14] = 0.0,
  26. [15] = 0.0,
  27. [16] = 0.0,
  28. [17] = 0.0,
  29. [18] = 0.0,
  30. [19] = 0.0,
  31. [20] = 0.0,
  32. [21] = 0.0,
  33. [22] = 0.0,
  34. [23] = 0.0,
  35. [24] = 0.0,
  36. [25] = 0.0,
  37. [26] = 0.0,
  38. [27] = 0.0,
  39. [28] = 0.0,
  40. [29] = 0.0,
  41. [30] = 0.0,
  42. [31] = 0.0,
  43. [32] = 3.8671874999999996, //
  44. [33] = 4.3291015625, // !
  45. [34] = 5.048828125, // "
  46. [35] = 9.001953125, // #
  47. [36] = 6.9931640625, // $
  48. [37] = 11.837890625, // %
  49. [38] = 7.992187499999999, // &
  50. [39] = 2.9541015625, // '
  51. [40] = 4.9951171875, // (
  52. [41] = 4.9951171875, // )
  53. [42] = 6.9931640625, // *
  54. [43] = 9.001953125, // +
  55. [44] = 4.00146484375, // ,
  56. [45] = 4.9951171875, // -
  57. [46] = 4.00146484375, // .
  58. [47] = 4.9951171875, // /
  59. [48] = 6.9931640625, // 0
  60. [49] = 6.9931640625, // 1
  61. [50] = 6.9931640625, // 2
  62. [51] = 6.9931640625, // 3
  63. [52] = 6.9931640625, // 4
  64. [53] = 6.9931640625, // 5
  65. [54] = 6.9931640625, // 6
  66. [55] = 6.9931640625, // 7
  67. [56] = 6.9931640625, // 8
  68. [57] = 6.9931640625, // 9
  69. [58] = 4.9951171875, // :
  70. [59] = 4.9951171875, // ;
  71. [60] = 9.001953125, // <
  72. [61] = 9.001953125, // =
  73. [62] = 9.001953125, // >
  74. [63] = 5.99951171875, // ?
  75. [64] = 11.0, // @
  76. [65] = 7.51953125, // A
  77. [66] = 7.541015625, // B
  78. [67] = 7.680664062499999, // C
  79. [68] = 8.4755859375, // D
  80. [69] = 6.95556640625, // E
  81. [70] = 6.32177734375, // F
  82. [71] = 8.529296875, // G
  83. [72] = 8.26611328125, // H
  84. [73] = 4.6298828125, // I
  85. [74] = 5.00048828125, // J
  86. [75] = 7.62158203125, // K
  87. [76] = 6.123046875, // L
  88. [77] = 9.2705078125, // M
  89. [78] = 8.228515625, // N
  90. [79] = 8.658203125, // O
  91. [80] = 6.63330078125, // P
  92. [81] = 8.658203125, // Q
  93. [82] = 7.6484375, // R
  94. [83] = 7.51953125, // S
  95. [84] = 6.7783203125, // T
  96. [85] = 8.05126953125, // U
  97. [86] = 7.51953125, // V
  98. [87] = 10.87646484375, // W
  99. [88] = 7.53564453125, // X
  100. [89] = 6.767578125, // Y
  101. [90] = 7.53564453125, // Z
  102. [91] = 4.9951171875, // [
  103. [92] = 4.9951171875, // backslash
  104. [93] = 4.9951171875, // ]
  105. [94] = 9.001953125, // ^
  106. [95] = 6.9931640625, // _
  107. [96] = 6.9931640625, // `
  108. [97] = 6.6064453125, // a
  109. [98] = 6.853515625, // b
  110. [99] = 5.73095703125, // c
  111. [100] = 6.853515625, // d
  112. [101] = 6.552734375, // e
  113. [102] = 3.8671874999999996, // f
  114. [103] = 6.853515625, // g
  115. [104] = 6.9609375, // h
  116. [105] = 3.0185546875, // i
  117. [106] = 3.78662109375, // j
  118. [107] = 6.509765625, // k
  119. [108] = 3.0185546875, // l
  120. [109] = 10.69921875, // m
  121. [110] = 6.9609375, // n
  122. [111] = 6.67626953125, // o
  123. [112] = 6.853515625, // p
  124. [113] = 6.853515625, // q
  125. [114] = 4.6943359375, // r
  126. [115] = 5.73095703125, // s
  127. [116] = 4.33447265625, // t
  128. [117] = 6.9609375, // u
  129. [118] = 6.509765625, // v
  130. [119] = 9.001953125, // w
  131. [120] = 6.509765625, // x
  132. [121] = 6.509765625, // y
  133. [122] = 5.779296875, // z
  134. [123] = 6.982421875, // {
  135. [124] = 4.9951171875, // |
  136. [125] = 6.982421875, // }
  137. [126] = 9.001953125, // ~
  138. [127] = 0.0,
  139. [128] = 0.0,
  140. [129] = 0.0,
  141. [130] = 0.0,
  142. [131] = 0.0,
  143. [132] = 0.0,
  144. [133] = 0.0,
  145. [134] = 0.0,
  146. [135] = 0.0,
  147. [136] = 0.0,
  148. [137] = 0.0,
  149. [138] = 0.0,
  150. [139] = 0.0,
  151. [140] = 0.0,
  152. [141] = 0.0,
  153. [142] = 0.0,
  154. [143] = 0.0,
  155. [144] = 0.0,
  156. [145] = 0.0,
  157. [146] = 0.0,
  158. [147] = 0.0,
  159. [148] = 0.0,
  160. [149] = 0.0,
  161. [150] = 0.0,
  162. [151] = 0.0,
  163. [152] = 0.0,
  164. [153] = 0.0,
  165. [154] = 0.0,
  166. [155] = 0.0,
  167. [156] = 0.0,
  168. [157] = 0.0,
  169. [158] = 0.0,
  170. [159] = 0.0,
  171. [160] = 0.0,
  172. [161] = 0.0,
  173. [162] = 0.0,
  174. [163] = 0.0,
  175. [164] = 0.0,
  176. [165] = 0.0,
  177. [166] = 0.0,
  178. [167] = 0.0,
  179. [168] = 0.0,
  180. [169] = 0.0,
  181. [170] = 0.0,
  182. [171] = 0.0,
  183. [172] = 0.0,
  184. [173] = 0.0,
  185. [174] = 0.0,
  186. [175] = 0.0,
  187. [176] = 0.0,
  188. [177] = 0.0,
  189. [178] = 0.0,
  190. [179] = 0.0,
  191. [180] = 0.0,
  192. [181] = 0.0,
  193. [182] = 0.0,
  194. [183] = 0.0,
  195. [184] = 0.0,
  196. [185] = 0.0,
  197. [186] = 0.0,
  198. [187] = 0.0,
  199. [188] = 0.0,
  200. [189] = 0.0,
  201. [190] = 0.0,
  202. [191] = 0.0,
  203. [192] = 0.0,
  204. [193] = 0.0,
  205. [194] = 0.0,
  206. [195] = 0.0,
  207. [196] = 0.0,
  208. [197] = 0.0,
  209. [198] = 0.0,
  210. [199] = 0.0,
  211. [200] = 0.0,
  212. [201] = 0.0,
  213. [202] = 0.0,
  214. [203] = 0.0,
  215. [204] = 0.0,
  216. [205] = 0.0,
  217. [206] = 0.0,
  218. [207] = 0.0,
  219. [208] = 0.0,
  220. [209] = 0.0,
  221. [210] = 0.0,
  222. [211] = 0.0,
  223. [212] = 0.0,
  224. [213] = 0.0,
  225. [214] = 0.0,
  226. [215] = 0.0,
  227. [216] = 0.0,
  228. [217] = 0.0,
  229. [218] = 0.0,
  230. [219] = 0.0,
  231. [220] = 0.0,
  232. [221] = 0.0,
  233. [222] = 0.0,
  234. [223] = 0.0,
  235. [224] = 0.0,
  236. [225] = 0.0,
  237. [226] = 0.0,
  238. [227] = 0.0,
  239. [228] = 0.0,
  240. [229] = 0.0,
  241. [230] = 0.0,
  242. [231] = 0.0,
  243. [232] = 0.0,
  244. [233] = 0.0,
  245. [234] = 0.0,
  246. [235] = 0.0,
  247. [236] = 0.0,
  248. [237] = 0.0,
  249. [238] = 0.0,
  250. [239] = 0.0,
  251. [240] = 0.0,
  252. [241] = 0.0,
  253. [242] = 0.0,
  254. [243] = 0.0,
  255. [244] = 0.0,
  256. [245] = 0.0,
  257. [246] = 0.0,
  258. [247] = 0.0,
  259. [248] = 0.0,
  260. [249] = 0.0,
  261. [250] = 0.0,
  262. [251] = 0.0,
  263. [252] = 0.0,
  264. [253] = 0.0,
  265. [254] = 0.0,
  266. [255] = 0.0
  267. };
  268. // find the width of the string using the verdana 11points font
  269. // re-write the string in place, skiping zero-length characters
  270. static inline double verdana11_width(char *s) {
  271. double w = 0.0;
  272. char *d = s;
  273. while(*s) {
  274. double t = verdana11_widths[(unsigned char)*s];
  275. if(t == 0.0)
  276. s++;
  277. else {
  278. w += t + VERDANA_KERNING;
  279. if(d != s)
  280. *d++ = *s++;
  281. else
  282. d = ++s;
  283. }
  284. }
  285. *d = '\0';
  286. w -= VERDANA_KERNING;
  287. w += VERDANA_PADDING;
  288. return w;
  289. }
  290. static inline size_t escape_xmlz(char *dst, const char *src, size_t len) {
  291. size_t i = len;
  292. // required escapes from
  293. // https://github.com/badges/shields/blob/master/badge.js
  294. while(*src && i) {
  295. switch(*src) {
  296. case '\\':
  297. *dst++ = '/';
  298. src++;
  299. i--;
  300. break;
  301. case '&':
  302. if(i > 5) {
  303. strcpy(dst, "&amp;");
  304. i -= 5;
  305. dst += 5;
  306. src++;
  307. }
  308. else goto cleanup;
  309. break;
  310. case '<':
  311. if(i > 4) {
  312. strcpy(dst, "&lt;");
  313. i -= 4;
  314. dst += 4;
  315. src++;
  316. }
  317. else goto cleanup;
  318. break;
  319. case '>':
  320. if(i > 4) {
  321. strcpy(dst, "&gt;");
  322. i -= 4;
  323. dst += 4;
  324. src++;
  325. }
  326. else goto cleanup;
  327. break;
  328. case '"':
  329. if(i > 6) {
  330. strcpy(dst, "&quot;");
  331. i -= 6;
  332. dst += 6;
  333. src++;
  334. }
  335. else goto cleanup;
  336. break;
  337. case '\'':
  338. if(i > 6) {
  339. strcpy(dst, "&apos;");
  340. i -= 6;
  341. dst += 6;
  342. src++;
  343. }
  344. else goto cleanup;
  345. break;
  346. default:
  347. i--;
  348. *dst++ = *src++;
  349. break;
  350. }
  351. }
  352. cleanup:
  353. *dst = '\0';
  354. return len - i;
  355. }
  356. static inline char *format_value_with_precision_and_unit(char *value_string, size_t value_string_len, calculated_number value, const char *units, int precision) {
  357. if(unlikely(isnan(value) || isinf(value)))
  358. value = 0.0;
  359. char *separator = "";
  360. if(unlikely(isalnum(*units)))
  361. separator = " ";
  362. if(precision < 0) {
  363. int len, lstop = 0, trim_zeros = 1;
  364. calculated_number abs = value;
  365. if(isless(value, 0)) {
  366. lstop = 1;
  367. abs = calculated_number_fabs(value);
  368. }
  369. if(isgreaterequal(abs, 1000)) {
  370. len = snprintfz(value_string, value_string_len, "%0.0" LONG_DOUBLE_MODIFIER, (LONG_DOUBLE) value);
  371. trim_zeros = 0;
  372. }
  373. else if(isgreaterequal(abs, 10)) len = snprintfz(value_string, value_string_len, "%0.1" LONG_DOUBLE_MODIFIER, (LONG_DOUBLE) value);
  374. else if(isgreaterequal(abs, 1)) len = snprintfz(value_string, value_string_len, "%0.2" LONG_DOUBLE_MODIFIER, (LONG_DOUBLE) value);
  375. else if(isgreaterequal(abs, 0.1)) len = snprintfz(value_string, value_string_len, "%0.2" LONG_DOUBLE_MODIFIER, (LONG_DOUBLE) value);
  376. else if(isgreaterequal(abs, 0.01)) len = snprintfz(value_string, value_string_len, "%0.4" LONG_DOUBLE_MODIFIER, (LONG_DOUBLE) value);
  377. else if(isgreaterequal(abs, 0.001)) len = snprintfz(value_string, value_string_len, "%0.5" LONG_DOUBLE_MODIFIER, (LONG_DOUBLE) value);
  378. else if(isgreaterequal(abs, 0.0001)) len = snprintfz(value_string, value_string_len, "%0.6" LONG_DOUBLE_MODIFIER, (LONG_DOUBLE) value);
  379. else len = snprintfz(value_string, value_string_len, "%0.7" LONG_DOUBLE_MODIFIER, (LONG_DOUBLE) value);
  380. if(unlikely(trim_zeros)) {
  381. int l;
  382. // remove trailing zeros from the decimal part
  383. for(l = len - 1; l > lstop; l--) {
  384. if(likely(value_string[l] == '0')) {
  385. value_string[l] = '\0';
  386. len--;
  387. }
  388. else if(unlikely(value_string[l] == '.')) {
  389. value_string[l] = '\0';
  390. len--;
  391. break;
  392. }
  393. else
  394. break;
  395. }
  396. }
  397. if(unlikely(len <= 0)) len = 1;
  398. snprintfz(&value_string[len], value_string_len - len, "%s%s", separator, units);
  399. }
  400. else {
  401. if(precision > 50) precision = 50;
  402. snprintfz(value_string, value_string_len, "%0.*" LONG_DOUBLE_MODIFIER "%s%s", precision, (LONG_DOUBLE) value, separator, units);
  403. }
  404. return value_string;
  405. }
  406. typedef enum badge_units_format {
  407. UNITS_FORMAT_NONE,
  408. UNITS_FORMAT_SECONDS,
  409. UNITS_FORMAT_SECONDS_AGO,
  410. UNITS_FORMAT_MINUTES,
  411. UNITS_FORMAT_MINUTES_AGO,
  412. UNITS_FORMAT_HOURS,
  413. UNITS_FORMAT_HOURS_AGO,
  414. UNITS_FORMAT_ONOFF,
  415. UNITS_FORMAT_UPDOWN,
  416. UNITS_FORMAT_OKERROR,
  417. UNITS_FORMAT_OKFAILED,
  418. UNITS_FORMAT_EMPTY,
  419. UNITS_FORMAT_PERCENT
  420. } UNITS_FORMAT;
  421. static struct units_formatter {
  422. const char *units;
  423. uint32_t hash;
  424. UNITS_FORMAT format;
  425. } badge_units_formatters[] = {
  426. { "seconds", 0, UNITS_FORMAT_SECONDS },
  427. { "seconds ago", 0, UNITS_FORMAT_SECONDS_AGO },
  428. { "minutes", 0, UNITS_FORMAT_MINUTES },
  429. { "minutes ago", 0, UNITS_FORMAT_MINUTES_AGO },
  430. { "hours", 0, UNITS_FORMAT_HOURS },
  431. { "hours ago", 0, UNITS_FORMAT_HOURS_AGO },
  432. { "on/off", 0, UNITS_FORMAT_ONOFF },
  433. { "on-off", 0, UNITS_FORMAT_ONOFF },
  434. { "onoff", 0, UNITS_FORMAT_ONOFF },
  435. { "up/down", 0, UNITS_FORMAT_UPDOWN },
  436. { "up-down", 0, UNITS_FORMAT_UPDOWN },
  437. { "updown", 0, UNITS_FORMAT_UPDOWN },
  438. { "ok/error", 0, UNITS_FORMAT_OKERROR },
  439. { "ok-error", 0, UNITS_FORMAT_OKERROR },
  440. { "okerror", 0, UNITS_FORMAT_OKERROR },
  441. { "ok/failed", 0, UNITS_FORMAT_OKFAILED },
  442. { "ok-failed", 0, UNITS_FORMAT_OKFAILED },
  443. { "okfailed", 0, UNITS_FORMAT_OKFAILED },
  444. { "empty", 0, UNITS_FORMAT_EMPTY },
  445. { "null", 0, UNITS_FORMAT_EMPTY },
  446. { "percentage", 0, UNITS_FORMAT_PERCENT },
  447. { "percent", 0, UNITS_FORMAT_PERCENT },
  448. { "pcent", 0, UNITS_FORMAT_PERCENT },
  449. // terminator
  450. { NULL, 0, UNITS_FORMAT_NONE }
  451. };
  452. inline char *format_value_and_unit(char *value_string, size_t value_string_len, calculated_number value, const char *units, int precision) {
  453. static int max = -1;
  454. int i;
  455. if(unlikely(max == -1)) {
  456. for(i = 0; badge_units_formatters[i].units; i++)
  457. badge_units_formatters[i].hash = simple_hash(badge_units_formatters[i].units);
  458. max = i;
  459. }
  460. if(unlikely(!units)) units = "";
  461. uint32_t hash_units = simple_hash(units);
  462. UNITS_FORMAT format = UNITS_FORMAT_NONE;
  463. for(i = 0; i < max; i++) {
  464. struct units_formatter *ptr = &badge_units_formatters[i];
  465. if(hash_units == ptr->hash && !strcmp(units, ptr->units)) {
  466. format = ptr->format;
  467. break;
  468. }
  469. }
  470. if(unlikely(format == UNITS_FORMAT_SECONDS || format == UNITS_FORMAT_SECONDS_AGO)) {
  471. if(value == 0.0) {
  472. snprintfz(value_string, value_string_len, "%s", "now");
  473. return value_string;
  474. }
  475. else if(isnan(value) || isinf(value)) {
  476. snprintfz(value_string, value_string_len, "%s", "undefined");
  477. return value_string;
  478. }
  479. const char *suffix = (format == UNITS_FORMAT_SECONDS_AGO)?" ago":"";
  480. size_t s = (size_t)value;
  481. size_t d = s / 86400;
  482. s = s % 86400;
  483. size_t h = s / 3600;
  484. s = s % 3600;
  485. size_t m = s / 60;
  486. s = s % 60;
  487. if(d)
  488. snprintfz(value_string, value_string_len, "%zu %s %02zu:%02zu:%02zu%s", d, (d == 1)?"day":"days", h, m, s, suffix);
  489. else
  490. snprintfz(value_string, value_string_len, "%02zu:%02zu:%02zu%s", h, m, s, suffix);
  491. return value_string;
  492. }
  493. else if(unlikely(format == UNITS_FORMAT_MINUTES || format == UNITS_FORMAT_MINUTES_AGO)) {
  494. if(value == 0.0) {
  495. snprintfz(value_string, value_string_len, "%s", "now");
  496. return value_string;
  497. }
  498. else if(isnan(value) || isinf(value)) {
  499. snprintfz(value_string, value_string_len, "%s", "undefined");
  500. return value_string;
  501. }
  502. const char *suffix = (format == UNITS_FORMAT_MINUTES_AGO)?" ago":"";
  503. size_t m = (size_t)value;
  504. size_t d = m / (60 * 24);
  505. m = m % (60 * 24);
  506. size_t h = m / 60;
  507. m = m % 60;
  508. if(d)
  509. snprintfz(value_string, value_string_len, "%zud %02zuh %02zum%s", d, h, m, suffix);
  510. else
  511. snprintfz(value_string, value_string_len, "%zuh %zum%s", h, m, suffix);
  512. return value_string;
  513. }
  514. else if(unlikely(format == UNITS_FORMAT_HOURS || format == UNITS_FORMAT_HOURS_AGO)) {
  515. if(value == 0.0) {
  516. snprintfz(value_string, value_string_len, "%s", "now");
  517. return value_string;
  518. }
  519. else if(isnan(value) || isinf(value)) {
  520. snprintfz(value_string, value_string_len, "%s", "undefined");
  521. return value_string;
  522. }
  523. const char *suffix = (format == UNITS_FORMAT_HOURS_AGO)?" ago":"";
  524. size_t h = (size_t)value;
  525. size_t d = h / 24;
  526. h = h % 24;
  527. if(d)
  528. snprintfz(value_string, value_string_len, "%zud %zuh%s", d, h, suffix);
  529. else
  530. snprintfz(value_string, value_string_len, "%zuh%s", h, suffix);
  531. return value_string;
  532. }
  533. else if(unlikely(format == UNITS_FORMAT_ONOFF)) {
  534. snprintfz(value_string, value_string_len, "%s", (value != 0.0)?"on":"off");
  535. return value_string;
  536. }
  537. else if(unlikely(format == UNITS_FORMAT_UPDOWN)) {
  538. snprintfz(value_string, value_string_len, "%s", (value != 0.0)?"up":"down");
  539. return value_string;
  540. }
  541. else if(unlikely(format == UNITS_FORMAT_OKERROR)) {
  542. snprintfz(value_string, value_string_len, "%s", (value != 0.0)?"ok":"error");
  543. return value_string;
  544. }
  545. else if(unlikely(format == UNITS_FORMAT_OKFAILED)) {
  546. snprintfz(value_string, value_string_len, "%s", (value != 0.0)?"ok":"failed");
  547. return value_string;
  548. }
  549. else if(unlikely(format == UNITS_FORMAT_EMPTY))
  550. units = "";
  551. else if(unlikely(format == UNITS_FORMAT_PERCENT))
  552. units = "%";
  553. if(unlikely(isnan(value) || isinf(value))) {
  554. strcpy(value_string, "-");
  555. return value_string;
  556. }
  557. return format_value_with_precision_and_unit(value_string, value_string_len, value, units, precision);
  558. }
  559. static struct badge_color {
  560. const char *name;
  561. uint32_t hash;
  562. const char *color;
  563. } badge_colors[] = {
  564. // colors from:
  565. // https://github.com/badges/shields/blob/master/colorscheme.json
  566. { "brightgreen", 0, "#4c1" },
  567. { "green", 0, "#97CA00" },
  568. { "yellow", 0, "#dfb317" },
  569. { "yellowgreen", 0, "#a4a61d" },
  570. { "orange", 0, "#fe7d37" },
  571. { "red", 0, "#e05d44" },
  572. { "blue", 0, "#007ec6" },
  573. { "grey", 0, "#555" },
  574. { "gray", 0, "#555" },
  575. { "lightgrey", 0, "#9f9f9f" },
  576. { "lightgray", 0, "#9f9f9f" },
  577. // terminator
  578. { NULL, 0, NULL }
  579. };
  580. static inline const char *color_map(const char *color) {
  581. static int max = -1;
  582. int i;
  583. if(unlikely(max == -1)) {
  584. for(i = 0; badge_colors[i].name ;i++)
  585. badge_colors[i].hash = simple_hash(badge_colors[i].name);
  586. max = i;
  587. }
  588. uint32_t hash = simple_hash(color);
  589. for(i = 0; i < max; i++) {
  590. struct badge_color *ptr = &badge_colors[i];
  591. if(hash == ptr->hash && !strcmp(color, ptr->name))
  592. return ptr->color;
  593. }
  594. return color;
  595. }
  596. typedef enum color_comparison {
  597. COLOR_COMPARE_EQUAL,
  598. COLOR_COMPARE_NOTEQUAL,
  599. COLOR_COMPARE_LESS,
  600. COLOR_COMPARE_LESSEQUAL,
  601. COLOR_COMPARE_GREATER,
  602. COLOR_COMPARE_GREATEREQUAL,
  603. } BADGE_COLOR_COMPARISON;
  604. static inline void calc_colorz(const char *color, char *final, size_t len, calculated_number value) {
  605. if(isnan(value) || isinf(value))
  606. value = NAN;
  607. char color_buffer[256 + 1] = "";
  608. char value_buffer[256 + 1] = "";
  609. BADGE_COLOR_COMPARISON comparison = COLOR_COMPARE_GREATER;
  610. // example input:
  611. // color<max|color>min|color:null...
  612. const char *c = color;
  613. while(*c) {
  614. char *dc = color_buffer, *dv = NULL;
  615. size_t ci = 0, vi = 0;
  616. const char *t = c;
  617. while(*t && *t != '|') {
  618. switch(*t) {
  619. case '!':
  620. if(t[1] == '=') t++;
  621. comparison = COLOR_COMPARE_NOTEQUAL;
  622. dv = value_buffer;
  623. break;
  624. case '=':
  625. case ':':
  626. comparison = COLOR_COMPARE_EQUAL;
  627. dv = value_buffer;
  628. break;
  629. case '}':
  630. case ')':
  631. case '>':
  632. if(t[1] == '=') {
  633. comparison = COLOR_COMPARE_GREATEREQUAL;
  634. t++;
  635. }
  636. else
  637. comparison = COLOR_COMPARE_GREATER;
  638. dv = value_buffer;
  639. break;
  640. case '{':
  641. case '(':
  642. case '<':
  643. if(t[1] == '=') {
  644. comparison = COLOR_COMPARE_LESSEQUAL;
  645. t++;
  646. }
  647. else if(t[1] == '>' || t[1] == ')' || t[1] == '}') {
  648. comparison = COLOR_COMPARE_NOTEQUAL;
  649. t++;
  650. }
  651. else
  652. comparison = COLOR_COMPARE_LESS;
  653. dv = value_buffer;
  654. break;
  655. default:
  656. if(dv) {
  657. if(vi < 256) {
  658. vi++;
  659. *dv++ = *t;
  660. }
  661. }
  662. else {
  663. if(ci < 256) {
  664. ci++;
  665. *dc++ = *t;
  666. }
  667. }
  668. break;
  669. }
  670. t++;
  671. }
  672. // prepare for next iteration
  673. if(*t == '|') t++;
  674. c = t;
  675. // do the math
  676. *dc = '\0';
  677. if(dv) {
  678. *dv = '\0';
  679. calculated_number v;
  680. if(!*value_buffer || !strcmp(value_buffer, "null")) {
  681. v = NAN;
  682. }
  683. else {
  684. v = str2l(value_buffer);
  685. if(isnan(v) || isinf(v))
  686. v = NAN;
  687. }
  688. if(unlikely(isnan(value) || isnan(v))) {
  689. if(isnan(value) && isnan(v))
  690. break;
  691. }
  692. else {
  693. if (unlikely(comparison == COLOR_COMPARE_LESS && isless(value, v))) break;
  694. else if (unlikely(comparison == COLOR_COMPARE_LESSEQUAL && islessequal(value, v))) break;
  695. else if (unlikely(comparison == COLOR_COMPARE_GREATER && isgreater(value, v))) break;
  696. else if (unlikely(comparison == COLOR_COMPARE_GREATEREQUAL && isgreaterequal(value, v))) break;
  697. else if (unlikely(comparison == COLOR_COMPARE_EQUAL && !islessgreater(value, v))) break;
  698. else if (unlikely(comparison == COLOR_COMPARE_NOTEQUAL && islessgreater(value, v))) break;
  699. }
  700. }
  701. else
  702. break;
  703. }
  704. const char *b;
  705. if(color_buffer[0])
  706. b = color_buffer;
  707. else
  708. b = color;
  709. strncpyz(final, b, len);
  710. }
  711. // value + units
  712. #define VALUE_STRING_SIZE 100
  713. // label
  714. #define LABEL_STRING_SIZE 200
  715. // colors
  716. #define COLOR_STRING_SIZE 100
  717. void buffer_svg(BUFFER *wb, const char *label, calculated_number value, const char *units, const char *label_color, const char *value_color, int precision, int scale, uint32_t options) {
  718. char label_buffer[LABEL_STRING_SIZE + 1]
  719. , value_color_buffer[COLOR_STRING_SIZE + 1]
  720. , value_string[VALUE_STRING_SIZE + 1]
  721. , label_escaped[LABEL_STRING_SIZE + 1]
  722. , value_escaped[VALUE_STRING_SIZE + 1]
  723. , label_color_escaped[COLOR_STRING_SIZE + 1]
  724. , value_color_escaped[COLOR_STRING_SIZE + 1];
  725. double label_width, value_width, total_width, height = 20.0, font_size = 11.0, text_offset = 5.8, round_corner = 3.0;
  726. if(scale < 100) scale = 100;
  727. if(unlikely(!label_color || !*label_color))
  728. label_color = "#555";
  729. if(unlikely(!value_color || !*value_color))
  730. value_color = (isnan(value) || isinf(value))?"#999":"#4c1";
  731. calc_colorz(value_color, value_color_buffer, COLOR_STRING_SIZE, value);
  732. format_value_and_unit(value_string, VALUE_STRING_SIZE, (options & RRDR_OPTION_DISPLAY_ABS)?calculated_number_fabs(value):value, units, precision);
  733. // we need to copy the label, since verdana11_width may write to it
  734. strncpyz(label_buffer, label, LABEL_STRING_SIZE);
  735. label_width = verdana11_width(label_buffer) + (BADGE_HORIZONTAL_PADDING * 2);
  736. value_width = verdana11_width(value_string) + (BADGE_HORIZONTAL_PADDING * 2);
  737. total_width = label_width + value_width;
  738. escape_xmlz(label_escaped, label_buffer, LABEL_STRING_SIZE);
  739. escape_xmlz(value_escaped, value_string, VALUE_STRING_SIZE);
  740. escape_xmlz(label_color_escaped, color_map(label_color), COLOR_STRING_SIZE);
  741. escape_xmlz(value_color_escaped, color_map(value_color_buffer), COLOR_STRING_SIZE);
  742. wb->contenttype = CT_IMAGE_SVG_XML;
  743. total_width = total_width * scale / 100.0;
  744. height = height * scale / 100.0;
  745. font_size = font_size * scale / 100.0;
  746. text_offset = text_offset * scale / 100.0;
  747. label_width = label_width * scale / 100.0;
  748. value_width = value_width * scale / 100.0;
  749. round_corner = round_corner * scale / 100.0;
  750. // svg template from:
  751. // https://raw.githubusercontent.com/badges/shields/master/templates/flat-template.svg
  752. buffer_sprintf(wb,
  753. "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"%0.2f\" height=\"%0.2f\">"
  754. "<linearGradient id=\"smooth\" x2=\"0\" y2=\"100%%\">"
  755. "<stop offset=\"0\" stop-color=\"#bbb\" stop-opacity=\".1\"/>"
  756. "<stop offset=\"1\" stop-opacity=\".1\"/>"
  757. "</linearGradient>"
  758. "<mask id=\"round\">"
  759. "<rect width=\"%0.2f\" height=\"%0.2f\" rx=\"%0.2f\" fill=\"#fff\"/>"
  760. "</mask>"
  761. "<g mask=\"url(#round)\">"
  762. "<rect width=\"%0.2f\" height=\"%0.2f\" fill=\"%s\"/>"
  763. "<rect x=\"%0.2f\" width=\"%0.2f\" height=\"%0.2f\" fill=\"%s\"/>"
  764. "<rect width=\"%0.2f\" height=\"%0.2f\" fill=\"url(#smooth)\"/>"
  765. "</g>"
  766. "<g fill=\"#fff\" text-anchor=\"middle\" font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\" font-size=\"%0.2f\">"
  767. "<text x=\"%0.2f\" y=\"%0.0f\" fill=\"#010101\" fill-opacity=\".3\">%s</text>"
  768. "<text x=\"%0.2f\" y=\"%0.0f\">%s</text>"
  769. "<text x=\"%0.2f\" y=\"%0.0f\" fill=\"#010101\" fill-opacity=\".3\">%s</text>"
  770. "<text x=\"%0.2f\" y=\"%0.0f\">%s</text>"
  771. "</g>"
  772. "</svg>",
  773. total_width, height,
  774. total_width, height, round_corner,
  775. label_width, height, label_color_escaped,
  776. label_width, value_width, height, value_color_escaped,
  777. total_width, height,
  778. font_size,
  779. label_width / 2, ceil(height - text_offset), label_escaped,
  780. label_width / 2, ceil(height - text_offset - 1.0), label_escaped,
  781. label_width + value_width / 2 -1, ceil(height - text_offset), value_escaped,
  782. label_width + value_width / 2 -1, ceil(height - text_offset - 1.0), value_escaped);
  783. }
  784. int web_client_api_request_v1_badge(RRDHOST *host, struct web_client *w, char *url) {
  785. int ret = 400;
  786. buffer_flush(w->response.data);
  787. BUFFER *dimensions = NULL;
  788. const char *chart = NULL
  789. , *before_str = NULL
  790. , *after_str = NULL
  791. , *points_str = NULL
  792. , *multiply_str = NULL
  793. , *divide_str = NULL
  794. , *label = NULL
  795. , *units = NULL
  796. , *label_color = NULL
  797. , *value_color = NULL
  798. , *refresh_str = NULL
  799. , *precision_str = NULL
  800. , *scale_str = NULL
  801. , *alarm = NULL;
  802. int group = RRDR_GROUPING_AVERAGE;
  803. uint32_t options = 0x00000000;
  804. while(url) {
  805. char *value = mystrsep(&url, "/?&");
  806. if(!value || !*value) continue;
  807. char *name = mystrsep(&value, "=");
  808. if(!name || !*name) continue;
  809. if(!value || !*value) continue;
  810. debug(D_WEB_CLIENT, "%llu: API v1 badge.svg query param '%s' with value '%s'", w->id, name, value);
  811. // name and value are now the parameters
  812. // they are not null and not empty
  813. if(!strcmp(name, "chart")) chart = value;
  814. else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) {
  815. if(!dimensions)
  816. dimensions = buffer_create(100);
  817. buffer_strcat(dimensions, "|");
  818. buffer_strcat(dimensions, value);
  819. }
  820. else if(!strcmp(name, "after")) after_str = value;
  821. else if(!strcmp(name, "before")) before_str = value;
  822. else if(!strcmp(name, "points")) points_str = value;
  823. else if(!strcmp(name, "group")) {
  824. group = web_client_api_request_v1_data_group(value, RRDR_GROUPING_AVERAGE);
  825. }
  826. else if(!strcmp(name, "options")) {
  827. options |= web_client_api_request_v1_data_options(value);
  828. }
  829. else if(!strcmp(name, "label")) label = value;
  830. else if(!strcmp(name, "units")) units = value;
  831. else if(!strcmp(name, "label_color")) label_color = value;
  832. else if(!strcmp(name, "value_color")) value_color = value;
  833. else if(!strcmp(name, "multiply")) multiply_str = value;
  834. else if(!strcmp(name, "divide")) divide_str = value;
  835. else if(!strcmp(name, "refresh")) refresh_str = value;
  836. else if(!strcmp(name, "precision")) precision_str = value;
  837. else if(!strcmp(name, "scale")) scale_str = value;
  838. else if(!strcmp(name, "alarm")) alarm = value;
  839. }
  840. if(!chart || !*chart) {
  841. buffer_no_cacheable(w->response.data);
  842. buffer_sprintf(w->response.data, "No chart id is given at the request.");
  843. goto cleanup;
  844. }
  845. int scale = (scale_str && *scale_str)?str2i(scale_str):100;
  846. RRDSET *st = rrdset_find(host, chart);
  847. if(!st) st = rrdset_find_byname(host, chart);
  848. if(!st) {
  849. buffer_no_cacheable(w->response.data);
  850. buffer_svg(w->response.data, "chart not found", NAN, "", NULL, NULL, -1, scale, 0);
  851. ret = 200;
  852. goto cleanup;
  853. }
  854. st->last_accessed_time = now_realtime_sec();
  855. RRDCALC *rc = NULL;
  856. if(alarm) {
  857. rc = rrdcalc_find(st, alarm);
  858. if (!rc) {
  859. buffer_no_cacheable(w->response.data);
  860. buffer_svg(w->response.data, "alarm not found", NAN, "", NULL, NULL, -1, scale, 0);
  861. ret = 200;
  862. goto cleanup;
  863. }
  864. }
  865. long long multiply = (multiply_str && *multiply_str )?str2l(multiply_str):1;
  866. long long divide = (divide_str && *divide_str )?str2l(divide_str):1;
  867. long long before = (before_str && *before_str )?str2l(before_str):0;
  868. long long after = (after_str && *after_str )?str2l(after_str):-st->update_every;
  869. int points = (points_str && *points_str )?str2i(points_str):1;
  870. int precision = (precision_str && *precision_str)?str2i(precision_str):-1;
  871. if(!multiply) multiply = 1;
  872. if(!divide) divide = 1;
  873. int refresh = 0;
  874. if(refresh_str && *refresh_str) {
  875. if(!strcmp(refresh_str, "auto")) {
  876. if(rc) refresh = rc->update_every;
  877. else if(options & RRDR_OPTION_NOT_ALIGNED)
  878. refresh = st->update_every;
  879. else {
  880. refresh = (int)(before - after);
  881. if(refresh < 0) refresh = -refresh;
  882. }
  883. }
  884. else {
  885. refresh = str2i(refresh_str);
  886. if(refresh < 0) refresh = -refresh;
  887. }
  888. }
  889. if(!label) {
  890. if(alarm) {
  891. char *s = (char *)alarm;
  892. while(*s) {
  893. if(*s == '_') *s = ' ';
  894. s++;
  895. }
  896. label = alarm;
  897. }
  898. else if(dimensions) {
  899. const char *dim = buffer_tostring(dimensions);
  900. if(*dim == '|') dim++;
  901. label = dim;
  902. }
  903. else
  904. label = st->name;
  905. }
  906. if(!units) {
  907. if(alarm) {
  908. if(rc->units)
  909. units = rc->units;
  910. else
  911. units = "";
  912. }
  913. else if(options & RRDR_OPTION_PERCENTAGE)
  914. units = "%";
  915. else
  916. units = st->units;
  917. }
  918. debug(D_WEB_CLIENT, "%llu: API command 'badge.svg' for chart '%s', alarm '%s', dimensions '%s', after '%lld', before '%lld', points '%d', group '%d', options '0x%08x'"
  919. , w->id
  920. , chart
  921. , alarm?alarm:""
  922. , (dimensions)?buffer_tostring(dimensions):""
  923. , after
  924. , before
  925. , points
  926. , group
  927. , options
  928. );
  929. if(rc) {
  930. if (refresh > 0) {
  931. buffer_sprintf(w->response.header, "Refresh: %d\r\n", refresh);
  932. w->response.data->expires = now_realtime_sec() + refresh;
  933. }
  934. else buffer_no_cacheable(w->response.data);
  935. if(!value_color) {
  936. switch(rc->status) {
  937. case RRDCALC_STATUS_CRITICAL:
  938. value_color = "red";
  939. break;
  940. case RRDCALC_STATUS_WARNING:
  941. value_color = "orange";
  942. break;
  943. case RRDCALC_STATUS_CLEAR:
  944. value_color = "brightgreen";
  945. break;
  946. case RRDCALC_STATUS_UNDEFINED:
  947. value_color = "lightgrey";
  948. break;
  949. case RRDCALC_STATUS_UNINITIALIZED:
  950. value_color = "#000";
  951. break;
  952. default:
  953. value_color = "grey";
  954. break;
  955. }
  956. }
  957. buffer_svg(w->response.data,
  958. label,
  959. (isnan(rc->value)||isinf(rc->value)) ? rc->value : rc->value * multiply / divide,
  960. units,
  961. label_color,
  962. value_color,
  963. precision,
  964. scale,
  965. options
  966. );
  967. ret = 200;
  968. }
  969. else {
  970. time_t latest_timestamp = 0;
  971. int value_is_null = 1;
  972. calculated_number n = NAN;
  973. ret = 500;
  974. // if the collected value is too old, don't calculate its value
  975. if (rrdset_last_entry_t(st) >= (now_realtime_sec() - (st->update_every * st->gap_when_lost_iterations_above)))
  976. ret = rrdset2value_api_v1(st, w->response.data, &n, (dimensions) ? buffer_tostring(dimensions) : NULL
  977. , points, after, before, group, 0, options, NULL, &latest_timestamp, &value_is_null);
  978. // if the value cannot be calculated, show empty badge
  979. if (ret != 200) {
  980. buffer_no_cacheable(w->response.data);
  981. value_is_null = 1;
  982. n = 0;
  983. ret = 200;
  984. }
  985. else if (refresh > 0) {
  986. buffer_sprintf(w->response.header, "Refresh: %d\r\n", refresh);
  987. w->response.data->expires = now_realtime_sec() + refresh;
  988. }
  989. else buffer_no_cacheable(w->response.data);
  990. // render the badge
  991. buffer_svg(w->response.data,
  992. label,
  993. (value_is_null)?NAN:(n * multiply / divide),
  994. units,
  995. label_color,
  996. value_color,
  997. precision,
  998. scale,
  999. options
  1000. );
  1001. }
  1002. cleanup:
  1003. buffer_free(dimensions);
  1004. return ret;
  1005. }