gvrender_pango.c 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. /*************************************************************************
  2. * Copyright (c) 2011 AT&T Intellectual Property
  3. * All rights reserved. This program and the accompanying materials
  4. * are made available under the terms of the Eclipse Public License v1.0
  5. * which accompanies this distribution, and is available at
  6. * https://www.eclipse.org/legal/epl-v10.html
  7. *
  8. * Contributors: Details at https://graphviz.org
  9. *************************************************************************/
  10. #include "config.h"
  11. #include <assert.h>
  12. #include <errno.h>
  13. #include <limits.h>
  14. #include <stdio.h>
  15. #include <stdlib.h>
  16. #include <string.h>
  17. #include <time.h>
  18. #include <common/const.h>
  19. #include <gvc/gvplugin_render.h>
  20. #include <common/utils.h>
  21. #include <gvc/gvplugin_device.h>
  22. #include <gvc/gvio.h>
  23. #include <math.h>
  24. #include <util/agxbuf.h>
  25. #include "gvplugin_pango.h"
  26. #include <pango/pangocairo.h>
  27. typedef enum {
  28. FORMAT_CAIRO,
  29. FORMAT_PNG,
  30. FORMAT_PS,
  31. FORMAT_PDF,
  32. FORMAT_SVG,
  33. FORMAT_EPS,
  34. } format_type;
  35. #define ARRAY_SIZE(A) (sizeof(A)/sizeof(A[0]))
  36. static double dashed[] = {6.};
  37. static int dashed_len = ARRAY_SIZE(dashed);
  38. static double dotted[] = {2., 6.};
  39. static int dotted_len = ARRAY_SIZE(dotted);
  40. #ifdef CAIRO_HAS_PS_SURFACE
  41. #include <cairo-ps.h>
  42. #endif
  43. #ifdef CAIRO_HAS_PDF_SURFACE
  44. #include <cairo-pdf.h>
  45. #endif
  46. #ifdef CAIRO_HAS_SVG_SURFACE
  47. #include <cairo-svg.h>
  48. #endif
  49. static void cairogen_polyline(GVJ_t *job, pointf *A, size_t n);
  50. static void cairogen_set_color(cairo_t * cr, gvcolor_t * color)
  51. {
  52. cairo_set_source_rgba(cr, color->u.RGBA[0], color->u.RGBA[1],
  53. color->u.RGBA[2], color->u.RGBA[3]);
  54. }
  55. static void cairogen_add_color_stop_rgba(cairo_pattern_t *pat, double stop , gvcolor_t * color)
  56. {
  57. cairo_pattern_add_color_stop_rgba (pat, stop,color->u.RGBA[0], color->u.RGBA[1],
  58. color->u.RGBA[2], color->u.RGBA[3]);
  59. }
  60. static cairo_status_t
  61. writer (void *closure, const unsigned char *data, unsigned int length)
  62. {
  63. if (length == gvwrite(closure, (const char*)data, length))
  64. return CAIRO_STATUS_SUCCESS;
  65. return CAIRO_STATUS_WRITE_ERROR;
  66. }
  67. static void cairogen_begin_job(GVJ_t * job)
  68. {
  69. if (job->external_context && job->context)
  70. cairo_save(job->context);
  71. }
  72. static void cairogen_end_job(GVJ_t * job)
  73. {
  74. cairo_t *cr = job->context;
  75. if (job->external_context)
  76. cairo_restore(cr);
  77. else {
  78. cairo_destroy(cr);
  79. job->context = NULL;
  80. }
  81. }
  82. static const double CAIRO_XMAX = 32767;
  83. static const double CAIRO_YMAX = 32767;
  84. static void cairogen_begin_page(GVJ_t * job)
  85. {
  86. cairo_t *cr = job->context;
  87. cairo_surface_t *surface;
  88. cairo_status_t status;
  89. if (cr == NULL) {
  90. switch (job->render.id) {
  91. case FORMAT_PS:
  92. case FORMAT_EPS:
  93. #ifdef CAIRO_HAS_PS_SURFACE
  94. surface = cairo_ps_surface_create_for_stream (writer,
  95. job, job->width, job->height);
  96. if (job->render.id == FORMAT_EPS)
  97. cairo_ps_surface_set_eps (surface, TRUE);
  98. #endif
  99. break;
  100. case FORMAT_PDF:
  101. #ifdef CAIRO_HAS_PDF_SURFACE
  102. surface = cairo_pdf_surface_create_for_stream (writer,
  103. job, job->width, job->height);
  104. {
  105. const char *source_date_epoch = getenv("SOURCE_DATE_EPOCH");
  106. if (source_date_epoch != NULL) {
  107. char *end = NULL;
  108. errno = 0;
  109. long epoch = strtol(source_date_epoch, &end, 10);
  110. // from https://reproducible-builds.org/specs/source-date-epoch/
  111. //
  112. // If the value is malformed, the build process SHOULD
  113. // exit with a non-zero error code.
  114. if ((epoch == LONG_MAX && errno != 0) || epoch < 0
  115. || *end != '\0') {
  116. fprintf(stderr,
  117. "malformed value %s for $SOURCE_DATE_EPOCH\n",
  118. source_date_epoch);
  119. exit(EXIT_FAILURE);
  120. }
  121. time_t tepoch = (time_t)epoch;
  122. struct tm *tm = gmtime(&tepoch);
  123. if (tm == NULL) {
  124. fprintf(stderr,
  125. "malformed value %s for $SOURCE_DATE_EPOCH\n",
  126. source_date_epoch);
  127. exit(EXIT_FAILURE);
  128. }
  129. #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
  130. char iso8601[sizeof("YYYY-MM-DDThh:mm:ss")] = {0};
  131. (void)strftime(iso8601, sizeof(iso8601), "%Y-%m-%dT%H:%M:%SZ", tm);
  132. cairo_pdf_surface_set_metadata(surface,
  133. CAIRO_PDF_METADATA_CREATE_DATE,
  134. iso8601);
  135. cairo_pdf_surface_set_metadata(surface,
  136. CAIRO_PDF_METADATA_MOD_DATE,
  137. iso8601);
  138. #endif
  139. }
  140. }
  141. #endif
  142. break;
  143. case FORMAT_SVG:
  144. #ifdef CAIRO_HAS_SVG_SURFACE
  145. surface = cairo_svg_surface_create_for_stream (writer,
  146. job, job->width, job->height);
  147. #endif
  148. break;
  149. case FORMAT_CAIRO:
  150. case FORMAT_PNG:
  151. default:
  152. if (job->width >= CAIRO_XMAX || job->height >= CAIRO_YMAX) {
  153. double scale = fmin(CAIRO_XMAX / job->width, CAIRO_YMAX / job->height);
  154. assert(job->width * scale <= UINT_MAX);
  155. job->width = (unsigned)(job->width * scale);
  156. assert(job->height * scale <= UINT_MAX);
  157. job->height = (unsigned)(job->height * scale);
  158. job->scale.x *= scale;
  159. job->scale.y *= scale;
  160. fprintf(stderr,
  161. "%s: graph is too large for cairo-renderer bitmaps. Scaling by %g to fit\n",
  162. job->common->cmdname, scale);
  163. }
  164. assert(job->width <= INT_MAX);
  165. assert(job->height <= INT_MAX);
  166. surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
  167. (int)job->width, (int)job->height);
  168. if (job->common->verbose)
  169. fprintf(stderr,
  170. "%s: allocating a %.0fK cairo image surface (%d x %d pixels)\n",
  171. job->common->cmdname,
  172. round(job->width * job->height * 4 / 1024.),
  173. job->width, job->height);
  174. break;
  175. }
  176. status = cairo_surface_status(surface);
  177. if (status != CAIRO_STATUS_SUCCESS) {
  178. fprintf(stderr, "%s: failure to create cairo surface: %s\n",
  179. job->common->cmdname,
  180. cairo_status_to_string(status));
  181. cairo_surface_destroy (surface);
  182. return;
  183. }
  184. cr = cairo_create(surface);
  185. cairo_surface_destroy (surface);
  186. job->context = cr;
  187. }
  188. cairo_scale(cr, job->scale.x, job->scale.y);
  189. cairo_rotate(cr, -job->rotation * M_PI / 180.);
  190. cairo_translate(cr, job->translation.x, -job->translation.y);
  191. cairo_rectangle(cr, job->clip.LL.x, - job->clip.LL.y,
  192. job->clip.UR.x - job->clip.LL.x, - (job->clip.UR.y - job->clip.LL.y));
  193. cairo_clip(cr);
  194. }
  195. static void cairogen_end_page(GVJ_t * job)
  196. {
  197. cairo_t *cr = job->context;
  198. cairo_surface_t *surface;
  199. cairo_status_t status;
  200. switch (job->render.id) {
  201. #ifdef CAIRO_HAS_PNG_FUNCTIONS
  202. case FORMAT_PNG:
  203. surface = cairo_get_target(cr);
  204. cairo_surface_write_to_png_stream(surface, writer, job);
  205. break;
  206. #endif
  207. case FORMAT_PS:
  208. case FORMAT_PDF:
  209. case FORMAT_SVG:
  210. cairo_show_page(cr);
  211. surface = cairo_surface_reference(cairo_get_target(cr));
  212. cairo_surface_finish(surface);
  213. status = cairo_surface_status(surface);
  214. cairo_surface_destroy(surface);
  215. if (status != CAIRO_STATUS_SUCCESS)
  216. fprintf(stderr, "cairo: %s\n", cairo_status_to_string(status));
  217. break;
  218. case FORMAT_CAIRO:
  219. default:
  220. surface = cairo_get_target(cr);
  221. if (cairo_image_surface_get_width(surface) == 0 || cairo_image_surface_get_height(surface) == 0) {
  222. /* apparently cairo never allocates a surface if nothing was ever written to it */
  223. /* but suppress this error message since a zero area surface seems to happen during normal operations, particular in -Tx11
  224. fprintf(stderr, "ERROR: cairo surface has zero area, this may indicate some problem during rendering shapes.\n");
  225. - jce */
  226. }
  227. job->imagedata = (char *)(cairo_image_surface_get_data(surface));
  228. break;
  229. /* formatting will be done by gvdevice_format() */
  230. }
  231. }
  232. static void cairogen_begin_anchor(GVJ_t *job, char *url, char *tooltip, char *target, char *id)
  233. {
  234. obj_state_t *obj = job->obj;
  235. cairo_t *cr = job->context;
  236. double p0x, p0y, p1x, p1y;
  237. // suppress unused parameter warnings
  238. (void)tooltip;
  239. (void)target;
  240. (void)id;
  241. if (url && obj->url_map_p) {
  242. p0x = obj->url_map_p[0].x;
  243. p0y = -obj->url_map_p[0].y;
  244. cairo_user_to_device (cr, &p0x, &p0y);
  245. p1x = obj->url_map_p[1].x;
  246. p1y = -obj->url_map_p[1].y;
  247. cairo_user_to_device (cr, &p1x, &p1y);
  248. agxbuf buf = {0};
  249. agxbprint(&buf, "rect=[%f %f %f %f] uri='%s'", p0x, p0y, p1x - p0x,
  250. p1y - p0y, url);
  251. #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 16, 0)
  252. cairo_tag_begin(cr, CAIRO_TAG_LINK, agxbuse(&buf));
  253. cairo_tag_end (cr, CAIRO_TAG_LINK);
  254. #endif
  255. agxbfree(&buf);
  256. }
  257. }
  258. static void cairogen_textspan(GVJ_t * job, pointf p, textspan_t * span)
  259. {
  260. obj_state_t *obj = job->obj;
  261. cairo_t *cr = job->context;
  262. pointf A[2];
  263. cairo_set_dash (cr, dashed, 0, 0.0); /* clear any dashing */
  264. cairogen_set_color(cr, &obj->pencolor);
  265. switch (span->just) {
  266. case 'r':
  267. p.x -= span->size.x;
  268. break;
  269. case 'l':
  270. p.x -= 0.0;
  271. break;
  272. case 'n':
  273. default:
  274. p.x -= span->size.x / 2.0;
  275. break;
  276. }
  277. p.y += span->yoffset_centerline + span->yoffset_layout;
  278. cairo_move_to (cr, p.x, -p.y);
  279. cairo_save(cr);
  280. cairo_scale(cr, POINTS_PER_INCH / FONT_DPI, POINTS_PER_INCH / FONT_DPI);
  281. pango_cairo_show_layout(cr, (PangoLayout*)span->layout);
  282. cairo_restore(cr);
  283. if (span->font && (span->font->flags & HTML_OL)) {
  284. A[0].x = p.x;
  285. A[1].x = p.x + span->size.x;
  286. A[1].y = A[0].y = p.y;
  287. cairogen_polyline(job, A, 2);
  288. }
  289. }
  290. static void cairogen_set_penstyle(GVJ_t *job, cairo_t *cr)
  291. {
  292. obj_state_t *obj = job->obj;
  293. if (obj->pen == PEN_DASHED) {
  294. cairo_set_dash (cr, dashed, dashed_len, 0.0);
  295. } else if (obj->pen == PEN_DOTTED) {
  296. cairo_set_dash (cr, dotted, dotted_len, 0.0);
  297. } else {
  298. cairo_set_dash (cr, dashed, 0, 0.0);
  299. }
  300. cairo_set_line_width (cr, obj->penwidth);
  301. }
  302. static void cairo_gradient_fill(cairo_t *cr, obj_state_t *obj, int filled,
  303. pointf *A, size_t n) {
  304. cairo_pattern_t* pat;
  305. double angle = obj->gradient_angle * M_PI / 180;
  306. pointf G[2],c1;
  307. if (filled == GRADIENT) {
  308. get_gradient_points(A, G, n, angle, 0);
  309. pat = cairo_pattern_create_linear (G[0].x,G[0].y,G[1].x,G[1].y);
  310. }
  311. else {
  312. get_gradient_points(A, G, n, 0, 1);
  313. //r1 is inner radius, r2 is outer radius
  314. const double r1 = G[1].x; // set a r2 ÷ 4 in get_gradient_points
  315. const double r2 = G[1].y;
  316. if (obj->gradient_angle == 0) {
  317. c1.x = G[0].x;
  318. c1.y = G[0].y;
  319. }
  320. else {
  321. c1.x = G[0].x + r1 * cos(angle);
  322. c1.y = G[0].y - r1 * sin(angle);
  323. }
  324. pat = cairo_pattern_create_radial(c1.x,c1.y,r1,G[0].x,G[0].y,r2);
  325. }
  326. if (obj->gradient_frac > 0) {
  327. cairogen_add_color_stop_rgba(pat,obj->gradient_frac - 0.001,&(obj->fillcolor));
  328. cairogen_add_color_stop_rgba(pat,obj->gradient_frac,&(obj->stopcolor));
  329. }
  330. else {
  331. cairogen_add_color_stop_rgba(pat,0,&(obj->fillcolor));
  332. cairogen_add_color_stop_rgba(pat,1,&(obj->stopcolor));
  333. }
  334. cairo_set_source (cr, pat);
  335. cairo_fill_preserve (cr);
  336. cairo_pattern_destroy (pat);
  337. }
  338. static void cairogen_ellipse(GVJ_t * job, pointf * A, int filled)
  339. {
  340. obj_state_t *obj = job->obj;
  341. cairo_t *cr = job->context;
  342. cairo_matrix_t matrix;
  343. double rx, ry;
  344. cairogen_set_penstyle(job, cr);
  345. cairo_get_matrix(cr, &matrix);
  346. rx = A[1].x - A[0].x;
  347. ry = A[1].y - A[0].y;
  348. #define RMIN 0.01
  349. rx = fmax(rx, RMIN);
  350. ry = fmax(ry, RMIN);
  351. cairo_translate(cr, A[0].x, -A[0].y);
  352. cairo_scale(cr, rx, ry);
  353. cairo_move_to(cr, 1., 0.);
  354. cairo_arc(cr, 0., 0., 1., 0., 2 * M_PI);
  355. cairo_set_matrix(cr, &matrix);
  356. if (filled == GRADIENT || filled == RGRADIENT) {
  357. cairo_gradient_fill (cr, obj, filled, A, 2);
  358. }
  359. else if (filled) {
  360. cairogen_set_color(cr, &obj->fillcolor);
  361. cairo_fill_preserve(cr);
  362. }
  363. cairogen_set_color(cr, &obj->pencolor);
  364. cairo_stroke(cr);
  365. }
  366. static void cairogen_polygon(GVJ_t *job, pointf *A, size_t n, int filled) {
  367. obj_state_t *obj = job->obj;
  368. cairo_t *cr = job->context;
  369. cairogen_set_penstyle(job, cr);
  370. cairo_move_to(cr, A[0].x, -A[0].y);
  371. for (size_t i = 1; i < n; i++)
  372. cairo_line_to(cr, A[i].x, -A[i].y);
  373. cairo_close_path(cr);
  374. if (filled == GRADIENT || filled == RGRADIENT) {
  375. cairo_gradient_fill(cr, obj, filled, A, n);
  376. }
  377. else if (filled) {
  378. cairogen_set_color(cr, &obj->fillcolor);
  379. cairo_fill_preserve(cr);
  380. }
  381. cairogen_set_color(cr, &obj->pencolor);
  382. cairo_stroke(cr);
  383. }
  384. static void cairogen_bezier(GVJ_t *job, pointf *A, size_t n, int filled) {
  385. obj_state_t *obj = job->obj;
  386. cairo_t *cr = job->context;
  387. cairogen_set_penstyle(job, cr);
  388. cairo_move_to(cr, A[0].x, -A[0].y);
  389. for (size_t i = 1; i < n; i += 3)
  390. cairo_curve_to(cr, A[i].x, -A[i].y, A[i + 1].x, -A[i + 1].y,
  391. A[i + 2].x, -A[i + 2].y);
  392. if (filled == GRADIENT || filled == RGRADIENT) {
  393. cairo_gradient_fill(cr, obj, filled, A, n);
  394. }
  395. else if (filled) {
  396. cairogen_set_color(cr, &obj->fillcolor);
  397. cairo_fill_preserve(cr);
  398. }
  399. cairogen_set_color(cr, &obj->pencolor);
  400. cairo_stroke(cr);
  401. }
  402. static void cairogen_polyline(GVJ_t *job, pointf *A, size_t n) {
  403. obj_state_t *obj = job->obj;
  404. cairo_t *cr = job->context;
  405. cairogen_set_penstyle(job, cr);
  406. cairo_move_to(cr, A[0].x, -A[0].y);
  407. for (size_t i = 1; i < n; i++)
  408. cairo_line_to(cr, A[i].x, -A[i].y);
  409. cairogen_set_color(cr, &obj->pencolor);
  410. cairo_stroke(cr);
  411. }
  412. static gvrender_engine_t cairogen_engine = {
  413. cairogen_begin_job,
  414. cairogen_end_job,
  415. 0, /* cairogen_begin_graph */
  416. 0, /* cairogen_end_graph */
  417. 0, /* cairogen_begin_layer */
  418. 0, /* cairogen_end_layer */
  419. cairogen_begin_page,
  420. cairogen_end_page,
  421. 0, /* cairogen_begin_cluster */
  422. 0, /* cairogen_end_cluster */
  423. 0, /* cairogen_begin_nodes */
  424. 0, /* cairogen_end_nodes */
  425. 0, /* cairogen_begin_edges */
  426. 0, /* cairogen_end_edges */
  427. 0, /* cairogen_begin_node */
  428. 0, /* cairogen_end_node */
  429. 0, /* cairogen_begin_edge */
  430. 0, /* cairogen_end_edge */
  431. cairogen_begin_anchor, /* cairogen_begin_anchor */
  432. 0, /* cairogen_end_anchor */
  433. 0, /* cairogen_begin_label */
  434. 0, /* cairogen_end_label */
  435. cairogen_textspan,
  436. 0, /* cairogen_resolve_color */
  437. cairogen_ellipse,
  438. cairogen_polygon,
  439. cairogen_bezier,
  440. cairogen_polyline,
  441. 0, /* cairogen_comment */
  442. 0, /* cairogen_library_shape */
  443. };
  444. static gvrender_features_t render_features_cairo = {
  445. GVRENDER_Y_GOES_DOWN
  446. | GVRENDER_DOES_TRANSFORM, /* flags */
  447. 4., /* default pad - graph units */
  448. 0, /* knowncolors */
  449. 0, /* sizeof knowncolors */
  450. RGBA_DOUBLE, /* color_type */
  451. };
  452. static gvdevice_features_t device_features_png = {
  453. GVDEVICE_BINARY_FORMAT
  454. | GVDEVICE_DOES_TRUECOLOR,/* flags */
  455. {0.,0.}, /* default margin - points */
  456. {0.,0.}, /* default page width, height - points */
  457. {96.,96.}, /* typical monitor dpi */
  458. };
  459. static gvdevice_features_t device_features_ps = {
  460. GVRENDER_NO_WHITE_BG
  461. | GVDEVICE_DOES_TRUECOLOR, /* flags */
  462. {36.,36.}, /* default margin - points */
  463. {0.,0.}, /* default page width, height - points */
  464. {72.,72.}, /* postscript 72 dpi */
  465. };
  466. static gvdevice_features_t device_features_eps = {
  467. GVRENDER_NO_WHITE_BG
  468. | GVDEVICE_DOES_TRUECOLOR, /* flags */
  469. {36.,36.}, /* default margin - points */
  470. {0.,0.}, /* default page width, height - points */
  471. {72.,72.}, /* postscript 72 dpi */
  472. };
  473. static gvdevice_features_t device_features_pdf = {
  474. GVDEVICE_BINARY_FORMAT
  475. | GVRENDER_NO_WHITE_BG
  476. | GVRENDER_DOES_MAPS
  477. | GVRENDER_DOES_MAP_RECTANGLE
  478. | GVDEVICE_DOES_TRUECOLOR,/* flags */
  479. {36.,36.}, /* default margin - points */
  480. {0.,0.}, /* default page width, height - points */
  481. {72.,72.}, /* postscript 72 dpi */
  482. };
  483. static gvdevice_features_t device_features_svg = {
  484. GVRENDER_NO_WHITE_BG
  485. | GVDEVICE_DOES_TRUECOLOR, /* flags */
  486. {0.,0.}, /* default margin - points */
  487. {0.,0.}, /* default page width, height - points */
  488. {72.,72.}, /* svg 72 dpi */
  489. };
  490. gvplugin_installed_t gvrender_pango_types[] = {
  491. {FORMAT_CAIRO, "cairo", 10, &cairogen_engine, &render_features_cairo},
  492. {0, NULL, 0, NULL, NULL}
  493. };
  494. gvplugin_installed_t gvdevice_pango_types[] = {
  495. #ifdef CAIRO_HAS_PNG_FUNCTIONS
  496. {FORMAT_PNG, "png:cairo", 10, NULL, &device_features_png},
  497. #endif
  498. #ifdef CAIRO_HAS_PS_SURFACE
  499. {FORMAT_PS, "ps:cairo", -10, NULL, &device_features_ps},
  500. {FORMAT_EPS, "eps:cairo", -10, NULL, &device_features_eps},
  501. #endif
  502. #ifdef CAIRO_HAS_PDF_SURFACE
  503. {FORMAT_PDF, "pdf:cairo", 10, NULL, &device_features_pdf},
  504. #endif
  505. #ifdef CAIRO_HAS_SVG_SURFACE
  506. {FORMAT_SVG, "svg:cairo", -10, NULL, &device_features_svg},
  507. #endif
  508. {0, NULL, 0, NULL, NULL}
  509. };